kritzel-stencil 0.4.8 → 0.4.10

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 (166) hide show
  1. package/dist/cjs/index.cjs.js +9 -1
  2. package/dist/cjs/kritzel-active-users_47.cjs.entry.js +1427 -1289
  3. package/dist/cjs/loader.cjs.js +1 -1
  4. package/dist/cjs/{schema.constants-gDeS7USU.js → schema.constants-C6PknABJ.js} +81 -6
  5. package/dist/cjs/stencil.cjs.js +1 -1
  6. package/dist/collection/classes/objects/image.class.js +9 -0
  7. package/dist/collection/classes/objects/path.class.js +9 -0
  8. package/dist/collection/classes/objects/shape.class.js +9 -0
  9. package/dist/collection/classes/providers/sync/broadcast-sync-provider.class.js +1 -0
  10. package/dist/collection/classes/providers/sync/hocuspocus-sync-provider.class.js +2 -0
  11. package/dist/collection/classes/providers/sync/in-memory-sync-provider.class.js +1 -0
  12. package/dist/collection/classes/providers/sync/indexeddb-sync-provider.class.js +1 -0
  13. package/dist/collection/classes/providers/sync/websocket-sync-provider.class.js +2 -0
  14. package/dist/collection/components/core/kritzel-editor/kritzel-editor.js +201 -67
  15. package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +137 -170
  16. package/dist/collection/components/core/kritzel-watermark/kritzel-watermark.js +1 -1
  17. package/dist/collection/components/shared/kritzel-avatar/kritzel-avatar.js +3 -3
  18. package/dist/collection/components/shared/kritzel-button/kritzel-button.js +2 -2
  19. package/dist/collection/components/shared/kritzel-color/kritzel-color.js +2 -2
  20. package/dist/collection/components/shared/kritzel-color-palette/kritzel-color-palette.js +1 -1
  21. package/dist/collection/components/shared/kritzel-font/kritzel-font.js +1 -1
  22. package/dist/collection/components/shared/kritzel-font-size/kritzel-font-size.js +1 -1
  23. package/dist/collection/components/shared/kritzel-input/kritzel-input.js +1 -1
  24. package/dist/collection/components/shared/kritzel-master-detail/kritzel-master-detail.js +3 -3
  25. package/dist/collection/components/shared/kritzel-menu/kritzel-menu.js +1 -1
  26. package/dist/collection/components/shared/kritzel-menu-item/kritzel-menu-item.js +2 -2
  27. package/dist/collection/components/shared/kritzel-numeric-input/kritzel-numeric-input.js +1 -1
  28. package/dist/collection/components/shared/kritzel-opacity-slider/kritzel-opacity-slider.js +1 -1
  29. package/dist/collection/components/shared/kritzel-portal/kritzel-portal.js +1 -1
  30. package/dist/collection/components/shared/kritzel-slide-toggle/kritzel-slide-toggle.js +1 -1
  31. package/dist/collection/components/shared/kritzel-split-button/kritzel-split-button.js +1 -1
  32. package/dist/collection/components/shared/kritzel-stroke-size/kritzel-stroke-size.js +1 -1
  33. package/dist/collection/components/shared/kritzel-sync-indicator/kritzel-sync-indicator.css +5 -5
  34. package/dist/collection/components/shared/kritzel-sync-indicator/kritzel-sync-indicator.js +1 -1
  35. package/dist/collection/components/shared/kritzel-tooltip/kritzel-tooltip.js +2 -2
  36. package/dist/collection/components/ui/kritzel-back-to-content/kritzel-back-to-content.js +1 -1
  37. package/dist/collection/components/ui/kritzel-controls/kritzel-controls.js +7 -7
  38. package/dist/collection/components/ui/kritzel-current-user/kritzel-current-user.js +2 -2
  39. package/dist/collection/components/ui/kritzel-current-user-dialog/kritzel-current-user-dialog.js +2 -2
  40. package/dist/collection/components/ui/kritzel-export/kritzel-export.js +2 -2
  41. package/dist/collection/components/ui/kritzel-loading-overlay/kritzel-loading-overlay.css +6 -6
  42. package/dist/collection/components/ui/kritzel-loading-overlay/kritzel-loading-overlay.js +1 -1
  43. package/dist/collection/components/ui/kritzel-login-dialog/kritzel-login-dialog.js +1 -1
  44. package/dist/collection/components/ui/kritzel-more-menu/kritzel-more-menu.js +91 -5
  45. package/dist/collection/components/ui/kritzel-settings/kritzel-settings.js +2 -2
  46. package/dist/collection/components/ui/kritzel-share-dialog/kritzel-share-dialog.js +3 -3
  47. package/dist/collection/components/ui/kritzel-tool-config/kritzel-tool-config.js +1 -1
  48. package/dist/collection/components/ui/kritzel-utility-panel/kritzel-utility-panel.js +2 -2
  49. package/dist/collection/components/ui/kritzel-workspace-manager/kritzel-workspace-manager.js +1 -1
  50. package/dist/collection/components/ui/kritzel-zoom-panel/kritzel-zoom-panel.js +2 -2
  51. package/dist/collection/constants/version.js +1 -1
  52. package/dist/collection/helpers/object.helper.js +11 -0
  53. package/dist/collection/index.js +1 -0
  54. package/dist/collection/interfaces/object-export.interface.js +1 -0
  55. package/dist/collection/interfaces/sync-config.interface.js +21 -1
  56. package/dist/collection/locales/de-locale.js +2 -2
  57. package/dist/collection/locales/en-locale.js +2 -2
  58. package/dist/collection/locales/fr-locale.js +2 -2
  59. package/dist/collection/themes/dark-theme.js +7 -0
  60. package/dist/collection/themes/light-theme.js +7 -0
  61. package/dist/components/index.js +11 -4
  62. package/dist/components/kritzel-active-users.js +1 -1
  63. package/dist/components/kritzel-avatar.js +1 -1
  64. package/dist/components/kritzel-awareness-cursors.js +1 -1
  65. package/dist/components/kritzel-back-to-content.js +1 -1
  66. package/dist/components/kritzel-button.js +1 -1
  67. package/dist/components/kritzel-color-palette.js +1 -1
  68. package/dist/components/kritzel-color.js +1 -1
  69. package/dist/components/kritzel-controls.js +1 -1
  70. package/dist/components/kritzel-current-user-dialog.js +1 -1
  71. package/dist/components/kritzel-current-user.js +1 -1
  72. package/dist/components/kritzel-editor.js +200 -99
  73. package/dist/components/kritzel-engine.js +1 -1
  74. package/dist/components/kritzel-export.js +1 -1
  75. package/dist/components/kritzel-font-size.js +1 -1
  76. package/dist/components/kritzel-font.js +1 -1
  77. package/dist/components/kritzel-input.js +1 -1
  78. package/dist/components/kritzel-loading-overlay.js +1 -1
  79. package/dist/components/kritzel-login-dialog.js +1 -1
  80. package/dist/components/kritzel-master-detail.js +1 -1
  81. package/dist/components/kritzel-menu-item.js +1 -1
  82. package/dist/components/kritzel-menu.js +1 -1
  83. package/dist/components/kritzel-more-menu.js +1 -1
  84. package/dist/components/kritzel-numeric-input.js +1 -1
  85. package/dist/components/kritzel-opacity-slider.js +1 -1
  86. package/dist/components/kritzel-portal.js +1 -1
  87. package/dist/components/kritzel-settings.js +1 -1
  88. package/dist/components/kritzel-share-dialog.js +1 -1
  89. package/dist/components/kritzel-slide-toggle.js +1 -1
  90. package/dist/components/kritzel-split-button.js +1 -1
  91. package/dist/components/kritzel-stroke-size.js +1 -1
  92. package/dist/components/kritzel-sync-indicator.js +1 -1
  93. package/dist/components/kritzel-tool-config.js +1 -1
  94. package/dist/components/kritzel-tooltip.js +1 -1
  95. package/dist/components/kritzel-utility-panel.js +1 -1
  96. package/dist/components/kritzel-watermark.js +1 -1
  97. package/dist/components/kritzel-workspace-manager.js +1 -1
  98. package/dist/components/kritzel-zoom-panel.js +1 -1
  99. package/dist/components/{p-8xkoVMfO.js → p-2WiGyky4.js} +2 -2
  100. package/dist/components/{p-D8TOEhPY.js → p-30g2W_Gx.js} +3 -3
  101. package/dist/components/{p-DXcjIb_a.js → p-4SPnZZkH.js} +3 -3
  102. package/dist/components/{p-CURiiQfq.js → p-7I4Uzjrw.js} +14 -0
  103. package/dist/components/{p-9m1cAVwG.js → p-B-585egz.js} +3 -3
  104. package/dist/components/{p-COjsYPir.js → p-BP6Fo3rt.js} +2 -2
  105. package/dist/components/{p-BLOPBDUF.js → p-BPsKYMH6.js} +2 -2
  106. package/dist/components/{p-DkY7pIg5.js → p-BX1aRLEi.js} +95 -6
  107. package/dist/components/{p-BMRln59F.js → p-B_lZQo3s.js} +3 -3
  108. package/dist/components/{p-oGAPPwAL.js → p-BoN700-O.js} +2 -2
  109. package/dist/components/{p-BzJEwFtn.js → p-BozrueXV.js} +1 -1
  110. package/dist/components/{p-DwY5pghH.js → p-Bvv1pGFD.js} +1 -1
  111. package/dist/components/{p-D8bHNNEm.js → p-C6e7eOEb.js} +1238 -1241
  112. package/dist/components/{p-CM1duewg.js → p-CAoN_STZ.js} +2 -2
  113. package/dist/components/{p-CVhcOiEA.js → p-CM65T33m.js} +3 -3
  114. package/dist/components/{p-BknTMcbz.js → p-COylwYw1.js} +16 -16
  115. package/dist/components/{p-BYO6pz8T.js → p-CZ99VWQc.js} +1 -1
  116. package/dist/components/{p-DrQRChp3.js → p-CaBbajU2.js} +2 -2
  117. package/dist/components/{p-BzrxIesp.js → p-CrHvaLRV.js} +1 -1
  118. package/dist/components/{p-BKOsmXB5.js → p-D-UkGru5.js} +4 -4
  119. package/dist/components/{p-BVEGQLBD.js → p-DB58Oa5V.js} +2 -2
  120. package/dist/components/{p-yD_3Z3K1.js → p-DBqoPgev.js} +1 -1
  121. package/dist/components/{p-DIMNEOTW.js → p-DKGg-76-.js} +1 -1
  122. package/dist/components/{p-Dd-pMvlr.js → p-DPONTran.js} +11 -0
  123. package/dist/components/{p-DgnK50xO.js → p-DVXTPI71.js} +1 -1
  124. package/dist/components/{p-DgA865Tp.js → p-DdabFzSa.js} +5 -5
  125. package/dist/components/{p-CMZ09rB1.js → p-Dt7-I2Xz.js} +3 -3
  126. package/dist/components/{p-CwuGA058.js → p-DtnMdWWB.js} +1 -1
  127. package/dist/components/{p-7Rt7ml3v.js → p-Dv4pZx8G.js} +1 -1
  128. package/dist/components/{p-BO9BvTtK.js → p-DwMqCX2P.js} +2 -2
  129. package/dist/components/{p-Cr2z2UBL.js → p-Dy3uucl_.js} +7 -7
  130. package/dist/components/{p-Ckm1Aq0V.js → p-GKmqCI8r.js} +2 -2
  131. package/dist/components/{p-DSOc0eIT.js → p-PjPPCHVL.js} +1 -1
  132. package/dist/components/{p-OLfoATNk.js → p-ShJrZeyt.js} +1 -1
  133. package/dist/components/{p-0H4HOJjy.js → p-bAacqNHq.js} +11 -2
  134. package/dist/components/{p-BU-FZuj0.js → p-vqEUQ-4Q.js} +3 -3
  135. package/dist/components/{p-DDbO5q-e.js → p-x3z0Cz1B.js} +4 -4
  136. package/dist/esm/index.js +9 -2
  137. package/dist/esm/kritzel-active-users_47.entry.js +1427 -1289
  138. package/dist/esm/loader.js +1 -1
  139. package/dist/esm/{schema.constants-Rpm1NA9u.js → schema.constants-C-oQbvCY.js} +81 -7
  140. package/dist/esm/stencil.js +1 -1
  141. package/dist/stencil/index.esm.js +9 -2
  142. package/dist/stencil/{p-Rpm1NA9u.js → p-C-oQbvCY.js} +81 -7
  143. package/dist/stencil/{p-8c1b7b08.entry.js → p-be87f9d3.entry.js} +1427 -1289
  144. package/dist/stencil/stencil.esm.js +1 -1
  145. package/dist/types/classes/objects/image.class.d.ts +3 -0
  146. package/dist/types/classes/objects/path.class.d.ts +3 -0
  147. package/dist/types/classes/objects/shape.class.d.ts +3 -0
  148. package/dist/types/classes/providers/sync/broadcast-sync-provider.class.d.ts +1 -0
  149. package/dist/types/classes/providers/sync/hocuspocus-sync-provider.class.d.ts +1 -0
  150. package/dist/types/classes/providers/sync/in-memory-sync-provider.class.d.ts +1 -0
  151. package/dist/types/classes/providers/sync/indexeddb-sync-provider.class.d.ts +1 -0
  152. package/dist/types/classes/providers/sync/websocket-sync-provider.class.d.ts +1 -0
  153. package/dist/types/components/core/kritzel-editor/kritzel-editor.d.ts +17 -3
  154. package/dist/types/components/core/kritzel-engine/kritzel-engine.d.ts +16 -34
  155. package/dist/types/components/ui/kritzel-more-menu/kritzel-more-menu.d.ts +10 -1
  156. package/dist/types/components.d.ts +27 -26
  157. package/dist/types/constants/version.d.ts +1 -1
  158. package/dist/types/helpers/object.helper.d.ts +4 -0
  159. package/dist/types/index.d.ts +1 -0
  160. package/dist/types/interfaces/localization.interface.d.ts +1 -1
  161. package/dist/types/interfaces/menu-item.interface.d.ts +1 -1
  162. package/dist/types/interfaces/object-export.interface.d.ts +20 -0
  163. package/dist/types/interfaces/sync-config.interface.d.ts +5 -0
  164. package/dist/types/interfaces/sync-provider.interface.d.ts +5 -0
  165. package/dist/types/interfaces/theme.interface.d.ts +11 -5
  166. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var index = require('./index-Xav9JFHg.js');
4
- var schema_constants = require('./schema.constants-gDeS7USU.js');
4
+ var schema_constants = require('./schema.constants-C6PknABJ.js');
5
5
  var Y = require('yjs');
6
6
  require('y-indexeddb');
7
7
  require('y-websocket');
@@ -158,16 +158,16 @@ const KritzelAvatar = class {
158
158
  height: `${this.size}px`,
159
159
  fontSize: `${Math.round(this.size * 0.4)}px`,
160
160
  };
161
- return (index.h(index.Host, { key: '116725d55e4443e80216b3b3251e957d2009b43e', style: containerStyles, class: {
161
+ return (index.h(index.Host, { key: '90ce966bb2a374e68c544906c8c746df6cc01038', style: containerStyles, class: {
162
162
  'has-image': !!showImage,
163
163
  'has-initials': !!showInitials,
164
164
  'has-default': !!showDefaultIcon,
165
- }, role: "img", "aria-label": this.getDisplayName() || 'User avatar' }, showImage && (index.h("img", { key: 'd70ad4eb484404b18c13e066e8c10b3c86918ee5', src: imageUrl, alt: "", class: "avatar-image", ref: (el) => {
165
+ }, role: "img", "aria-label": this.getDisplayName() || 'User avatar' }, showImage && (index.h("img", { key: '6799f4ad45c7d40ebaf640f191171ed6a5f14ae9', src: imageUrl, alt: "", class: "avatar-image", ref: (el) => {
166
166
  if (el) {
167
167
  el.referrerPolicy = 'no-referrer';
168
168
  el.crossOrigin = 'anonymous';
169
169
  }
170
- }, onError: this.handleImageError })), showInitials && (index.h("span", { key: '21a9022cffdb69e0c3626e513cb3c036a9d3a16d', class: "avatar-initials", style: { backgroundColor: this.getBackgroundColor() } }, initials)), showDefaultIcon && (index.h("span", { key: '43e676af007bf254b1921f3efcbc7fff4e204359', class: "avatar-default" }, index.h("svg", { key: 'a7378795539a902e28ca644a80f1ca00cc9f818a', viewBox: "0 0 24 24", fill: "currentColor" }, index.h("path", { key: '31548a495a86f033325c98cfcc0ae398b6bb8281', d: "M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" }))))));
170
+ }, onError: this.handleImageError })), showInitials && (index.h("span", { key: '98cdcda64b900994b61a03179be9f2af17c489e8', class: "avatar-initials", style: { backgroundColor: this.getBackgroundColor() } }, initials)), showDefaultIcon && (index.h("span", { key: '64e6a6ee67017c927dce0b51810a5bb02fc0d6d1', class: "avatar-default" }, index.h("svg", { key: '42f99637a3c23aaf10fa7c99753347ede1d518c0', viewBox: "0 0 24 24", fill: "currentColor" }, index.h("path", { key: '28b52a68b36e98b0e14891fd225457be65f1bb46', d: "M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" }))))));
171
171
  }
172
172
  static get watchers() { return {
173
173
  "user": [{
@@ -492,7 +492,7 @@ const KritzelBackToContent = class {
492
492
  this.backToContent.emit();
493
493
  };
494
494
  render() {
495
- return (index.h(index.Host, { key: '86a94bdc5bb02977214f999b7b880ce3193b6d8f' }, index.h("button", { key: '6a07c13c9986ebef6100140f7e702a1dfb5c2033', class: { 'back-to-content-button': true, visible: this.visible }, onClick: this.handleClick, "aria-label": this.text }, index.h("kritzel-icon", { key: 'ebadc99b15f2f015ada8795fa05a3a64ccac9cb5', name: "chevrons-left" }))));
495
+ return (index.h(index.Host, { key: '2576489b9f97c77d1931e7c8496bae968d998f9d' }, index.h("button", { key: 'da891543ce8a9b5ccd2f4a461bdcdc7f3f9ad266', class: { 'back-to-content-button': true, visible: this.visible }, onClick: this.handleClick, "aria-label": this.text }, index.h("kritzel-icon", { key: '4559cc932918707d5961c6237f3c5e23c9e4cc9d', name: "chevrons-left" }))));
496
496
  }
497
497
  };
498
498
  KritzelBackToContent.style = kritzelBackToContentCss();
@@ -519,11 +519,11 @@ const KritzelButton = class {
519
519
  this.buttonClick.emit();
520
520
  };
521
521
  render() {
522
- return (index.h(index.Host, { key: 'b9de2a1261ca0cb491f6980db6a596c48fa45b3a' }, index.h("button", { key: 'a9507ea0f6a1d9fcb19a7df16bea54cc0ead24f9', type: this.type, class: {
522
+ return (index.h(index.Host, { key: '51621006f6b085db2b43accf52192f0674d2c50c' }, index.h("button", { key: '01ab2d96abe5ad34285e59f6fec66f6b21e91849', type: this.type, class: {
523
523
  'kritzel-button': true,
524
524
  [this.variant]: true,
525
525
  'disabled': this.disabled,
526
- }, disabled: this.disabled, onClick: this.handleClick }, index.h("slot", { key: '8449a26d4f9fe0226c6150a6639c522486b59780' }))));
526
+ }, disabled: this.disabled, onClick: this.handleClick }, index.h("slot", { key: '4b085139ea85ad9f836059f4cf2dde15b948e0e2' }))));
527
527
  }
528
528
  };
529
529
  KritzelButton.style = kritzelButtonCss();
@@ -571,13 +571,13 @@ const KritzelColorComponent = class {
571
571
  render() {
572
572
  const resolvedColor = this.resolveColor();
573
573
  const isColorVeryLight = this.isLightColor(resolvedColor);
574
- return (index.h(index.Host, { key: 'e39c77c55942b9c615e404efabf4939b2cf05a34' }, index.h("div", { key: 'ac343295592ea80ca7d3da61ca1244dd1aaa50db', class: "checkerboard-bg", style: {
574
+ return (index.h(index.Host, { key: 'e797bfa6a2aed82d1913ec2f02f8b25a063564e5' }, index.h("div", { key: '5b62fcdbb2985b90fe0d7c7b65b507ec1c55e240', class: "checkerboard-bg", style: {
575
575
  width: `${this.size}px`,
576
576
  height: `${this.size}px`,
577
577
  borderRadius: '50%',
578
578
  display: 'inline-block',
579
579
  position: 'relative',
580
- } }, index.h("div", { key: '4c87934b3febd4c41874cd290d86a9a9a557e3e5', class: {
580
+ } }, index.h("div", { key: '2d62717ebf81be93219ebda7071c0ba992d7441a', class: {
581
581
  'color-circle': true,
582
582
  'white': isColorVeryLight,
583
583
  }, style: {
@@ -633,7 +633,7 @@ const KritzelColorPalette = class {
633
633
  render() {
634
634
  const displayedColors = this.isExpanded ? this.colors : this.colors.slice(0, 6);
635
635
  const expandedHeight = this.isExpanded ? this.calculateHeight() : '32px';
636
- return (index.h(index.Host, { key: '16aa405a43b3943473e577c5a6ab86adf4ae1ba9' }, index.h("div", { key: 'f65bb286889700dc1f0e8d84ff59721071b7b73a', class: {
636
+ return (index.h(index.Host, { key: '9a637b3a6946384a889f54dc3a635da7ff014439' }, index.h("div", { key: '4db8bbc7a234bfed26b60ed306a5f999875e2195', class: {
637
637
  'color-grid': true,
638
638
  'expanded': this.isExpanded,
639
639
  }, style: {
@@ -1202,13 +1202,13 @@ const KritzelControls = class {
1202
1202
  // Separate tool controls from config control
1203
1203
  const toolControls = this.internalControls.filter(c => c.type === 'tool' || c.type === 'separator');
1204
1204
  const configControl = this.internalControls.find(c => c.type === 'config' && c.name === this.firstConfig?.name);
1205
- return (index.h(index.Host, { key: 'a72f9b1befd185b67d24f6d7a4987ebeb4d6742d', style: { display: this.visible ? '' : 'none' }, class: {
1205
+ return (index.h(index.Host, { key: '2a7595b72594df3e8ff9a103c3ff7dd70f0a8045', style: { display: this.visible ? '' : 'none' }, class: {
1206
1206
  mobile: this.isTouchDevice,
1207
- } }, this.isUtilityPanelVisible && (index.h("kritzel-utility-panel", { key: '4e69324b23542878919119d10db98fc44b0dc536', style: {
1207
+ } }, this.isUtilityPanelVisible && (index.h("kritzel-utility-panel", { key: '1c2d9f14c53944f291cf49cbcd1bf282ad8a8683', style: {
1208
1208
  position: 'absolute',
1209
1209
  bottom: '56px',
1210
1210
  left: '12px',
1211
- }, undoState: this.undoState, terms: this.terms, onUndo: () => this.kritzelEngine?.undo(), onRedo: () => this.kritzelEngine?.redo(), onDelete: () => this.kritzelEngine?.delete() })), index.h("div", { key: 'f4c7b461a433055512f5b6d397ab69d470531285', class: "kritzel-controls" }, index.h("div", { key: '53c27ef4ac2a9121f07ce91c82e63de5d7029de6', class: { 'scroll-indicator-left': true, 'visible': this.canScrollLeft } }), index.h("div", { key: '43d90c6031e164843dd43563d4fe75410b990278', class: "kritzel-tools-scroll", ref: el => (this.toolsScrollRef = el), onScroll: this.handleToolsScroll }, toolControls.map(control => {
1211
+ }, undoState: this.undoState, terms: this.terms, onUndo: () => this.kritzelEngine?.undo(), onRedo: () => this.kritzelEngine?.redo(), onDelete: () => this.kritzelEngine?.delete() })), index.h("div", { key: '8ac30e183885bc106033ca163aa7e5927802e67c', class: "kritzel-controls" }, index.h("div", { key: '74b2808a506e671682aed91107c7c9e27aa4b2c7', class: { 'scroll-indicator-left': true, 'visible': this.canScrollLeft } }), index.h("div", { key: '3fd693c993d5b11eadd2eec7481214fbc1550301', class: "kritzel-tools-scroll", ref: el => (this.toolsScrollRef = el), onScroll: this.handleToolsScroll }, toolControls.map(control => {
1212
1212
  // Check if this control has sub-options (split-button)
1213
1213
  if (control.subOptions?.length) {
1214
1214
  const selectedSubOption = this.getSelectedSubOption(control);
@@ -1238,10 +1238,10 @@ const KritzelControls = class {
1238
1238
  'kritzel-control': true,
1239
1239
  'selected': this.activeControl?.name === control?.name,
1240
1240
  }, key: control.name, "data-testid": `tool-${control.name}`, onClick: _event => this.handleControlClick?.(control), "aria-label": control.name.charAt(0).toUpperCase() + control.name.slice(1) }, index.h("kritzel-icon", { name: control.icon })));
1241
- })), index.h("div", { key: '9a8bd38a8266f1bb5feddcb4e396bb20c25512fa', class: { 'scroll-indicator-right': true, 'visible': this.canScrollRight && !(configControl && this.activeControl && hasConfigUI) } }), configControl && this.activeControl && (index.h("div", { class: {
1241
+ })), index.h("div", { key: '6f16a8332dc6a082f2e6aedbec3e31398f2a8b66', class: { 'scroll-indicator-right': true, 'visible': this.canScrollRight && !(configControl && this.activeControl && hasConfigUI) } }), configControl && this.activeControl && (index.h("div", { class: {
1242
1242
  'kritzel-config-container': true,
1243
1243
  'visible': hasConfigUI,
1244
- }, key: configControl.name }, index.h("div", { key: 'c889744c924d29f1e65cf67263be9ff708916610', class: { 'config-gradient-left': true, 'visible': this.needsScrolling } }), index.h("kritzel-tooltip", { key: '9df35a6967a9738e24d328865b07b50328cef811', anchorElement: this.host.shadowRoot?.querySelector('.kritzel-config-container'), triggerElement: this.configTriggerRef }, index.h("kritzel-tool-config", { key: '6f0079c879dc2ca5910193a02d19f246068d09e6', tool: this.activeControl.tool, theme: this.theme, engine: this.kritzelEngine, terms: this.terms, onToolChange: event => this.handleToolChange?.(event), onDisplayValuesChange: this.handleDisplayValuesChange, style: { width: '100%', height: '100%' } })), index.h("div", { key: 'a6af0ad5cc2fd0bbef0ade522f82ead53b30f92b', tabIndex: hasConfigUI ? 0 : -1, class: "kritzel-config", "data-testid": "tool-config", ref: el => {
1244
+ }, key: configControl.name }, index.h("div", { key: '554d743fccfb62d8c368f2a5cdca232ae9842b7f', class: { 'config-gradient-left': true, 'visible': this.needsScrolling } }), index.h("kritzel-tooltip", { key: 'af97d986d82c961d66c910f7edabd6e597ec59ab', anchorElement: this.host.shadowRoot?.querySelector('.kritzel-config-container'), triggerElement: this.configTriggerRef }, index.h("kritzel-tool-config", { key: '3721e5463a85881c7e806d69c0a9524f44732978', tool: this.activeControl.tool, theme: this.theme, engine: this.kritzelEngine, terms: this.terms, onToolChange: event => this.handleToolChange?.(event), onDisplayValuesChange: this.handleDisplayValuesChange, style: { width: '100%', height: '100%' } })), index.h("div", { key: 'c6e3fe4a4a9b781db394073c8f929533487be760', tabIndex: hasConfigUI ? 0 : -1, class: "kritzel-config", "data-testid": "tool-config", ref: el => {
1245
1245
  if (el)
1246
1246
  this.configTriggerRef = el;
1247
1247
  }, onKeyDown: event => {
@@ -1250,7 +1250,7 @@ const KritzelControls = class {
1250
1250
  }
1251
1251
  }, style: {
1252
1252
  cursor: 'pointer',
1253
- } }, this.displayValues && (index.h("div", { key: '07a7fb2e0dc5c53a389da7f742322fdf489e0bd7', class: "color-container" }, index.h("kritzel-color", { key: '2987b76ca66d58a12ee71dd4c7d82a9c42480690', value: this.displayValues.color, theme: this.theme, size: 18, style: {
1253
+ } }, this.displayValues && (index.h("div", { key: '9eeb3a9fc29f4848ee9f325d50b4cbfc5bfe5f1c', class: "color-container" }, index.h("kritzel-color", { key: '1d45edb7ed07a8d12ab55e374ea456b8e95c8d41', value: this.displayValues.color, theme: this.theme, size: 18, style: {
1254
1254
  borderRadius: '50%',
1255
1255
  border: 'none',
1256
1256
  } })))))))));
@@ -1284,7 +1284,7 @@ const KritzelCurrentUser = class {
1284
1284
  /** Resolved localized strings keyed by term key, supplied by the editor. */
1285
1285
  terms = {};
1286
1286
  render() {
1287
- return (index.h(index.Host, { key: '3c23677e98ba459144fafde994dc66a14068c5b1' }, index.h("kritzel-avatar", { key: 'e5c04bf871f7808ab0e380770a2a94ddab8eac83', user: this.user, size: this.avatarSize })));
1287
+ return (index.h(index.Host, { key: '19551eb12507f1384aedf3ca4ce5212eca509c24' }, index.h("kritzel-avatar", { key: 'e39405e8e6c2e77a8acf5f286fa3890e28e8ab17', user: this.user, size: this.avatarSize })));
1288
1288
  }
1289
1289
  };
1290
1290
  KritzelCurrentUser.style = kritzelCurrentUserCss();
@@ -1325,7 +1325,7 @@ const KritzelCurrentUserDialog = class {
1325
1325
  }
1326
1326
  render() {
1327
1327
  const displayName = this.getDisplayName();
1328
- return (index.h(index.Host, { key: 'd5be0723f615c45bac897d4f2a2ebe5c67b4769c' }, index.h("kritzel-dialog", { key: '4de42a3df3d18f96ddfbc362c05e98ade4aeaaf8', dialogTitle: this.terms['currentUser.dialogTitle'] ?? 'Account', isOpen: this.isDialogOpen, onDialogClose: this.closeDialog, size: "small", contained: true }, index.h("div", { key: '7908d833617b67a6663e41fd91599356ab66619b', class: "user-info" }, index.h("kritzel-avatar", { key: '93fd9ddb633ad9ad7e17a7c4292861b86ef132f1', user: this.user, size: 80 }), displayName && index.h("div", { key: 'dff873e3c8b643e7a52f212a3e00167adc301cf7', class: "user-name" }, displayName), this.user?.email && index.h("div", { key: '7b30a9930cd454ca040181cb11c1d000e408bfc8', class: "user-email" }, this.user.email), index.h("kritzel-button", { key: '04ea8cbadd90b149ee8f5bbac071040b74eb12b3', class: "logout-button", variant: "primary", onButtonClick: this.handleLogout }, this.terms['menu.logout'] ?? 'Logout')))));
1328
+ return (index.h(index.Host, { key: '8f8e63ac6f789f2d0ee43b8b7439634411a91ac9' }, index.h("kritzel-dialog", { key: '4022a7251881f669f290e0a8beb9a39ef6242b19', dialogTitle: this.terms['currentUser.dialogTitle'] ?? 'Account', isOpen: this.isDialogOpen, onDialogClose: this.closeDialog, size: "small", contained: true }, index.h("div", { key: '4671f884c121cfaefdd8a47bd52b30acc2d7b345', class: "user-info" }, index.h("kritzel-avatar", { key: 'a1847b4b67302f3b7179e4e2b751f7f2106b76d4', user: this.user, size: 80 }), displayName && index.h("div", { key: 'b3d09b4c99d76014a2e36260edce52c39f1081e1', class: "user-name" }, displayName), this.user?.email && index.h("div", { key: 'a76ce19cbd24ad536b022264dab6a83f4d1b36c2', class: "user-email" }, this.user.email), index.h("kritzel-button", { key: 'f125164a9d9362eedfbfff4fc2629f7ec055fc14', class: "logout-button", variant: "primary", onButtonClick: this.handleLogout }, this.terms['menu.logout'] ?? 'Logout')))));
1329
1329
  }
1330
1330
  };
1331
1331
  KritzelCurrentUserDialog.style = kritzelCurrentUserDialogCss();
@@ -2265,6 +2265,19 @@ const KritzelEditor = class {
2265
2265
  group: 'clipboard',
2266
2266
  action: (menu, _) => this.engineRef.paste(menu.x, menu.y),
2267
2267
  },
2268
+ {
2269
+ label: 'menu.export',
2270
+ icon: 'upload',
2271
+ group: 'clipboard',
2272
+ visible: (_, objects) => this.canExportObjects(objects),
2273
+ action: (_, objects) => {
2274
+ const format = this.getPreferredExportFormat(objects);
2275
+ if (!format) {
2276
+ return;
2277
+ }
2278
+ void this.engineRef.exportSelectedObject(format);
2279
+ },
2280
+ },
2268
2281
  {
2269
2282
  label: 'menu.order',
2270
2283
  icon: 'ordering',
@@ -2306,16 +2319,43 @@ const KritzelEditor = class {
2306
2319
  },
2307
2320
  ],
2308
2321
  },
2322
+ { label: 'menu.delete', icon: 'delete', group: 'edit', action: () => this.engineRef.delete() },
2323
+ ];
2324
+ moreMenuItems = [
2325
+ {
2326
+ id: 'share',
2327
+ label: 'menu.share',
2328
+ icon: 'share',
2329
+ isVisible: async () => schema_constants.hasRemoteSyncProvider(this.syncConfig),
2330
+ action: () => {
2331
+ if (!this.isLoggedIn && this.loginConfig) {
2332
+ this.openLoginDialog();
2333
+ return;
2334
+ }
2335
+ this.shareDialogRef.open();
2336
+ },
2337
+ },
2309
2338
  {
2339
+ id: 'export',
2310
2340
  label: 'menu.export',
2341
+ icon: 'upload',
2342
+ action: async () => {
2343
+ const preview = await this.engineRef.getScreenshot('png');
2344
+ this.exportRef.open(preview ?? undefined);
2345
+ },
2346
+ },
2347
+ {
2348
+ id: 'import',
2349
+ label: 'menu.import',
2311
2350
  icon: 'download',
2312
- group: 'export',
2313
- children: [
2314
- { label: 'menu.exportAsSvg', icon: 'download', action: () => this.engineRef.exportSelectedObjectsAsSvg() },
2315
- { label: 'menu.exportAsPng', icon: 'download', action: () => this.engineRef.exportSelectedObjectsAsPng() },
2316
- ],
2351
+ action: () => this.engineRef.importFromFile(),
2352
+ },
2353
+ {
2354
+ id: 'settings',
2355
+ label: 'menu.settings',
2356
+ icon: 'settings',
2357
+ action: () => this.settingsRef.open(),
2317
2358
  },
2318
- { label: 'menu.delete', icon: 'delete', group: 'edit', action: () => this.engineRef.delete() },
2319
2359
  ];
2320
2360
  themes;
2321
2361
  theme = 'light';
@@ -2387,6 +2427,9 @@ const KritzelEditor = class {
2387
2427
  isNotificationDismissing = false;
2388
2428
  contextMenuState;
2389
2429
  normalizedControls = [];
2430
+ resolvedMoreMenuItems = [];
2431
+ isSyncing = false;
2432
+ isLoadingOverlayVisible = false;
2390
2433
  notificationDismissTimeout;
2391
2434
  notificationDismissAnimationTimeout;
2392
2435
  notificationDisplayDurationMs = 5000;
@@ -2429,6 +2472,18 @@ const KritzelEditor = class {
2429
2472
  onControlsChange(newValue) {
2430
2473
  this.normalizedControls = this.normalizeControls(newValue);
2431
2474
  }
2475
+ onMoreMenuItemsChange() {
2476
+ this.recomputeMoreMenuItems();
2477
+ }
2478
+ onSyncConfigChange() {
2479
+ this.recomputeMoreMenuItems();
2480
+ }
2481
+ onLoginConfigChange() {
2482
+ this.recomputeMoreMenuItems();
2483
+ }
2484
+ onUserChange() {
2485
+ this.recomputeMoreMenuItems();
2486
+ }
2432
2487
  onActiveWorkspaceIdChange(newId) {
2433
2488
  // If no ID is provided, do nothing (allows other logic to control activeWorkspace)
2434
2489
  if (!newId) {
@@ -2478,6 +2533,7 @@ const KritzelEditor = class {
2478
2533
  }
2479
2534
  this.resolvedTerms = await this.engineRef.getResolvedTerms();
2480
2535
  this.availableLocaleOptions = await this.engineRef.getAvailableLocaleOptions();
2536
+ this.recomputeMoreMenuItems();
2481
2537
  }
2482
2538
  onTouchStart(event) {
2483
2539
  if (event.cancelable) {
@@ -2638,11 +2694,14 @@ const KritzelEditor = class {
2638
2694
  async exportViewportAsSvg(options) {
2639
2695
  return this.engineRef.exportViewportAsSvg(options);
2640
2696
  }
2641
- async exportSelectedObjectsAsPng() {
2642
- return this.engineRef.exportSelectedObjectsAsPng();
2697
+ async getSelectedObjectSupportedExportFormats() {
2698
+ return this.engineRef.getSelectedObjectSupportedExportFormats();
2643
2699
  }
2644
- async exportSelectedObjectsAsSvg() {
2645
- return this.engineRef.exportSelectedObjectsAsSvg();
2700
+ async canExportSelectedObjectAs(format) {
2701
+ return this.engineRef.canExportSelectedObjectAs(format);
2702
+ }
2703
+ async exportSelectedObject(format) {
2704
+ return this.engineRef.exportSelectedObject(format);
2646
2705
  }
2647
2706
  async downloadAsJson(filename) {
2648
2707
  return this.engineRef.downloadAsJson(filename);
@@ -2709,6 +2768,7 @@ const KritzelEditor = class {
2709
2768
  currentUserDialogRef;
2710
2769
  componentWillLoad() {
2711
2770
  this.normalizedControls = this.normalizeControls(this.controls);
2771
+ this.recomputeMoreMenuItems();
2712
2772
  this.loadSettingsFromStorage();
2713
2773
  this.applyTheme();
2714
2774
  }
@@ -2729,13 +2789,18 @@ const KritzelEditor = class {
2729
2789
  };
2730
2790
  });
2731
2791
  }
2792
+ recomputeMoreMenuItems() {
2793
+ if (this.moreMenuItems) {
2794
+ this.resolvedMoreMenuItems = this.moreMenuItems;
2795
+ return;
2796
+ }
2797
+ }
2732
2798
  applyTheme() {
2733
2799
  const themeObj = this.resolveThemeObject();
2734
2800
  schema_constants.ThemeHelper.applyThemeToElement(this.host, themeObj);
2735
2801
  }
2736
2802
  resolveThemeObject() {
2737
- return this.themes?.find(t => t.name === this.theme)
2738
- ?? (this.theme === 'dark' ? schema_constants.darkTheme : schema_constants.lightTheme);
2803
+ return this.themes?.find(t => t.name === this.theme) ?? (this.theme === 'dark' ? schema_constants.darkTheme : schema_constants.lightTheme);
2739
2804
  }
2740
2805
  componentDidLoad() {
2741
2806
  this.registerCustomSvgIcons();
@@ -2770,7 +2835,7 @@ const KritzelEditor = class {
2770
2835
  syncConfig: this.syncConfig,
2771
2836
  assetStorageConfig: this.assetStorageConfig,
2772
2837
  loginConfig: this.loginConfig,
2773
- theme: this.theme
2838
+ theme: this.theme,
2774
2839
  });
2775
2840
  }
2776
2841
  async onEngineReady(event) {
@@ -2933,43 +2998,6 @@ const KritzelEditor = class {
2933
2998
  }
2934
2999
  this.themeChange.emit(event.detail.theme);
2935
3000
  }
2936
- get moreMenuItems() {
2937
- return [
2938
- {
2939
- id: 'share',
2940
- label: this.resolvedTerms['menu.share'] ?? 'Share',
2941
- icon: 'share',
2942
- action: () => {
2943
- if (!this.isLoggedIn && this.loginConfig) {
2944
- this.openLoginDialog();
2945
- return;
2946
- }
2947
- this.shareDialogRef.open();
2948
- },
2949
- },
2950
- {
2951
- id: 'export',
2952
- label: this.resolvedTerms['menu.export'] ?? 'Export',
2953
- icon: 'upload',
2954
- action: async () => {
2955
- const preview = await this.engineRef.getScreenshot('png');
2956
- this.exportRef.open(preview ?? undefined);
2957
- },
2958
- },
2959
- {
2960
- id: 'import',
2961
- label: this.resolvedTerms['menu.import'] ?? 'Import',
2962
- icon: 'download',
2963
- action: () => this.engineRef.importFromFile(),
2964
- },
2965
- {
2966
- id: 'settings',
2967
- label: this.resolvedTerms['menu.settings'] ?? 'Settings',
2968
- icon: 'settings',
2969
- action: () => this.settingsRef.open(),
2970
- }
2971
- ];
2972
- }
2973
3001
  handleToggleIsPublic = async (event) => {
2974
3002
  const isPublic = event.detail;
2975
3003
  const activeWorkspace = await this.engineRef.getActiveWorkspace();
@@ -2999,6 +3027,28 @@ const KritzelEditor = class {
2999
3027
  get isLoggedIn() {
3000
3028
  return !!this.user && !this.user.isGuest;
3001
3029
  }
3030
+ canExportObjectsAs(objects, format) {
3031
+ if (!objects || objects.length !== 1) {
3032
+ return false;
3033
+ }
3034
+ const [selected] = objects;
3035
+ if (!schema_constants.ObjectHelper.isKritzelExportable(selected)) {
3036
+ return false;
3037
+ }
3038
+ return selected.getSupportedExportFormats().includes(format);
3039
+ }
3040
+ canExportObjects(objects) {
3041
+ return this.canExportObjectsAs(objects, 'png') || this.canExportObjectsAs(objects, 'svg');
3042
+ }
3043
+ getPreferredExportFormat(objects) {
3044
+ if (this.canExportObjectsAs(objects, 'png')) {
3045
+ return 'png';
3046
+ }
3047
+ if (this.canExportObjectsAs(objects, 'svg')) {
3048
+ return 'svg';
3049
+ }
3050
+ return null;
3051
+ }
3002
3052
  async openLoginDialog() {
3003
3053
  this.loginDialogRef?.open();
3004
3054
  }
@@ -3175,41 +3225,41 @@ const KritzelEditor = class {
3175
3225
  const isLoggedIn = this.isLoggedIn;
3176
3226
  const shouldShowCurrentUser = isLoggedIn;
3177
3227
  const shouldShowLoginButton = this.isReady && !!this.loginConfig && !isLoggedIn;
3178
- return (index.h(index.Host, { key: 'ab06876e3a07e97c3788113ffd0483bbb2819d5b' }, index.h("div", { key: 'e393eacfad2b224a720e5a97b047af8dd3ce97a5', class: "editor-content", style: {
3228
+ return (index.h(index.Host, { key: 'd9d87b5ca40972d7ab2c5986f340278347f78c6e' }, index.h("div", { key: 'd8da2ed24d19c550420689c6720dc491fe0d3a22', class: "editor-content", style: {
3179
3229
  opacity: this.isEditorVisible ? '1' : '0',
3180
3230
  visibility: this.isEditorVisible ? 'visible' : 'hidden',
3181
3231
  transition: 'opacity 0.2s ease-in-out, visibility 0.2s ease-in-out',
3182
- } }, index.h("div", { key: 'd9dcbd6859bea28d75c7f8b0cc26ea3ba58b2433', class: "top-left-buttons" }, index.h("kritzel-workspace-manager", { key: '945953a6c95520316c4e0f35503a824414bd3c7a', visible: this.isWorkspaceManagerVisible, workspaces: this.workspaces, activeWorkspace: this.activeWorkspace, terms: this.resolvedTerms, onWorkspaceChange: event => (this.activeWorkspace = event.detail), onIsWorkspaceManagerReady: () => (this.isWorkspaceManagerReady = true) }), index.h("kritzel-back-to-content", { key: '8b345bf6ce0ec33ca35f1f630af2050b95bc6b74', visible: this.isBackToContentButtonVisible, text: this.resolvedTerms['backToContent.label'] ?? 'Back to content', onBackToContent: () => this.backToContent() })), this.activeNotification && (index.h("div", { key: '1b965de38a7f1f40d5d91431801e873289416924', class: "top-center-notification-layer", role: "presentation" }, index.h("div", { key: 'f165dc7acc5e1bc7923fec217ebc16fe4a3f2ece', class: { 'top-center-notification-container': true, 'is-dismissing': this.isNotificationDismissing }, role: "presentation" }, index.h("kritzel-notification-card", { key: 'c5cf7edc1f921e35310c89449f5a043dda171a04', notification: this.activeNotification, locale: this.locale, onDismiss: this.dismissNotification, onHoverChange: event => this.handleNotificationHoverChange(event) })))), this.contextMenuState && this.contextMenuState.items.length > 0 && (index.h("kritzel-context-menu", { key: 'fc3417276f0034e7fc7e8360d3f3ce57106146b2', class: "context-menu", items: this.contextMenuState.items, objects: this.contextMenuState.objects, style: {
3232
+ } }, index.h("div", { key: '82a5d216e7c95307b1bdbd1f06fdfe881c1c50d4', class: "top-left-buttons" }, index.h("kritzel-workspace-manager", { key: 'de13673384b44bb6bd309a927a2866a9b78b519e', visible: this.isWorkspaceManagerVisible, workspaces: this.workspaces, activeWorkspace: this.activeWorkspace, terms: this.resolvedTerms, onWorkspaceChange: event => (this.activeWorkspace = event.detail), onIsWorkspaceManagerReady: () => (this.isWorkspaceManagerReady = true) }), index.h("kritzel-back-to-content", { key: 'f68610d7d91713f7a0ec5f0135d208769f7fab9c', visible: this.isBackToContentButtonVisible, text: this.resolvedTerms['backToContent.label'] ?? 'Back to content', onBackToContent: () => this.backToContent() })), this.activeNotification && (index.h("div", { key: '9d1f972fb67050b1f996e3088710713fa1219b9a', class: "top-center-notification-layer", role: "presentation" }, index.h("div", { key: '295eff0a02f7cc79ad8e80cda23baa3c576ce156', class: { 'top-center-notification-container': true, 'is-dismissing': this.isNotificationDismissing }, role: "presentation" }, index.h("kritzel-notification-card", { key: '1d1bf140ddfb7b927d30fb1d77e7b887240d7deb', notification: this.activeNotification, locale: this.locale, onDismiss: this.dismissNotification, onHoverChange: event => this.handleNotificationHoverChange(event) })))), this.contextMenuState && this.contextMenuState.items.length > 0 && (index.h("kritzel-context-menu", { key: '1062bc9554e5834ef2002fbfe953f7938866fb84', class: "context-menu", items: this.contextMenuState.items, objects: this.contextMenuState.objects, style: {
3183
3233
  position: 'absolute',
3184
3234
  left: `${this.contextMenuState.position.x}px`,
3185
3235
  top: `${this.contextMenuState.position.y}px`,
3186
3236
  zIndex: '10002',
3187
- }, onActionSelected: event => this.handleContextMenuActionSelected(event), onClose: () => this.hideContextMenu() })), index.h("kritzel-engine", { key: '0c9df0386d38a87f50d324cfc51307a1cb947a58', ref: el => {
3237
+ }, onActionSelected: event => this.handleContextMenuActionSelected(event), onClose: () => this.hideContextMenu() })), index.h("kritzel-sync-indicator", { key: '0094a026828473988f857d82cfccb811f36f049c', visible: this.isSyncing, text: "Syncing..." }), index.h("kritzel-loading-overlay", { key: '680096e20b6175104a866b9a92ca4c1d0087c93a', visible: this.isLoadingOverlayVisible, text: this.resolvedTerms['editor.loading'] ?? 'Loading...' }), index.h("kritzel-engine", { key: '0698a44fcbf2d6ea786d53502350ac0c9ffb9d66', ref: el => {
3188
3238
  if (el) {
3189
3239
  this.engineRef = el;
3190
3240
  }
3191
- }, 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) }), index.h("kritzel-controls", { key: '46d8e9c45c7bed24fc02d17c8fef836d596088d7', visible: this.isControlsVisible, class: { 'keyboard-open': this.isVirtualKeyboardOpen }, ref: el => {
3241
+ }, workspace: this.activeWorkspace, activeWorkspaceId: this.activeWorkspaceId, editorId: this.editorId, syncConfig: this.syncConfig, assetStorageConfig: this.assetStorageConfig, user: this.user, scaleMax: this.scaleMax, lockDrawingScale: this.lockDrawingScale, isObjectDistanceFadingActive: this.isObjectDistanceFadingActive, scaleMin: this.scaleMin, cursorTarget: this.cursorTarget, isLoading: this.isLoading, viewportBoundaryLeft: this.viewportBoundaryLeft, viewportBoundaryRight: this.viewportBoundaryRight, viewportBoundaryTop: this.viewportBoundaryTop, viewportBoundaryBottom: this.viewportBoundaryBottom, isPanningEnabled: this.isPanningEnabled, isZoomingEnabled: this.isZoomingEnabled, theme: this.theme, themes: this.themes, licenseKey: this.licenseKey, locale: this.locale, locales: this.locales, fallbackLocale: this.fallbackLocale, debugInfo: this.debugInfo, globalContextMenuItems: this.globalContextMenuItems, objectContextMenuItems: this.objectContextMenuItems, onIsEngineReady: event => this.onEngineReady(event), onWorkspacesChange: event => this.handleWorkspacesChange(event), onActiveWorkspaceChange: event => this.handleActiveWorkspaceChange(event), onObjectsChange: event => this.handleObjectsChange(event), onObjectsAdded: event => this.handleObjectsAdded(event), onObjectsRemoved: event => this.handleObjectsRemoved(event), onObjectsUpdated: event => this.handleObjectsUpdated(event), onUndoStateChange: event => this.handleUndoStateChange(event), onObjectsInViewportChange: event => this.handleObjectsInViewportChange(event), onViewportChange: event => this.handleViewportChange(event), onAwarenessChange: event => this.handleAwarenessChange(event), onNotificationsChange: event => this.handleNotificationsChange(event), onContextMenuStateChange: event => this.handleContextMenuStateChange(event), onSyncingChange: event => (this.isSyncing = event.detail), onLoadingChange: event => (this.isLoadingOverlayVisible = event.detail) }), index.h("kritzel-controls", { key: '765a670807111ad68e9f8d14bdca2651a33704d4', visible: this.isControlsVisible, class: { 'keyboard-open': this.isVirtualKeyboardOpen }, ref: el => {
3192
3242
  if (el) {
3193
3243
  this.controlsRef = el;
3194
3244
  }
3195
- }, controls: this.normalizedControls, isUtilityPanelVisible: this.isUtilityPanelVisible, undoState: this.undoState ?? undefined, theme: this.theme, terms: this.resolvedTerms, onIsControlsReady: () => (this.isControlsReady = true) }), index.h("div", { key: 'e3ef94509a82fe435f827b66cf1813969b7dec66', class: "bottom-left-buttons" }, index.h("kritzel-zoom-panel", { key: '4e6cbdf85f550e6e3037dce3b910c0582ea0320b', visible: this.isZoomPanelVisible, disabled: !this.isZoomingEnabled, zoomPercent: this.currentZoomPercent, terms: this.resolvedTerms, onZoomIn: () => this.zoomIn(), onZoomOut: () => this.zoomOut() })), index.h("div", { key: 'a62df7f8a68cbed728b701894a5284c2bff39e1e', class: "top-right-buttons" }, index.h("kritzel-settings", { key: '0b6daf9ff43551df9f86431d398a32913a845925', ref: el => {
3245
+ }, controls: this.normalizedControls, isUtilityPanelVisible: this.isUtilityPanelVisible, undoState: this.undoState ?? undefined, theme: this.theme, terms: this.resolvedTerms, onIsControlsReady: () => (this.isControlsReady = true) }), index.h("div", { key: 'e7c5ebab4a40b7ab4fa5d19d203129326c0009ed', class: "bottom-left-buttons" }, index.h("kritzel-zoom-panel", { key: '0aa3891b553d2abcd06e635227fd6bb6656ed739', visible: this.isZoomPanelVisible, disabled: !this.isZoomingEnabled, zoomPercent: this.currentZoomPercent, terms: this.resolvedTerms, onZoomIn: () => this.zoomIn(), onZoomOut: () => this.zoomOut() })), index.h("div", { key: '824244bbd78784c1d78ab89cc12e206ca61d84cc', class: "top-right-buttons" }, index.h("kritzel-settings", { key: 'baf4830b6b9ccb6241b1a1baa5cc6ff9d0295fb9', ref: el => {
3196
3246
  if (el) {
3197
3247
  this.settingsRef = el;
3198
3248
  }
3199
- }, shortcuts: this.shortcuts, availableThemes: this.themes && this.themes.length > 0 ? this.themes.map(t => t.name) : ['light', 'dark'], availableLocales: this.availableLocaleOptions, settings: this.currentSettingsConfig, terms: this.resolvedTerms, onSettingsChange: event => this.handleSettingsChange(event) }), index.h("kritzel-export", { key: '29d02ffd3358fe91d27435cc3fdd5f8ac2ad7cf5', ref: el => {
3249
+ }, shortcuts: this.shortcuts, availableThemes: this.themes && this.themes.length > 0 ? this.themes.map(t => t.name) : ['light', 'dark'], availableLocales: this.availableLocaleOptions, settings: this.currentSettingsConfig, terms: this.resolvedTerms, onSettingsChange: event => this.handleSettingsChange(event) }), index.h("kritzel-export", { key: '57e2682450c71803cd49bfe65f16e02d9d3358bc', ref: el => {
3200
3250
  if (el) {
3201
3251
  this.exportRef = el;
3202
3252
  }
3203
- }, workspaceName: this.activeWorkspace?.name || 'workspace', terms: this.resolvedTerms, onExportPng: () => this.engineRef.exportViewportAsPng(), onExportSvg: () => this.engineRef.exportViewportAsSvg(), onExportJson: event => this.engineRef.downloadAsJson(event.detail) }), index.h("kritzel-active-users", { key: '729ca599e46654bc1429b5d91ff73f26d9b6e7bb', users: this.activeUsers }), shouldShowCurrentUser && index.h("kritzel-current-user", { key: '84c07268072418a82cf504f16803c59a1e37c0ae', user: this.user, terms: this.resolvedTerms, onClick: () => this.currentUserDialogRef?.open() }), shouldShowCurrentUser && (index.h("kritzel-current-user-dialog", { key: '55189e3da7899628b252e7995b20bc422fce2d5c', ref: el => {
3253
+ }, workspaceName: this.activeWorkspace?.name || 'workspace', terms: this.resolvedTerms, onExportPng: () => this.engineRef.exportViewportAsPng(), onExportSvg: () => this.engineRef.exportViewportAsSvg(), onExportJson: event => this.engineRef.downloadAsJson(event.detail) }), index.h("kritzel-active-users", { key: '955d2a61b98d937f8eec39ab19f1b73e702c9fd7', users: this.activeUsers }), shouldShowCurrentUser && index.h("kritzel-current-user", { key: 'c0885e0c8cab1a4cb50c8f6d4b17e03f32cc2b87', user: this.user, terms: this.resolvedTerms, onClick: () => this.currentUserDialogRef?.open() }), shouldShowCurrentUser && (index.h("kritzel-current-user-dialog", { key: '8209b2562ffb821036bb8e4dacf052cacbe99bee', ref: el => {
3204
3254
  if (el) {
3205
3255
  this.currentUserDialogRef = el;
3206
3256
  this.currentUserDialogRef.addEventListener('logoutRequest', this.handleCurrentUserLogout);
3207
3257
  }
3208
- }, user: this.user, terms: this.resolvedTerms })), shouldShowLoginButton && (index.h("kritzel-button", { key: 'a55d85769bad635fcc7934f584bf1bf74c1e7ea6', onButtonClick: () => this.loginDialogRef?.open() }, this.resolvedTerms['login.dialogTitle'] ?? 'Sign in')), index.h("kritzel-more-menu", { key: '67dd879ec3c55c98e32f6112ca53d925f47b56b4', items: this.moreMenuItems, visible: this.isMoreMenuVisible, terms: this.resolvedTerms }), index.h("kritzel-share-dialog", { key: '5f13130099110eb472022099219e15877ceceb57', ref: el => {
3258
+ }, user: this.user, terms: this.resolvedTerms })), shouldShowLoginButton && index.h("kritzel-button", { key: '9cec12de2207e8931a08c0a4f6bda39d039dfd1f', onButtonClick: () => this.loginDialogRef?.open() }, this.resolvedTerms['login.dialogTitle'] ?? 'Sign in'), index.h("kritzel-more-menu", { key: 'fe0e1413148b9ac64e5f058790ba96b856faef9e', items: this.resolvedMoreMenuItems, visible: this.isMoreMenuVisible, terms: this.resolvedTerms }), index.h("kritzel-share-dialog", { key: 'ea06c88b0cba0464144bea1e2247b37cc5c44646', ref: el => {
3209
3259
  if (el) {
3210
3260
  this.shareDialogRef = el;
3211
3261
  }
3212
- }, isPublic: this.currentIsPublic, workspaceId: this.activeWorkspace?.id, terms: this.resolvedTerms, onToggleIsPublic: this.handleToggleIsPublic }), this.loginConfig && (index.h("kritzel-login-dialog", { key: 'cda0fcdcad75de8730ce10bda425cc0fbe01429a', ref: el => {
3262
+ }, isPublic: this.currentIsPublic, workspaceId: this.activeWorkspace?.id, terms: this.resolvedTerms, onToggleIsPublic: this.handleToggleIsPublic }), this.loginConfig && (index.h("kritzel-login-dialog", { key: 'e9a523d5989284b7e30ac44f55882a17c82c643b', ref: el => {
3213
3263
  if (el) {
3214
3264
  this.loginDialogRef = el;
3215
3265
  }
@@ -3234,6 +3284,18 @@ const KritzelEditor = class {
3234
3284
  "controls": [{
3235
3285
  "onControlsChange": 0
3236
3286
  }],
3287
+ "moreMenuItems": [{
3288
+ "onMoreMenuItemsChange": 0
3289
+ }],
3290
+ "syncConfig": [{
3291
+ "onSyncConfigChange": 0
3292
+ }],
3293
+ "loginConfig": [{
3294
+ "onLoginConfigChange": 0
3295
+ }],
3296
+ "user": [{
3297
+ "onUserChange": 0
3298
+ }],
3237
3299
  "activeWorkspaceId": [{
3238
3300
  "onActiveWorkspaceIdChange": 0
3239
3301
  }],
@@ -25940,962 +26002,310 @@ class KritzelCore {
25940
26002
  }
25941
26003
  }
25942
26004
 
25943
- /**
25944
- * Helper class for exporting Kritzel objects to SVG format.
25945
- * Generates clean, standards-compliant SVG markup from canvas objects.
25946
- */
25947
- class KritzelSvgExportHelper {
25948
- /**
25949
- * Calculates the combined bounding box that encompasses all objects.
25950
- * Uses rotatedBoundingBox to account for object rotation.
25951
- * @param objects - Array of objects to calculate bounds for
25952
- * @returns Combined bounding box, or null if no objects provided
25953
- */
25954
- static calculateCombinedBounds(objects) {
25955
- if (!objects || objects.length === 0) {
25956
- return null;
25957
- }
25958
- let minX = Infinity;
25959
- let minY = Infinity;
25960
- let maxX = -Infinity;
25961
- let maxY = -Infinity;
25962
- for (const obj of objects) {
25963
- const bbox = obj.rotatedBoundingBox;
25964
- minX = Math.min(minX, bbox.x);
25965
- minY = Math.min(minY, bbox.y);
25966
- maxX = Math.max(maxX, bbox.x + bbox.width);
25967
- maxY = Math.max(maxY, bbox.y + bbox.height);
25968
- }
25969
- return {
25970
- x: minX,
25971
- y: minY,
25972
- z: 1,
25973
- width: maxX - minX,
25974
- height: maxY - minY,
25975
- };
26005
+ function resolveUrl(url, baseUrl) {
26006
+ // url is absolute already
26007
+ if (url.match(/^[a-z]+:\/\//i)) {
26008
+ return url;
25976
26009
  }
25977
- /**
25978
- * Generates a complete SVG document string from the given objects.
25979
- * @param objects - Objects to export
25980
- * @param options - Export options
25981
- * @returns SVG document string
25982
- */
25983
- static generateSvg(objects, options = {}) {
25984
- const { theme, padding = 0, includeXmlDeclaration = true, rasterSafeForCanvas = false, imageDataUrls } = options;
25985
- if (!objects || objects.length === 0) {
25986
- return '';
25987
- }
25988
- // Flatten groups to get all objects including nested children
25989
- const allObjects = this.flattenObjects(objects);
25990
- const bounds = this.calculateCombinedBounds(allObjects);
25991
- if (!bounds) {
25992
- return '';
26010
+ // url is absolute already, without protocol
26011
+ if (url.match(/^\/\//)) {
26012
+ return window.location.protocol + url;
26013
+ }
26014
+ // dataURI, mailto:, tel:, etc.
26015
+ if (url.match(/^[a-z]+:/i)) {
26016
+ return url;
26017
+ }
26018
+ const doc = document.implementation.createHTMLDocument();
26019
+ const base = doc.createElement('base');
26020
+ const a = doc.createElement('a');
26021
+ doc.head.appendChild(base);
26022
+ doc.body.appendChild(a);
26023
+ if (baseUrl) {
26024
+ base.href = baseUrl;
26025
+ }
26026
+ a.href = url;
26027
+ return a.href;
26028
+ }
26029
+ const uuid = (() => {
26030
+ // generate uuid for className of pseudo elements.
26031
+ // We should not use GUIDs, otherwise pseudo elements sometimes cannot be captured.
26032
+ let counter = 0;
26033
+ // ref: http://stackoverflow.com/a/6248722/2519373
26034
+ const random = () =>
26035
+ // eslint-disable-next-line no-bitwise
26036
+ `0000${((Math.random() * 36 ** 4) << 0).toString(36)}`.slice(-4);
26037
+ return () => {
26038
+ counter += 1;
26039
+ return `u${random()}${counter}`;
26040
+ };
26041
+ })();
26042
+ function toArray(arrayLike) {
26043
+ const arr = [];
26044
+ for (let i = 0, l = arrayLike.length; i < l; i++) {
26045
+ arr.push(arrayLike[i]);
26046
+ }
26047
+ return arr;
26048
+ }
26049
+ let styleProps = null;
26050
+ function getStyleProperties(options = {}) {
26051
+ if (styleProps) {
26052
+ return styleProps;
26053
+ }
26054
+ if (options.includeStyleProperties) {
26055
+ styleProps = options.includeStyleProperties;
26056
+ return styleProps;
26057
+ }
26058
+ styleProps = toArray(window.getComputedStyle(document.documentElement));
26059
+ return styleProps;
26060
+ }
26061
+ function px(node, styleProperty) {
26062
+ const win = node.ownerDocument.defaultView || window;
26063
+ const val = win.getComputedStyle(node).getPropertyValue(styleProperty);
26064
+ return val ? parseFloat(val.replace('px', '')) : 0;
26065
+ }
26066
+ function getNodeWidth(node) {
26067
+ const leftBorder = px(node, 'border-left-width');
26068
+ const rightBorder = px(node, 'border-right-width');
26069
+ return node.clientWidth + leftBorder + rightBorder;
26070
+ }
26071
+ function getNodeHeight(node) {
26072
+ const topBorder = px(node, 'border-top-width');
26073
+ const bottomBorder = px(node, 'border-bottom-width');
26074
+ return node.clientHeight + topBorder + bottomBorder;
26075
+ }
26076
+ function getImageSize(targetNode, options = {}) {
26077
+ const width = options.width || getNodeWidth(targetNode);
26078
+ const height = options.height || getNodeHeight(targetNode);
26079
+ return { width, height };
26080
+ }
26081
+ function getPixelRatio() {
26082
+ let ratio;
26083
+ let FINAL_PROCESS;
26084
+ try {
26085
+ FINAL_PROCESS = process;
26086
+ }
26087
+ catch (e) {
26088
+ // pass
26089
+ }
26090
+ const val = FINAL_PROCESS && FINAL_PROCESS.env
26091
+ ? FINAL_PROCESS.env.devicePixelRatio
26092
+ : null;
26093
+ if (val) {
26094
+ ratio = parseInt(val, 10);
26095
+ if (Number.isNaN(ratio)) {
26096
+ ratio = 1;
25993
26097
  }
25994
- // Add padding to bounds
25995
- const viewBoxX = bounds.x - padding;
25996
- const viewBoxY = bounds.y - padding;
25997
- const viewBoxWidth = bounds.width + padding * 2;
25998
- const viewBoxHeight = bounds.height + padding * 2;
25999
- // Collect all defs (markers, patterns, etc.)
26000
- const defs = this.collectDefs(allObjects, theme);
26001
- // Generate SVG elements for each object
26002
- const elements = objects
26003
- .sort((a, b) => a.zIndex - b.zIndex)
26004
- .map(obj => this.objectToSvgElement(obj, theme, imageDataUrls, rasterSafeForCanvas))
26005
- .filter(Boolean)
26006
- .join('\n ');
26007
- const xmlDecl = includeXmlDeclaration ? '<?xml version="1.0" encoding="UTF-8"?>\n' : '';
26008
- const defsContent = defs.length > 0 ? `\n <defs>\n ${defs.join('\n ')}\n </defs>` : '';
26009
- return `${xmlDecl}<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBoxX} ${viewBoxY} ${viewBoxWidth} ${viewBoxHeight}" width="${viewBoxWidth}" height="${viewBoxHeight}">${defsContent}
26010
- ${elements}
26011
- </svg>`;
26012
26098
  }
26013
- /**
26014
- * Flattens a list of objects, expanding groups into their children recursively.
26015
- * @param objects - Objects to flatten
26016
- * @returns Flat array of all objects including group children
26017
- */
26018
- static flattenObjects(objects) {
26019
- const result = [];
26020
- for (const obj of objects) {
26021
- if (schema_constants.KritzelClassHelper.isInstanceOf(obj, 'KritzelGroup')) {
26022
- result.push(...this.flattenObjects(obj.children));
26099
+ return ratio || window.devicePixelRatio || 1;
26100
+ }
26101
+ // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size
26102
+ const canvasDimensionLimit = 16384;
26103
+ function checkCanvasDimensions(canvas) {
26104
+ if (canvas.width > canvasDimensionLimit ||
26105
+ canvas.height > canvasDimensionLimit) {
26106
+ if (canvas.width > canvasDimensionLimit &&
26107
+ canvas.height > canvasDimensionLimit) {
26108
+ if (canvas.width > canvas.height) {
26109
+ canvas.height *= canvasDimensionLimit / canvas.width;
26110
+ canvas.width = canvasDimensionLimit;
26023
26111
  }
26024
26112
  else {
26025
- result.push(obj);
26113
+ canvas.width *= canvasDimensionLimit / canvas.height;
26114
+ canvas.height = canvasDimensionLimit;
26026
26115
  }
26027
26116
  }
26028
- return result;
26029
- }
26030
- /**
26031
- * Resolves every image referenced by the given objects (including those
26032
- * nested inside groups) to an inlined `data:` URL, keyed by object id.
26033
- *
26034
- * This is required for export flows that rasterize the SVG (e.g. PNG
26035
- * export): an SVG loaded into an `<img>` element will NOT load external
26036
- * or `blob:` resources, so a `<image href="blob:...">` renders as a
26037
- * broken-image placeholder. Embedding the bytes as a base64 `data:` URL
26038
- * makes the image self-contained and survive rasterization.
26039
- *
26040
- * Resolution is best-effort: images that fail to resolve are simply
26041
- * omitted from the map, and the SVG falls back to whatever `href` the
26042
- * synchronous path would have produced.
26043
- * @param objects - Objects to inline images for
26044
- * @param assetResolver - Resolver used to fetch asset bytes; optional
26045
- * @returns Map of image object id -> inlined `data:` URL
26046
- */
26047
- static async resolveImageDataUrls(objects, assetResolver) {
26048
- const dataUrls = new Map();
26049
- if (!objects || objects.length === 0) {
26050
- return dataUrls;
26117
+ else if (canvas.width > canvasDimensionLimit) {
26118
+ canvas.height *= canvasDimensionLimit / canvas.width;
26119
+ canvas.width = canvasDimensionLimit;
26051
26120
  }
26052
- const images = this.flattenObjects(objects).filter((obj) => schema_constants.KritzelClassHelper.isInstanceOf(obj, 'KritzelImage'));
26053
- await Promise.all(images.map(async (image) => {
26054
- // Legacy inline data URL is already embeddable as-is.
26055
- if (image.src?.startsWith('data:')) {
26056
- dataUrls.set(image.id, image.src);
26057
- return;
26058
- }
26059
- // A resolvedSrc that is itself a data URL can be embedded directly.
26060
- if (image.resolvedSrc?.startsWith('data:')) {
26061
- dataUrls.set(image.id, image.resolvedSrc);
26062
- return;
26063
- }
26064
- if (!image.assetId || !assetResolver) {
26065
- return;
26066
- }
26121
+ else {
26122
+ canvas.width *= canvasDimensionLimit / canvas.height;
26123
+ canvas.height = canvasDimensionLimit;
26124
+ }
26125
+ }
26126
+ }
26127
+ function createImage(url) {
26128
+ return new Promise((resolve, reject) => {
26129
+ const img = new Image();
26130
+ img.onload = () => {
26131
+ img.decode().then(() => {
26132
+ requestAnimationFrame(() => resolve(img));
26133
+ });
26134
+ };
26135
+ img.onerror = reject;
26136
+ img.crossOrigin = 'anonymous';
26137
+ img.decoding = 'async';
26138
+ img.src = url;
26139
+ });
26140
+ }
26141
+ async function svgToDataURL(svg) {
26142
+ return Promise.resolve()
26143
+ .then(() => new XMLSerializer().serializeToString(svg))
26144
+ .then(encodeURIComponent)
26145
+ .then((html) => `data:image/svg+xml;charset=utf-8,${html}`);
26146
+ }
26147
+ async function nodeToDataURL(node, width, height) {
26148
+ const xmlns = 'http://www.w3.org/2000/svg';
26149
+ const svg = document.createElementNS(xmlns, 'svg');
26150
+ const foreignObject = document.createElementNS(xmlns, 'foreignObject');
26151
+ svg.setAttribute('width', `${width}`);
26152
+ svg.setAttribute('height', `${height}`);
26153
+ svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
26154
+ foreignObject.setAttribute('width', '100%');
26155
+ foreignObject.setAttribute('height', '100%');
26156
+ foreignObject.setAttribute('x', '0');
26157
+ foreignObject.setAttribute('y', '0');
26158
+ foreignObject.setAttribute('externalResourcesRequired', 'true');
26159
+ svg.appendChild(foreignObject);
26160
+ foreignObject.appendChild(node);
26161
+ return svgToDataURL(svg);
26162
+ }
26163
+ const isInstanceOfElement = (node, instance) => {
26164
+ if (node instanceof instance)
26165
+ return true;
26166
+ const nodePrototype = Object.getPrototypeOf(node);
26167
+ if (nodePrototype === null)
26168
+ return false;
26169
+ return (nodePrototype.constructor.name === instance.name ||
26170
+ isInstanceOfElement(nodePrototype, instance));
26171
+ };
26172
+
26173
+ function formatCSSText(style) {
26174
+ const content = style.getPropertyValue('content');
26175
+ return `${style.cssText} content: '${content.replace(/'|"/g, '')}';`;
26176
+ }
26177
+ function formatCSSProperties(style, options) {
26178
+ return getStyleProperties(options)
26179
+ .map((name) => {
26180
+ const value = style.getPropertyValue(name);
26181
+ const priority = style.getPropertyPriority(name);
26182
+ return `${name}: ${value}${priority ? ' !important' : ''};`;
26183
+ })
26184
+ .join(' ');
26185
+ }
26186
+ function getPseudoElementStyle(className, pseudo, style, options) {
26187
+ const selector = `.${className}:${pseudo}`;
26188
+ const cssText = style.cssText
26189
+ ? formatCSSText(style)
26190
+ : formatCSSProperties(style, options);
26191
+ return document.createTextNode(`${selector}{${cssText}}`);
26192
+ }
26193
+ function clonePseudoElement(nativeNode, clonedNode, pseudo, options) {
26194
+ const style = window.getComputedStyle(nativeNode, pseudo);
26195
+ const content = style.getPropertyValue('content');
26196
+ if (content === '' || content === 'none') {
26197
+ return;
26198
+ }
26199
+ const className = uuid();
26200
+ try {
26201
+ clonedNode.className = `${clonedNode.className} ${className}`;
26202
+ }
26203
+ catch (err) {
26204
+ return;
26205
+ }
26206
+ const styleElement = document.createElement('style');
26207
+ styleElement.appendChild(getPseudoElementStyle(className, pseudo, style, options));
26208
+ clonedNode.appendChild(styleElement);
26209
+ }
26210
+ function clonePseudoElements(nativeNode, clonedNode, options) {
26211
+ clonePseudoElement(nativeNode, clonedNode, ':before', options);
26212
+ clonePseudoElement(nativeNode, clonedNode, ':after', options);
26213
+ }
26214
+
26215
+ const WOFF = 'application/font-woff';
26216
+ const JPEG = 'image/jpeg';
26217
+ const mimes = {
26218
+ woff: WOFF,
26219
+ woff2: WOFF,
26220
+ ttf: 'application/font-truetype',
26221
+ eot: 'application/vnd.ms-fontobject',
26222
+ png: 'image/png',
26223
+ jpg: JPEG,
26224
+ jpeg: JPEG,
26225
+ gif: 'image/gif',
26226
+ tiff: 'image/tiff',
26227
+ svg: 'image/svg+xml',
26228
+ webp: 'image/webp',
26229
+ };
26230
+ function getExtension(url) {
26231
+ const match = /\.([^./]*?)$/g.exec(url);
26232
+ return match ? match[1] : '';
26233
+ }
26234
+ function getMimeType(url) {
26235
+ const extension = getExtension(url).toLowerCase();
26236
+ return mimes[extension] || '';
26237
+ }
26238
+
26239
+ function getContentFromDataUrl(dataURL) {
26240
+ return dataURL.split(/,/)[1];
26241
+ }
26242
+ function isDataUrl(url) {
26243
+ return url.search(/^(data:)/) !== -1;
26244
+ }
26245
+ function makeDataUrl(content, mimeType) {
26246
+ return `data:${mimeType};base64,${content}`;
26247
+ }
26248
+ async function fetchAsDataURL(url, init, process) {
26249
+ const res = await fetch(url, init);
26250
+ if (res.status === 404) {
26251
+ throw new Error(`Resource "${res.url}" not found`);
26252
+ }
26253
+ const blob = await res.blob();
26254
+ return new Promise((resolve, reject) => {
26255
+ const reader = new FileReader();
26256
+ reader.onerror = reject;
26257
+ reader.onloadend = () => {
26067
26258
  try {
26068
- const blob = await assetResolver.fetchBlob(image.assetId);
26069
- dataUrls.set(image.id, await this.blobToDataUrl(blob));
26259
+ resolve(process({ res, result: reader.result }));
26070
26260
  }
26071
- catch (err) {
26072
- console.warn(`[KritzelSvgExportHelper] Failed to inline image asset ${image.assetId} for export:`, err);
26261
+ catch (error) {
26262
+ reject(error);
26073
26263
  }
26074
- }));
26075
- return dataUrls;
26264
+ };
26265
+ reader.readAsDataURL(blob);
26266
+ });
26267
+ }
26268
+ const cache = {};
26269
+ function getCacheKey(url, contentType, includeQueryParams) {
26270
+ let key = url.replace(/\?.*/, '');
26271
+ if (includeQueryParams) {
26272
+ key = url;
26076
26273
  }
26077
- /**
26078
- * Reads a Blob into a base64 `data:` URL.
26079
- * @param blob - The blob to encode
26080
- * @returns Promise resolving to the data URL string
26081
- */
26082
- static async blobToDataUrl(blob) {
26083
- // Prefer FileReader in browser-like runtimes for efficiency.
26084
- const hasFileReader = typeof FileReader !== 'undefined';
26085
- if (hasFileReader) {
26086
- try {
26087
- return await new Promise((resolve, reject) => {
26088
- const reader = new FileReader();
26089
- reader.onload = () => {
26090
- if (typeof reader.result === 'string') {
26091
- resolve(reader.result);
26092
- return;
26093
- }
26094
- reject(new Error('FileReader result is not a string'));
26095
- };
26096
- reader.onerror = () => reject(reader.error ?? new Error('Failed to read blob as data URL'));
26097
- reader.readAsDataURL(blob);
26098
- });
26099
- }
26100
- catch {
26101
- // Fall through to the ArrayBuffer-based encoder.
26274
+ // font resource
26275
+ if (/ttf|otf|eot|woff2?/i.test(key)) {
26276
+ key = key.replace(/.*\//, '');
26277
+ }
26278
+ return contentType ? `[${contentType}]${key}` : key;
26279
+ }
26280
+ async function resourceToDataURL(resourceUrl, contentType, options) {
26281
+ const cacheKey = getCacheKey(resourceUrl, contentType, options.includeQueryParams);
26282
+ if (cache[cacheKey] != null) {
26283
+ return cache[cacheKey];
26284
+ }
26285
+ // ref: https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache
26286
+ if (options.cacheBust) {
26287
+ // eslint-disable-next-line no-param-reassign
26288
+ resourceUrl += (/\?/.test(resourceUrl) ? '&' : '?') + new Date().getTime();
26289
+ }
26290
+ let dataURL;
26291
+ try {
26292
+ const content = await fetchAsDataURL(resourceUrl, options.fetchRequestInit, ({ res, result }) => {
26293
+ if (!contentType) {
26294
+ // eslint-disable-next-line no-param-reassign
26295
+ contentType = res.headers.get('Content-Type') || '';
26102
26296
  }
26103
- }
26104
- const bytes = new Uint8Array(await blob.arrayBuffer());
26105
- const mimeType = blob.type || 'application/octet-stream';
26106
- return `data:${mimeType};base64,${this.uint8ToBase64(bytes)}`;
26297
+ return getContentFromDataUrl(result);
26298
+ });
26299
+ dataURL = makeDataUrl(content, contentType);
26107
26300
  }
26108
- /**
26109
- * Encodes a byte array to base64 across browser and Node-like runtimes.
26110
- */
26111
- static uint8ToBase64(bytes) {
26112
- if (typeof Buffer !== 'undefined') {
26113
- return Buffer.from(bytes).toString('base64');
26301
+ catch (error) {
26302
+ dataURL = options.imagePlaceholder || '';
26303
+ let msg = `Failed to fetch resource: ${resourceUrl}`;
26304
+ if (error) {
26305
+ msg = typeof error === 'string' ? error : error.message;
26114
26306
  }
26115
- let binary = '';
26116
- const chunkSize = 0x8000;
26117
- for (let i = 0; i < bytes.length; i += chunkSize) {
26118
- const chunk = bytes.subarray(i, Math.min(i + chunkSize, bytes.length));
26119
- binary += String.fromCharCode(...chunk);
26120
- }
26121
- if (typeof btoa === 'function') {
26122
- return btoa(binary);
26123
- }
26124
- throw new Error('No base64 encoder available in this runtime');
26125
- }
26126
- /**
26127
- * Collects SVG defs (markers, patterns, etc.) needed by the objects.
26128
- * @param objects - Objects to collect defs for
26129
- * @param theme - Theme for color resolution
26130
- * @returns Array of def element strings
26131
- */
26132
- static collectDefs(objects, theme) {
26133
- const defs = [];
26134
- for (const obj of objects) {
26135
- if (schema_constants.KritzelClassHelper.isInstanceOf(obj, 'KritzelLine')) {
26136
- if (obj.hasStartArrow) {
26137
- defs.push(this.generateArrowMarker(obj, 'start', theme));
26138
- }
26139
- if (obj.hasEndArrow) {
26140
- defs.push(this.generateArrowMarker(obj, 'end', theme));
26141
- }
26142
- }
26143
- }
26144
- return defs;
26145
- }
26146
- /**
26147
- * Generates an SVG marker element for line arrows.
26148
- * @param line - The line object
26149
- * @param end - Which end of the line ('start' or 'end')
26150
- * @param theme - Theme for color resolution
26151
- * @returns SVG marker element string
26152
- */
26153
- static generateArrowMarker(line, end, _theme) {
26154
- const markerId = end === 'start' ? line.startMarkerId : line.endMarkerId;
26155
- const size = line.getArrowSize(end);
26156
- const fill = line.getArrowFill(end);
26157
- const style = end === 'start' ? line.arrows?.start?.style : line.arrows?.end?.style;
26158
- const path = line.getArrowPath(style);
26159
- const orient = end === 'start' ? 'auto-start-reverse' : 'auto';
26160
- const refX = 0;
26161
- const refY = size / 2;
26162
- return `<marker id="${markerId}" markerWidth="${size}" markerHeight="${size}" refX="${refX}" refY="${refY}" orient="${orient}" markerUnits="userSpaceOnUse">
26163
- <path d="${path}" fill="${fill}" transform="scale(${size / 10})"/>
26164
- </marker>`;
26165
- }
26166
- /**
26167
- * Converts a Kritzel object to its SVG element representation.
26168
- * @param object - The object to convert
26169
- * @param theme - Theme for color resolution
26170
- * @param imageDataUrls - Optional map of image id -> inlined data URL
26171
- * @returns SVG element string, or empty string if object type is not supported
26172
- */
26173
- static objectToSvgElement(object, theme, imageDataUrls, rasterSafeForCanvas = false) {
26174
- if (schema_constants.KritzelClassHelper.isInstanceOf(object, 'KritzelPath')) {
26175
- return this.pathToSvg(object, theme);
26176
- }
26177
- if (schema_constants.KritzelClassHelper.isInstanceOf(object, 'KritzelLine')) {
26178
- return this.lineToSvg(object, theme);
26179
- }
26180
- if (schema_constants.KritzelClassHelper.isInstanceOf(object, 'KritzelShape')) {
26181
- return this.shapeToSvg(object, theme);
26182
- }
26183
- if (schema_constants.KritzelClassHelper.isInstanceOf(object, 'KritzelText')) {
26184
- return this.textToSvg(object, theme, rasterSafeForCanvas);
26185
- }
26186
- if (schema_constants.KritzelClassHelper.isInstanceOf(object, 'KritzelImage')) {
26187
- return this.imageToSvg(object, imageDataUrls);
26188
- }
26189
- if (schema_constants.KritzelClassHelper.isInstanceOf(object, 'KritzelCustomElement')) {
26190
- return this.customElementToSvg(object, rasterSafeForCanvas);
26191
- }
26192
- if (schema_constants.KritzelClassHelper.isInstanceOf(object, 'KritzelGroup')) {
26193
- return this.groupToSvg(object, theme, imageDataUrls, rasterSafeForCanvas);
26194
- }
26195
- return '';
26196
- }
26197
- /**
26198
- * Converts a KritzelPath to SVG.
26199
- * @param path - The path object
26200
- * @param theme - Theme for color resolution
26201
- * @returns SVG element string
26202
- */
26203
- static pathToSvg(path, theme) {
26204
- const transform = this.buildTransform(path);
26205
- const fill = schema_constants.KritzelColorHelper.resolveThemeColor(path.fill, theme);
26206
- const stroke = schema_constants.KritzelColorHelper.resolveThemeColor(path.stroke, theme);
26207
- const opacity = path.opacity !== 1 ? ` opacity="${path.opacity}"` : '';
26208
- const scale = this.getObjectScale(path);
26209
- // Path needs to be wrapped in a g with translation since path.d uses local coordinates
26210
- return `<g transform="${transform}"${opacity}>
26211
- <svg viewBox="${path.viewBox}" width="${path.totalWidth / scale}" height="${path.totalHeight / scale}" overflow="visible">
26212
- <path d="${path.d}" fill="${fill || 'none'}" stroke="${stroke || 'none'}"${path.strokeWidth ? ` stroke-width="${path.strokeWidth}"` : ''}/>
26213
- </svg>
26214
- </g>`;
26215
- }
26216
- /**
26217
- * Converts a KritzelLine to SVG.
26218
- * @param line - The line object
26219
- * @param theme - Theme for color resolution
26220
- * @returns SVG element string
26221
- */
26222
- static lineToSvg(line, theme) {
26223
- const transform = this.buildTransform(line);
26224
- const stroke = schema_constants.KritzelColorHelper.resolveThemeColor(line.stroke, theme);
26225
- const opacity = line.opacity !== 1 ? ` opacity="${line.opacity}"` : '';
26226
- const scale = this.getObjectScale(line);
26227
- const markerStart = line.hasStartArrow ? ` marker-start="url(#${line.startMarkerId})"` : '';
26228
- const markerEnd = line.hasEndArrow ? ` marker-end="url(#${line.endMarkerId})"` : '';
26229
- return `<g transform="${transform}"${opacity}>
26230
- <svg viewBox="${line.viewBox}" width="${line.totalWidth / scale}" height="${line.totalHeight / scale}" overflow="visible">
26231
- <path d="${line.d}" fill="none" stroke="${stroke}" stroke-width="${line.strokeWidth}" stroke-linecap="round"${markerStart}${markerEnd}/>
26232
- </svg>
26233
- </g>`;
26234
- }
26235
- /**
26236
- * Converts a KritzelShape to SVG.
26237
- * @param shape - The shape object
26238
- * @param theme - Theme for color resolution
26239
- * @returns SVG element string
26240
- */
26241
- static shapeToSvg(shape, theme) {
26242
- const transform = this.buildTransform(shape);
26243
- const fill = schema_constants.KritzelColorHelper.resolveThemeColor(shape.fillColor, theme);
26244
- const stroke = schema_constants.KritzelColorHelper.resolveThemeColor(shape.strokeColor, theme);
26245
- const opacity = shape.opacity !== 1 ? ` opacity="${shape.opacity}"` : '';
26246
- const scale = this.getObjectScale(shape);
26247
- // Get the SVG path for the shape
26248
- const pathD = shape.getSvgPath();
26249
- // Handle text content if present
26250
- let textContent = '';
26251
- if (shape.editor && !shape.editor.state.doc.textContent.trim()) ;
26252
- else if (shape.content) {
26253
- const textHtml = this.prosemirrorToHtml(shape.content, shape, theme);
26254
- if (textHtml) {
26255
- const fontColor = schema_constants.KritzelColorHelper.resolveThemeColor(shape.fontColor, theme);
26256
- textContent = `
26257
- <foreignObject x="${shape.x}" y="${shape.y}" width="${shape.width}" height="${shape.height}">
26258
- <div xmlns="http://www.w3.org/1999/xhtml" style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; text-align: center; font-family: ${shape.fontFamily}; font-size: ${shape.fontSize}px; color: ${fontColor}; overflow: hidden;">
26259
- ${textHtml}
26260
- </div>
26261
- </foreignObject>`;
26262
- }
26263
- }
26264
- return `<g transform="${transform}"${opacity}>
26265
- <svg viewBox="${shape.viewBox}" width="${shape.totalWidth / scale}" height="${shape.totalHeight / scale}" overflow="visible" preserveAspectRatio="none">
26266
- <path d="${pathD}" fill="${fill || 'transparent'}" stroke="${stroke}" stroke-width="${shape.strokeWidth}"/>${textContent}
26267
- </svg>
26268
- </g>`;
26269
- }
26270
- /**
26271
- * Converts a KritzelText to SVG using foreignObject for rich text support.
26272
- * @param text - The text object
26273
- * @param theme - Theme for color resolution
26274
- * @returns SVG element string
26275
- */
26276
- static textToSvg(text, theme, rasterSafeForCanvas = false) {
26277
- if (rasterSafeForCanvas) {
26278
- return this.textToSvgRasterSafe(text, theme);
26279
- }
26280
- const transform = this.buildTransform(text);
26281
- const opacity = text.opacity !== 1 ? ` opacity="${text.opacity}"` : '';
26282
- const fontColor = schema_constants.KritzelColorHelper.resolveThemeColor(text.fontColor, theme);
26283
- const bgColor = schema_constants.KritzelColorHelper.resolveThemeColor(text.backgroundColor, theme);
26284
- const scale = this.getObjectScale(text);
26285
- // Convert ProseMirror content to HTML
26286
- const htmlContent = this.prosemirrorToHtml(text.content, text, theme);
26287
- // Calculate dimensions in SVG world units (world units = CSS px in a 1:1 SVG).
26288
- const width = text.totalWidth / scale;
26289
- const height = text.totalHeight / scale;
26290
- // Convert the font size from pt to SVG world units (px equivalent).
26291
- // text.fontSize is stored in CSS pt; 1 pt = 4/3 px at 96 dpi.
26292
- // We also bake in scaleFactor (user resize) and divide by the object scale so
26293
- // that the rendered font aligns with the world-unit dimensions above.
26294
- // The CSS transform: scale(scaleFactor) is removed from the div because the
26295
- // adjusted font-size already accounts for it – keeping both would double-scale
26296
- // the content and overflow the foreignObject.
26297
- const svgFontSizePx = (text.fontSize * (4 / 3) * (text.scaleFactor || 1) / scale).toFixed(4);
26298
- return `<g transform="${transform}"${opacity}>
26299
- <foreignObject x="0" y="0" width="${width}" height="${height}" overflow="visible">
26300
- <div xmlns="http://www.w3.org/1999/xhtml" style="font-family: ${text.fontFamily}; font-size: ${svgFontSizePx}px; color: ${fontColor}; background-color: ${bgColor || 'transparent'}; white-space: pre-wrap; word-wrap: break-word; width: 100%; margin: 0; padding: 0; box-sizing: border-box;">${htmlContent.trim()}</div>
26301
- </foreignObject>
26302
- </g>`;
26303
- }
26304
- /**
26305
- * Converts a KritzelText to SVG using pure SVG text primitives.
26306
- * This path avoids `<foreignObject>`, which taints canvases when the
26307
- * generated SVG is rasterized through `<img>` and `drawImage`.
26308
- */
26309
- static textToSvgRasterSafe(text, theme) {
26310
- const transform = this.buildTransform(text);
26311
- const opacity = text.opacity !== 1 ? ` opacity="${text.opacity}"` : '';
26312
- const fontColor = schema_constants.KritzelColorHelper.resolveThemeColor(text.fontColor, theme);
26313
- const bgColor = schema_constants.KritzelColorHelper.resolveThemeColor(text.backgroundColor, theme);
26314
- const scale = this.getObjectScale(text);
26315
- const width = text.totalWidth / scale;
26316
- const height = text.totalHeight / scale;
26317
- const lines = this.prosemirrorToPlainTextLines(text.content);
26318
- const resolvedLines = lines.length > 0 ? lines : [''];
26319
- // Convert the font size from pt to SVG world units (px equivalent).
26320
- // text.fontSize is stored in CSS pt; 1 pt = 4/3 px at 96 dpi.
26321
- // We also bake in scaleFactor (user resize) and divide by the object scale so
26322
- // that the rendered lines align with the world-unit bounding box dimensions above.
26323
- const resolvedFontSizePx = Math.max(1, text.fontSize * (4 / 3) * (text.scaleFactor || 1) / scale);
26324
- const lineHeight = resolvedFontSizePx * 1.2;
26325
- const startY = resolvedFontSizePx;
26326
- const bgRect = bgColor && bgColor !== 'transparent'
26327
- ? `<rect x="0" y="0" width="${width}" height="${height}" fill="${bgColor}"/>`
26328
- : '';
26329
- const tspans = resolvedLines
26330
- .map((line, index) => {
26331
- const y = startY + index * lineHeight;
26332
- return `<tspan x="0" y="${y}" xml:space="preserve">${this.escapeHtml(line)}</tspan>`;
26333
- })
26334
- .join('');
26335
- return `<g transform="${transform}"${opacity}>
26336
- ${bgRect}
26337
- <text font-family="${this.escapeHtml(text.fontFamily)}" font-size="${resolvedFontSizePx}" fill="${fontColor}">${tspans}</text>
26338
- </g>`;
26339
- }
26340
- /**
26341
- * Converts a KritzelImage to SVG.
26342
- *
26343
- * Prefers an inlined `data:` URL from `imageDataUrls` (keyed by object
26344
- * id) when provided, so the embedded bytes survive rasterization to
26345
- * PNG. Falls back to `resolvedSrc` (populated by the renderer via the
26346
- * asset resolver) and finally the legacy inline `src` field for
26347
- * documents persisted before the asset layer existed.
26348
- *
26349
- * Note: a `blob:` `resolvedSrc` renders correctly on-canvas but becomes
26350
- * a broken-image placeholder once the SVG is loaded into an `<img>` and
26351
- * drawn to a canvas. Callers that rasterize the SVG must pass
26352
- * `imageDataUrls` (see {@link KritzelSvgExportHelper.resolveImageDataUrls}).
26353
- * @param image - The image object
26354
- * @param imageDataUrls - Optional map of image id -> inlined data URL
26355
- * @returns SVG element string
26356
- */
26357
- static imageToSvg(image, imageDataUrls) {
26358
- const transform = this.buildTransform(image);
26359
- const opacity = image.opacity !== 1 ? ` opacity="${image.opacity}"` : '';
26360
- const scale = this.getObjectScale(image);
26361
- // Calculate dimensions
26362
- const width = image.totalWidth / scale;
26363
- const height = image.totalHeight / scale;
26364
- const href = imageDataUrls?.get(image.id) || image.resolvedSrc || image.src || '';
26365
- return `<g transform="${transform}"${opacity}>
26366
- <image href="${href}" x="0" y="0" width="${width}" height="${height}" preserveAspectRatio="xMidYMid meet"/>
26367
- </g>`;
26368
- }
26369
- /**
26370
- * Converts a KritzelCustomElement to SVG.
26371
- * SVG mode: embeds HTML in foreignObject (best-effort).
26372
- * PNG/raster mode: renders placeholder rect with warning.
26373
- * @param obj - The custom element object
26374
- * @param rasterSafeForCanvas - If true, render placeholder (for PNG export)
26375
- * @returns SVG element string
26376
- */
26377
- static customElementToSvg(obj, rasterSafeForCanvas = false) {
26378
- const transform = this.buildTransform(obj);
26379
- const opacity = obj.opacity !== 1 ? ` opacity="${obj.opacity}"` : '';
26380
- const scale = this.getObjectScale(obj);
26381
- const width = obj.totalWidth / scale;
26382
- const height = obj.totalHeight / scale;
26383
- if (rasterSafeForCanvas) {
26384
- // PNG export: render placeholder rect (foreignObject doesn't rasterize to PNG well)
26385
- console.warn('[KritzelCustomElement SVG Export] Custom elements do not serialize well to PNG. Use SVG export or screenshot instead.');
26386
- return `<g transform="${transform}"${opacity}>
26387
- <rect x="0" y="0" width="${width}" height="${height}" fill="#f0f0f0" stroke="#999" stroke-width="1" />
26388
- <text x="${width / 2}" y="${height / 2}" text-anchor="middle" dominant-baseline="middle" font-size="12" fill="#999">[Custom Element]</text>
26389
- </g>`;
26390
- }
26391
- // SVG export: use foreignObject to embed HTML
26392
- const html = obj.element?.outerHTML || '<div style="border:1px dashed #ccc">Empty</div>';
26393
- return `<g transform="${transform}"${opacity}>
26394
- <foreignObject x="0" y="0" width="${width}" height="${height}">
26395
- <div xmlns="http://www.w3.org/1999/xhtml" style="width:100%;height:100%;overflow:hidden;">
26396
- ${html}
26397
- </div>
26398
- </foreignObject>
26399
- </g>`;
26400
- }
26401
- /**
26402
- * Converts a KritzelGroup to SVG by recursively converting children.
26403
- * @param group - The group object
26404
- * @param theme - Theme for color resolution
26405
- * @param imageDataUrls - Optional map of image id -> inlined data URL
26406
- * @returns SVG element string
26407
- */
26408
- static groupToSvg(group, theme, imageDataUrls, rasterSafeForCanvas = false) {
26409
- const children = group.children
26410
- .sort((a, b) => a.zIndex - b.zIndex)
26411
- .map(child => this.objectToSvgElement(child, theme, imageDataUrls, rasterSafeForCanvas))
26412
- .filter(Boolean)
26413
- .join('\n ');
26414
- if (!children) {
26415
- return '';
26416
- }
26417
- const opacity = group.opacity !== 1 ? ` opacity="${group.opacity}"` : '';
26418
- return `<g${opacity}>
26419
- ${children}
26420
- </g>`;
26421
- }
26422
- /**
26423
- * Builds a transform attribute string for an object.
26424
- * Combines translation and rotation.
26425
- * @param object - The object to build transform for
26426
- * @returns Transform attribute value string
26427
- */
26428
- static buildTransform(object) {
26429
- const transforms = [];
26430
- const scale = this.getObjectScale(object);
26431
- // Translation
26432
- transforms.push(`translate(${object.translateX}, ${object.translateY})`);
26433
- // Rotation around center
26434
- if (object.rotation !== 0) {
26435
- const centerX = object.totalWidth / 2 / scale;
26436
- const centerY = object.totalHeight / 2 / scale;
26437
- const degrees = object.rotation * (180 / Math.PI);
26438
- transforms.push(`rotate(${degrees}, ${centerX}, ${centerY})`);
26439
- }
26440
- return transforms.join(' ');
26441
- }
26442
- /**
26443
- * Returns a safe non-zero scale for export geometry calculations.
26444
- */
26445
- static getObjectScale(object) {
26446
- const scale = object.scale ?? 1;
26447
- return scale === 0 ? 1 : scale;
26448
- }
26449
- /**
26450
- * Converts ProseMirror JSON content to HTML string.
26451
- * @param content - ProseMirror document JSON
26452
- * @param context - The text or shape object for styling context
26453
- * @param theme - Theme for color resolution
26454
- * @returns HTML string
26455
- */
26456
- static prosemirrorToHtml(content, _context, theme) {
26457
- if (!content || !content.content) {
26458
- return '';
26459
- }
26460
- return content.content
26461
- .map((node) => this.nodeToHtml(node, theme))
26462
- .join('');
26463
- }
26464
- /**
26465
- * Converts a ProseMirror node to HTML.
26466
- * @param node - ProseMirror node JSON
26467
- * @param theme - Theme for color resolution
26468
- * @returns HTML string
26469
- */
26470
- static nodeToHtml(node, theme) {
26471
- if (!node)
26472
- return '';
26473
- switch (node.type) {
26474
- case 'paragraph':
26475
- const pContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
26476
- return `<p style="margin: 0;">${pContent || '<br/>'}</p>`;
26477
- case 'text':
26478
- let text = this.escapeHtml(node.text || '');
26479
- if (node.marks) {
26480
- for (const mark of node.marks) {
26481
- text = this.applyMark(text, mark, theme);
26482
- }
26483
- }
26484
- return text;
26485
- case 'hard_break':
26486
- return '<br/>';
26487
- case 'bullet_list':
26488
- const blContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
26489
- return `<ul style="margin: 0; padding-left: 1.5em;">${blContent}</ul>`;
26490
- case 'ordered_list':
26491
- const olContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
26492
- return `<ol style="margin: 0; padding-left: 1.5em;">${olContent}</ol>`;
26493
- case 'list_item':
26494
- const liContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
26495
- return `<li>${liContent}</li>`;
26496
- default:
26497
- // For unknown node types, try to render content
26498
- if (node.content) {
26499
- return node.content.map((n) => this.nodeToHtml(n, theme)).join('');
26500
- }
26501
- return '';
26502
- }
26503
- }
26504
- /**
26505
- * Converts ProseMirror JSON content to plain-text lines.
26506
- * Used by raster-safe SVG export, which cannot depend on HTML/foreignObject.
26507
- */
26508
- static prosemirrorToPlainTextLines(content) {
26509
- if (!content || !Array.isArray(content.content)) {
26510
- return [];
26511
- }
26512
- const plainText = content.content
26513
- .map((node) => this.nodeToPlainText(node))
26514
- .join('')
26515
- .replace(/\r\n/g, '\n')
26516
- .replace(/\n+$/, '');
26517
- if (!plainText) {
26518
- return [];
26519
- }
26520
- return plainText.split('\n');
26521
- }
26522
- /**
26523
- * Converts a ProseMirror node tree into plain text.
26524
- */
26525
- static nodeToPlainText(node) {
26526
- if (!node) {
26527
- return '';
26528
- }
26529
- switch (node.type) {
26530
- case 'text':
26531
- return node.text || '';
26532
- case 'hard_break':
26533
- return '\n';
26534
- case 'paragraph': {
26535
- const paragraph = Array.isArray(node.content)
26536
- ? node.content.map((child) => this.nodeToPlainText(child)).join('')
26537
- : '';
26538
- return `${paragraph}\n`;
26539
- }
26540
- case 'list_item': {
26541
- const item = Array.isArray(node.content)
26542
- ? node.content.map((child) => this.nodeToPlainText(child)).join('')
26543
- : '';
26544
- return `- ${item.trim()}\n`;
26545
- }
26546
- default:
26547
- return Array.isArray(node.content)
26548
- ? node.content.map((child) => this.nodeToPlainText(child)).join('')
26549
- : '';
26550
- }
26551
- }
26552
- /**
26553
- * Applies a ProseMirror mark to text.
26554
- * @param text - The text to wrap
26555
- * @param mark - The mark to apply
26556
- * @param theme - Theme for color resolution
26557
- * @returns Wrapped HTML string
26558
- */
26559
- static applyMark(text, mark, _theme) {
26560
- switch (mark.type) {
26561
- case 'bold':
26562
- case 'strong':
26563
- return `<strong>${text}</strong>`;
26564
- case 'italic':
26565
- case 'em':
26566
- return `<em>${text}</em>`;
26567
- case 'underline':
26568
- return `<u>${text}</u>`;
26569
- case 'strike':
26570
- return `<s>${text}</s>`;
26571
- case 'code':
26572
- return `<code>${text}</code>`;
26573
- case 'link':
26574
- const href = mark.attrs?.href || '#';
26575
- return `<a href="${this.escapeHtml(href)}">${text}</a>`;
26576
- default:
26577
- return text;
26578
- }
26579
- }
26580
- /**
26581
- * Escapes HTML special characters.
26582
- * @param str - String to escape
26583
- * @returns Escaped string
26584
- */
26585
- static escapeHtml(str) {
26586
- return str
26587
- .replace(/&/g, '&amp;')
26588
- .replace(/</g, '&lt;')
26589
- .replace(/>/g, '&gt;')
26590
- .replace(/"/g, '&quot;')
26591
- .replace(/'/g, '&#039;');
26592
- }
26593
- }
26594
-
26595
- function resolveUrl(url, baseUrl) {
26596
- // url is absolute already
26597
- if (url.match(/^[a-z]+:\/\//i)) {
26598
- return url;
26599
- }
26600
- // url is absolute already, without protocol
26601
- if (url.match(/^\/\//)) {
26602
- return window.location.protocol + url;
26603
- }
26604
- // dataURI, mailto:, tel:, etc.
26605
- if (url.match(/^[a-z]+:/i)) {
26606
- return url;
26607
- }
26608
- const doc = document.implementation.createHTMLDocument();
26609
- const base = doc.createElement('base');
26610
- const a = doc.createElement('a');
26611
- doc.head.appendChild(base);
26612
- doc.body.appendChild(a);
26613
- if (baseUrl) {
26614
- base.href = baseUrl;
26615
- }
26616
- a.href = url;
26617
- return a.href;
26618
- }
26619
- const uuid = (() => {
26620
- // generate uuid for className of pseudo elements.
26621
- // We should not use GUIDs, otherwise pseudo elements sometimes cannot be captured.
26622
- let counter = 0;
26623
- // ref: http://stackoverflow.com/a/6248722/2519373
26624
- const random = () =>
26625
- // eslint-disable-next-line no-bitwise
26626
- `0000${((Math.random() * 36 ** 4) << 0).toString(36)}`.slice(-4);
26627
- return () => {
26628
- counter += 1;
26629
- return `u${random()}${counter}`;
26630
- };
26631
- })();
26632
- function toArray(arrayLike) {
26633
- const arr = [];
26634
- for (let i = 0, l = arrayLike.length; i < l; i++) {
26635
- arr.push(arrayLike[i]);
26636
- }
26637
- return arr;
26638
- }
26639
- let styleProps = null;
26640
- function getStyleProperties(options = {}) {
26641
- if (styleProps) {
26642
- return styleProps;
26643
- }
26644
- if (options.includeStyleProperties) {
26645
- styleProps = options.includeStyleProperties;
26646
- return styleProps;
26647
- }
26648
- styleProps = toArray(window.getComputedStyle(document.documentElement));
26649
- return styleProps;
26650
- }
26651
- function px(node, styleProperty) {
26652
- const win = node.ownerDocument.defaultView || window;
26653
- const val = win.getComputedStyle(node).getPropertyValue(styleProperty);
26654
- return val ? parseFloat(val.replace('px', '')) : 0;
26655
- }
26656
- function getNodeWidth(node) {
26657
- const leftBorder = px(node, 'border-left-width');
26658
- const rightBorder = px(node, 'border-right-width');
26659
- return node.clientWidth + leftBorder + rightBorder;
26660
- }
26661
- function getNodeHeight(node) {
26662
- const topBorder = px(node, 'border-top-width');
26663
- const bottomBorder = px(node, 'border-bottom-width');
26664
- return node.clientHeight + topBorder + bottomBorder;
26665
- }
26666
- function getImageSize(targetNode, options = {}) {
26667
- const width = options.width || getNodeWidth(targetNode);
26668
- const height = options.height || getNodeHeight(targetNode);
26669
- return { width, height };
26670
- }
26671
- function getPixelRatio() {
26672
- let ratio;
26673
- let FINAL_PROCESS;
26674
- try {
26675
- FINAL_PROCESS = process;
26676
- }
26677
- catch (e) {
26678
- // pass
26679
- }
26680
- const val = FINAL_PROCESS && FINAL_PROCESS.env
26681
- ? FINAL_PROCESS.env.devicePixelRatio
26682
- : null;
26683
- if (val) {
26684
- ratio = parseInt(val, 10);
26685
- if (Number.isNaN(ratio)) {
26686
- ratio = 1;
26687
- }
26688
- }
26689
- return ratio || window.devicePixelRatio || 1;
26690
- }
26691
- // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size
26692
- const canvasDimensionLimit = 16384;
26693
- function checkCanvasDimensions(canvas) {
26694
- if (canvas.width > canvasDimensionLimit ||
26695
- canvas.height > canvasDimensionLimit) {
26696
- if (canvas.width > canvasDimensionLimit &&
26697
- canvas.height > canvasDimensionLimit) {
26698
- if (canvas.width > canvas.height) {
26699
- canvas.height *= canvasDimensionLimit / canvas.width;
26700
- canvas.width = canvasDimensionLimit;
26701
- }
26702
- else {
26703
- canvas.width *= canvasDimensionLimit / canvas.height;
26704
- canvas.height = canvasDimensionLimit;
26705
- }
26706
- }
26707
- else if (canvas.width > canvasDimensionLimit) {
26708
- canvas.height *= canvasDimensionLimit / canvas.width;
26709
- canvas.width = canvasDimensionLimit;
26710
- }
26711
- else {
26712
- canvas.width *= canvasDimensionLimit / canvas.height;
26713
- canvas.height = canvasDimensionLimit;
26714
- }
26715
- }
26716
- }
26717
- function createImage(url) {
26718
- return new Promise((resolve, reject) => {
26719
- const img = new Image();
26720
- img.onload = () => {
26721
- img.decode().then(() => {
26722
- requestAnimationFrame(() => resolve(img));
26723
- });
26724
- };
26725
- img.onerror = reject;
26726
- img.crossOrigin = 'anonymous';
26727
- img.decoding = 'async';
26728
- img.src = url;
26729
- });
26730
- }
26731
- async function svgToDataURL(svg) {
26732
- return Promise.resolve()
26733
- .then(() => new XMLSerializer().serializeToString(svg))
26734
- .then(encodeURIComponent)
26735
- .then((html) => `data:image/svg+xml;charset=utf-8,${html}`);
26736
- }
26737
- async function nodeToDataURL(node, width, height) {
26738
- const xmlns = 'http://www.w3.org/2000/svg';
26739
- const svg = document.createElementNS(xmlns, 'svg');
26740
- const foreignObject = document.createElementNS(xmlns, 'foreignObject');
26741
- svg.setAttribute('width', `${width}`);
26742
- svg.setAttribute('height', `${height}`);
26743
- svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
26744
- foreignObject.setAttribute('width', '100%');
26745
- foreignObject.setAttribute('height', '100%');
26746
- foreignObject.setAttribute('x', '0');
26747
- foreignObject.setAttribute('y', '0');
26748
- foreignObject.setAttribute('externalResourcesRequired', 'true');
26749
- svg.appendChild(foreignObject);
26750
- foreignObject.appendChild(node);
26751
- return svgToDataURL(svg);
26752
- }
26753
- const isInstanceOfElement = (node, instance) => {
26754
- if (node instanceof instance)
26755
- return true;
26756
- const nodePrototype = Object.getPrototypeOf(node);
26757
- if (nodePrototype === null)
26758
- return false;
26759
- return (nodePrototype.constructor.name === instance.name ||
26760
- isInstanceOfElement(nodePrototype, instance));
26761
- };
26762
-
26763
- function formatCSSText(style) {
26764
- const content = style.getPropertyValue('content');
26765
- return `${style.cssText} content: '${content.replace(/'|"/g, '')}';`;
26766
- }
26767
- function formatCSSProperties(style, options) {
26768
- return getStyleProperties(options)
26769
- .map((name) => {
26770
- const value = style.getPropertyValue(name);
26771
- const priority = style.getPropertyPriority(name);
26772
- return `${name}: ${value}${priority ? ' !important' : ''};`;
26773
- })
26774
- .join(' ');
26775
- }
26776
- function getPseudoElementStyle(className, pseudo, style, options) {
26777
- const selector = `.${className}:${pseudo}`;
26778
- const cssText = style.cssText
26779
- ? formatCSSText(style)
26780
- : formatCSSProperties(style, options);
26781
- return document.createTextNode(`${selector}{${cssText}}`);
26782
- }
26783
- function clonePseudoElement(nativeNode, clonedNode, pseudo, options) {
26784
- const style = window.getComputedStyle(nativeNode, pseudo);
26785
- const content = style.getPropertyValue('content');
26786
- if (content === '' || content === 'none') {
26787
- return;
26788
- }
26789
- const className = uuid();
26790
- try {
26791
- clonedNode.className = `${clonedNode.className} ${className}`;
26792
- }
26793
- catch (err) {
26794
- return;
26795
- }
26796
- const styleElement = document.createElement('style');
26797
- styleElement.appendChild(getPseudoElementStyle(className, pseudo, style, options));
26798
- clonedNode.appendChild(styleElement);
26799
- }
26800
- function clonePseudoElements(nativeNode, clonedNode, options) {
26801
- clonePseudoElement(nativeNode, clonedNode, ':before', options);
26802
- clonePseudoElement(nativeNode, clonedNode, ':after', options);
26803
- }
26804
-
26805
- const WOFF = 'application/font-woff';
26806
- const JPEG = 'image/jpeg';
26807
- const mimes = {
26808
- woff: WOFF,
26809
- woff2: WOFF,
26810
- ttf: 'application/font-truetype',
26811
- eot: 'application/vnd.ms-fontobject',
26812
- png: 'image/png',
26813
- jpg: JPEG,
26814
- jpeg: JPEG,
26815
- gif: 'image/gif',
26816
- tiff: 'image/tiff',
26817
- svg: 'image/svg+xml',
26818
- webp: 'image/webp',
26819
- };
26820
- function getExtension(url) {
26821
- const match = /\.([^./]*?)$/g.exec(url);
26822
- return match ? match[1] : '';
26823
- }
26824
- function getMimeType(url) {
26825
- const extension = getExtension(url).toLowerCase();
26826
- return mimes[extension] || '';
26827
- }
26828
-
26829
- function getContentFromDataUrl(dataURL) {
26830
- return dataURL.split(/,/)[1];
26831
- }
26832
- function isDataUrl(url) {
26833
- return url.search(/^(data:)/) !== -1;
26834
- }
26835
- function makeDataUrl(content, mimeType) {
26836
- return `data:${mimeType};base64,${content}`;
26837
- }
26838
- async function fetchAsDataURL(url, init, process) {
26839
- const res = await fetch(url, init);
26840
- if (res.status === 404) {
26841
- throw new Error(`Resource "${res.url}" not found`);
26842
- }
26843
- const blob = await res.blob();
26844
- return new Promise((resolve, reject) => {
26845
- const reader = new FileReader();
26846
- reader.onerror = reject;
26847
- reader.onloadend = () => {
26848
- try {
26849
- resolve(process({ res, result: reader.result }));
26850
- }
26851
- catch (error) {
26852
- reject(error);
26853
- }
26854
- };
26855
- reader.readAsDataURL(blob);
26856
- });
26857
- }
26858
- const cache = {};
26859
- function getCacheKey(url, contentType, includeQueryParams) {
26860
- let key = url.replace(/\?.*/, '');
26861
- if (includeQueryParams) {
26862
- key = url;
26863
- }
26864
- // font resource
26865
- if (/ttf|otf|eot|woff2?/i.test(key)) {
26866
- key = key.replace(/.*\//, '');
26867
- }
26868
- return contentType ? `[${contentType}]${key}` : key;
26869
- }
26870
- async function resourceToDataURL(resourceUrl, contentType, options) {
26871
- const cacheKey = getCacheKey(resourceUrl, contentType, options.includeQueryParams);
26872
- if (cache[cacheKey] != null) {
26873
- return cache[cacheKey];
26874
- }
26875
- // ref: https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache
26876
- if (options.cacheBust) {
26877
- // eslint-disable-next-line no-param-reassign
26878
- resourceUrl += (/\?/.test(resourceUrl) ? '&' : '?') + new Date().getTime();
26879
- }
26880
- let dataURL;
26881
- try {
26882
- const content = await fetchAsDataURL(resourceUrl, options.fetchRequestInit, ({ res, result }) => {
26883
- if (!contentType) {
26884
- // eslint-disable-next-line no-param-reassign
26885
- contentType = res.headers.get('Content-Type') || '';
26886
- }
26887
- return getContentFromDataUrl(result);
26888
- });
26889
- dataURL = makeDataUrl(content, contentType);
26890
- }
26891
- catch (error) {
26892
- dataURL = options.imagePlaceholder || '';
26893
- let msg = `Failed to fetch resource: ${resourceUrl}`;
26894
- if (error) {
26895
- msg = typeof error === 'string' ? error : error.message;
26896
- }
26897
- if (msg) {
26898
- console.warn(msg);
26307
+ if (msg) {
26308
+ console.warn(msg);
26899
26309
  }
26900
26310
  }
26901
26311
  cache[cacheKey] = dataURL;
@@ -27429,156 +26839,808 @@ async function embedWebFonts(clonedNode, options) {
27429
26839
  clonedNode.appendChild(styleNode);
27430
26840
  }
27431
26841
  }
27432
- }
27433
-
27434
- async function toSvg(node, options = {}) {
27435
- const { width, height } = getImageSize(node, options);
27436
- const clonedNode = (await cloneNode(node, options, true));
27437
- await embedWebFonts(clonedNode, options);
27438
- await embedImages(clonedNode, options);
27439
- applyStyle(clonedNode, options);
27440
- const datauri = await nodeToDataURL(clonedNode, width, height);
27441
- return datauri;
27442
- }
27443
- async function toCanvas(node, options = {}) {
27444
- const { width, height } = getImageSize(node, options);
27445
- const svg = await toSvg(node, options);
27446
- const img = await createImage(svg);
27447
- const canvas = document.createElement('canvas');
27448
- const context = canvas.getContext('2d');
27449
- const ratio = options.pixelRatio || getPixelRatio();
27450
- const canvasWidth = options.canvasWidth || width;
27451
- const canvasHeight = options.canvasHeight || height;
27452
- canvas.width = canvasWidth * ratio;
27453
- canvas.height = canvasHeight * ratio;
27454
- if (!options.skipAutoScale) {
27455
- checkCanvasDimensions(canvas);
26842
+ }
26843
+
26844
+ async function toSvg(node, options = {}) {
26845
+ const { width, height } = getImageSize(node, options);
26846
+ const clonedNode = (await cloneNode(node, options, true));
26847
+ await embedWebFonts(clonedNode, options);
26848
+ await embedImages(clonedNode, options);
26849
+ applyStyle(clonedNode, options);
26850
+ const datauri = await nodeToDataURL(clonedNode, width, height);
26851
+ return datauri;
26852
+ }
26853
+ async function toCanvas(node, options = {}) {
26854
+ const { width, height } = getImageSize(node, options);
26855
+ const svg = await toSvg(node, options);
26856
+ const img = await createImage(svg);
26857
+ const canvas = document.createElement('canvas');
26858
+ const context = canvas.getContext('2d');
26859
+ const ratio = options.pixelRatio || getPixelRatio();
26860
+ const canvasWidth = options.canvasWidth || width;
26861
+ const canvasHeight = options.canvasHeight || height;
26862
+ canvas.width = canvasWidth * ratio;
26863
+ canvas.height = canvasHeight * ratio;
26864
+ if (!options.skipAutoScale) {
26865
+ checkCanvasDimensions(canvas);
26866
+ }
26867
+ canvas.style.width = `${canvasWidth}`;
26868
+ canvas.style.height = `${canvasHeight}`;
26869
+ if (options.backgroundColor) {
26870
+ context.fillStyle = options.backgroundColor;
26871
+ context.fillRect(0, 0, canvas.width, canvas.height);
26872
+ }
26873
+ context.drawImage(img, 0, 0, canvas.width, canvas.height);
26874
+ return canvas;
26875
+ }
26876
+ async function toPng(node, options = {}) {
26877
+ const canvas = await toCanvas(node, options);
26878
+ return canvas.toDataURL();
26879
+ }
26880
+
26881
+ class KritzelWheelHelper {
26882
+ static shouldKeepWheelInCustomElement(ev, customElement) {
26883
+ // Keep Ctrl+wheel available for viewport zoom gestures.
26884
+ if (ev.ctrlKey || customElement.isSelected !== true) {
26885
+ return false;
26886
+ }
26887
+ const path = typeof ev.composedPath === 'function' ? ev.composedPath() : [];
26888
+ const scrollContainer = this.getScrollableContainerFromEventPath(path, customElement);
26889
+ // No scroll container found inside the custom element: let the wheel
26890
+ // event pass through so the viewport can continue handling it.
26891
+ if (!scrollContainer) {
26892
+ return false;
26893
+ }
26894
+ return this.canElementConsumeWheel(scrollContainer, ev.deltaX, ev.deltaY);
26895
+ }
26896
+ static getScrollableContainerFromEventPath(path, customElement) {
26897
+ const potentialRoots = [];
26898
+ if (customElement.element instanceof HTMLElement) {
26899
+ potentialRoots.push(customElement.element);
26900
+ }
26901
+ if (customElement.elementRef instanceof HTMLElement) {
26902
+ potentialRoots.push(customElement.elementRef);
26903
+ }
26904
+ const legacyElementRef = customElement.elmeentRef;
26905
+ if (legacyElementRef instanceof HTMLElement) {
26906
+ potentialRoots.push(legacyElementRef);
26907
+ }
26908
+ for (const root of potentialRoots) {
26909
+ const rootIndex = path.findIndex(entry => entry === root);
26910
+ if (rootIndex === -1) {
26911
+ continue;
26912
+ }
26913
+ for (const entry of path.slice(0, rootIndex + 1)) {
26914
+ if (!(entry instanceof HTMLElement)) {
26915
+ continue;
26916
+ }
26917
+ if (this.isScrollableElement(entry)) {
26918
+ return entry;
26919
+ }
26920
+ }
26921
+ }
26922
+ // Fallback for synthetic/custom wheel events where composedPath is absent
26923
+ // or does not include the custom-element subtree.
26924
+ for (const root of potentialRoots) {
26925
+ if (this.isScrollableElement(root)) {
26926
+ return root;
26927
+ }
26928
+ const firstChild = root.firstElementChild;
26929
+ if (firstChild instanceof HTMLElement && this.isScrollableElement(firstChild)) {
26930
+ return firstChild;
26931
+ }
26932
+ }
26933
+ return null;
26934
+ }
26935
+ static isScrollableElement(element) {
26936
+ const canScrollVertically = element.scrollHeight - element.clientHeight > 0;
26937
+ const canScrollHorizontally = element.scrollWidth - element.clientWidth > 0;
26938
+ if (!canScrollVertically && !canScrollHorizontally) {
26939
+ return false;
26940
+ }
26941
+ const computedStyle = window.getComputedStyle(element);
26942
+ const overflowX = computedStyle.overflowX;
26943
+ const overflowY = computedStyle.overflowY;
26944
+ const allowsVerticalScroll = overflowY !== 'visible' && overflowY !== 'clip';
26945
+ const allowsHorizontalScroll = overflowX !== 'visible' && overflowX !== 'clip';
26946
+ return (canScrollVertically && allowsVerticalScroll) || (canScrollHorizontally && allowsHorizontalScroll);
26947
+ }
26948
+ static canElementConsumeWheel(element, deltaX, deltaY) {
26949
+ const canConsumeVertical = this.canElementConsumeVerticalWheel(element, deltaY);
26950
+ const canConsumeHorizontal = this.canElementConsumeHorizontalWheel(element, deltaX);
26951
+ // Keep wheel events inside the custom element whenever at least one
26952
+ // requested axis can still be consumed by its scroll container.
26953
+ return canConsumeVertical || canConsumeHorizontal;
26954
+ }
26955
+ static canElementConsumeVerticalWheel(element, deltaY) {
26956
+ if (deltaY === 0) {
26957
+ return false;
26958
+ }
26959
+ const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
26960
+ if (maxScrollTop === 0) {
26961
+ return false;
26962
+ }
26963
+ const epsilon = 1;
26964
+ const scrollingDown = deltaY > 0;
26965
+ const scrollingUp = deltaY < 0;
26966
+ if (scrollingDown) {
26967
+ return element.scrollTop < maxScrollTop - epsilon;
26968
+ }
26969
+ if (scrollingUp) {
26970
+ return element.scrollTop > epsilon;
26971
+ }
26972
+ return false;
26973
+ }
26974
+ static canElementConsumeHorizontalWheel(element, deltaX) {
26975
+ if (deltaX === 0) {
26976
+ return false;
26977
+ }
26978
+ const maxScrollLeft = Math.max(0, element.scrollWidth - element.clientWidth);
26979
+ if (maxScrollLeft === 0) {
26980
+ return false;
26981
+ }
26982
+ const epsilon = 1;
26983
+ const scrollingRight = deltaX > 0;
26984
+ const scrollingLeft = deltaX < 0;
26985
+ if (scrollingRight) {
26986
+ return element.scrollLeft < maxScrollLeft - epsilon;
26987
+ }
26988
+ if (scrollingLeft) {
26989
+ return element.scrollLeft > epsilon;
26990
+ }
26991
+ return false;
26992
+ }
26993
+ }
26994
+
26995
+ /**
26996
+ * Helper class for exporting Kritzel objects to SVG format.
26997
+ * Generates clean, standards-compliant SVG markup from canvas objects.
26998
+ */
26999
+ class KritzelSvgExportHelper {
27000
+ /**
27001
+ * Calculates the combined bounding box that encompasses all objects.
27002
+ * Uses rotatedBoundingBox to account for object rotation.
27003
+ * @param objects - Array of objects to calculate bounds for
27004
+ * @returns Combined bounding box, or null if no objects provided
27005
+ */
27006
+ static calculateCombinedBounds(objects) {
27007
+ if (!objects || objects.length === 0) {
27008
+ return null;
27009
+ }
27010
+ let minX = Infinity;
27011
+ let minY = Infinity;
27012
+ let maxX = -Infinity;
27013
+ let maxY = -Infinity;
27014
+ for (const obj of objects) {
27015
+ const bbox = obj.rotatedBoundingBox;
27016
+ minX = Math.min(minX, bbox.x);
27017
+ minY = Math.min(minY, bbox.y);
27018
+ maxX = Math.max(maxX, bbox.x + bbox.width);
27019
+ maxY = Math.max(maxY, bbox.y + bbox.height);
27020
+ }
27021
+ return {
27022
+ x: minX,
27023
+ y: minY,
27024
+ z: 1,
27025
+ width: maxX - minX,
27026
+ height: maxY - minY,
27027
+ };
27028
+ }
27029
+ /**
27030
+ * Generates a complete SVG document string from the given objects.
27031
+ * @param objects - Objects to export
27032
+ * @param options - Export options
27033
+ * @returns SVG document string
27034
+ */
27035
+ static generateSvg(objects, options = {}) {
27036
+ const { theme, padding = 0, includeXmlDeclaration = true, rasterSafeForCanvas = false, imageDataUrls } = options;
27037
+ if (!objects || objects.length === 0) {
27038
+ return '';
27039
+ }
27040
+ // Flatten groups to get all objects including nested children
27041
+ const allObjects = this.flattenObjects(objects);
27042
+ const bounds = this.calculateCombinedBounds(allObjects);
27043
+ if (!bounds) {
27044
+ return '';
27045
+ }
27046
+ // Add padding to bounds
27047
+ const viewBoxX = bounds.x - padding;
27048
+ const viewBoxY = bounds.y - padding;
27049
+ const viewBoxWidth = bounds.width + padding * 2;
27050
+ const viewBoxHeight = bounds.height + padding * 2;
27051
+ // Collect all defs (markers, patterns, etc.)
27052
+ const defs = this.collectDefs(allObjects, theme);
27053
+ // Generate SVG elements for each object
27054
+ const elements = objects
27055
+ .sort((a, b) => a.zIndex - b.zIndex)
27056
+ .map(obj => this.objectToSvgElement(obj, theme, imageDataUrls, rasterSafeForCanvas))
27057
+ .filter(Boolean)
27058
+ .join('\n ');
27059
+ const xmlDecl = includeXmlDeclaration ? '<?xml version="1.0" encoding="UTF-8"?>\n' : '';
27060
+ const defsContent = defs.length > 0 ? `\n <defs>\n ${defs.join('\n ')}\n </defs>` : '';
27061
+ return `${xmlDecl}<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBoxX} ${viewBoxY} ${viewBoxWidth} ${viewBoxHeight}" width="${viewBoxWidth}" height="${viewBoxHeight}">${defsContent}
27062
+ ${elements}
27063
+ </svg>`;
27064
+ }
27065
+ /**
27066
+ * Flattens a list of objects, expanding groups into their children recursively.
27067
+ * @param objects - Objects to flatten
27068
+ * @returns Flat array of all objects including group children
27069
+ */
27070
+ static flattenObjects(objects) {
27071
+ const result = [];
27072
+ for (const obj of objects) {
27073
+ if (schema_constants.KritzelClassHelper.isInstanceOf(obj, 'KritzelGroup')) {
27074
+ result.push(...this.flattenObjects(obj.children));
27075
+ }
27076
+ else {
27077
+ result.push(obj);
27078
+ }
27079
+ }
27080
+ return result;
27081
+ }
27082
+ /**
27083
+ * Resolves every image referenced by the given objects (including those
27084
+ * nested inside groups) to an inlined `data:` URL, keyed by object id.
27085
+ *
27086
+ * This is required for export flows that rasterize the SVG (e.g. PNG
27087
+ * export): an SVG loaded into an `<img>` element will NOT load external
27088
+ * or `blob:` resources, so a `<image href="blob:...">` renders as a
27089
+ * broken-image placeholder. Embedding the bytes as a base64 `data:` URL
27090
+ * makes the image self-contained and survive rasterization.
27091
+ *
27092
+ * Resolution is best-effort: images that fail to resolve are simply
27093
+ * omitted from the map, and the SVG falls back to whatever `href` the
27094
+ * synchronous path would have produced.
27095
+ * @param objects - Objects to inline images for
27096
+ * @param assetResolver - Resolver used to fetch asset bytes; optional
27097
+ * @returns Map of image object id -> inlined `data:` URL
27098
+ */
27099
+ static async resolveImageDataUrls(objects, assetResolver) {
27100
+ const dataUrls = new Map();
27101
+ if (!objects || objects.length === 0) {
27102
+ return dataUrls;
27103
+ }
27104
+ const images = this.flattenObjects(objects).filter((obj) => schema_constants.KritzelClassHelper.isInstanceOf(obj, 'KritzelImage'));
27105
+ await Promise.all(images.map(async (image) => {
27106
+ // Legacy inline data URL is already embeddable as-is.
27107
+ if (image.src?.startsWith('data:')) {
27108
+ dataUrls.set(image.id, image.src);
27109
+ return;
27110
+ }
27111
+ // A resolvedSrc that is itself a data URL can be embedded directly.
27112
+ if (image.resolvedSrc?.startsWith('data:')) {
27113
+ dataUrls.set(image.id, image.resolvedSrc);
27114
+ return;
27115
+ }
27116
+ if (!image.assetId || !assetResolver) {
27117
+ return;
27118
+ }
27119
+ try {
27120
+ const blob = await assetResolver.fetchBlob(image.assetId);
27121
+ dataUrls.set(image.id, await this.blobToDataUrl(blob));
27122
+ }
27123
+ catch (err) {
27124
+ console.warn(`[KritzelSvgExportHelper] Failed to inline image asset ${image.assetId} for export:`, err);
27125
+ }
27126
+ }));
27127
+ return dataUrls;
27128
+ }
27129
+ /**
27130
+ * Reads a Blob into a base64 `data:` URL.
27131
+ * @param blob - The blob to encode
27132
+ * @returns Promise resolving to the data URL string
27133
+ */
27134
+ static async blobToDataUrl(blob) {
27135
+ // Prefer FileReader in browser-like runtimes for efficiency.
27136
+ const hasFileReader = typeof FileReader !== 'undefined';
27137
+ if (hasFileReader) {
27138
+ try {
27139
+ return await new Promise((resolve, reject) => {
27140
+ const reader = new FileReader();
27141
+ reader.onload = () => {
27142
+ if (typeof reader.result === 'string') {
27143
+ resolve(reader.result);
27144
+ return;
27145
+ }
27146
+ reject(new Error('FileReader result is not a string'));
27147
+ };
27148
+ reader.onerror = () => reject(reader.error ?? new Error('Failed to read blob as data URL'));
27149
+ reader.readAsDataURL(blob);
27150
+ });
27151
+ }
27152
+ catch {
27153
+ // Fall through to the ArrayBuffer-based encoder.
27154
+ }
27155
+ }
27156
+ const bytes = new Uint8Array(await blob.arrayBuffer());
27157
+ const mimeType = blob.type || 'application/octet-stream';
27158
+ return `data:${mimeType};base64,${this.uint8ToBase64(bytes)}`;
27159
+ }
27160
+ /**
27161
+ * Encodes a byte array to base64 across browser and Node-like runtimes.
27162
+ */
27163
+ static uint8ToBase64(bytes) {
27164
+ if (typeof Buffer !== 'undefined') {
27165
+ return Buffer.from(bytes).toString('base64');
27166
+ }
27167
+ let binary = '';
27168
+ const chunkSize = 0x8000;
27169
+ for (let i = 0; i < bytes.length; i += chunkSize) {
27170
+ const chunk = bytes.subarray(i, Math.min(i + chunkSize, bytes.length));
27171
+ binary += String.fromCharCode(...chunk);
27172
+ }
27173
+ if (typeof btoa === 'function') {
27174
+ return btoa(binary);
27175
+ }
27176
+ throw new Error('No base64 encoder available in this runtime');
27177
+ }
27178
+ /**
27179
+ * Collects SVG defs (markers, patterns, etc.) needed by the objects.
27180
+ * @param objects - Objects to collect defs for
27181
+ * @param theme - Theme for color resolution
27182
+ * @returns Array of def element strings
27183
+ */
27184
+ static collectDefs(objects, theme) {
27185
+ const defs = [];
27186
+ for (const obj of objects) {
27187
+ if (schema_constants.KritzelClassHelper.isInstanceOf(obj, 'KritzelLine')) {
27188
+ if (obj.hasStartArrow) {
27189
+ defs.push(this.generateArrowMarker(obj, 'start', theme));
27190
+ }
27191
+ if (obj.hasEndArrow) {
27192
+ defs.push(this.generateArrowMarker(obj, 'end', theme));
27193
+ }
27194
+ }
27195
+ }
27196
+ return defs;
27197
+ }
27198
+ /**
27199
+ * Generates an SVG marker element for line arrows.
27200
+ * @param line - The line object
27201
+ * @param end - Which end of the line ('start' or 'end')
27202
+ * @param theme - Theme for color resolution
27203
+ * @returns SVG marker element string
27204
+ */
27205
+ static generateArrowMarker(line, end, _theme) {
27206
+ const markerId = end === 'start' ? line.startMarkerId : line.endMarkerId;
27207
+ const size = line.getArrowSize(end);
27208
+ const fill = line.getArrowFill(end);
27209
+ const style = end === 'start' ? line.arrows?.start?.style : line.arrows?.end?.style;
27210
+ const path = line.getArrowPath(style);
27211
+ const orient = end === 'start' ? 'auto-start-reverse' : 'auto';
27212
+ const refX = 0;
27213
+ const refY = size / 2;
27214
+ return `<marker id="${markerId}" markerWidth="${size}" markerHeight="${size}" refX="${refX}" refY="${refY}" orient="${orient}" markerUnits="userSpaceOnUse">
27215
+ <path d="${path}" fill="${fill}" transform="scale(${size / 10})"/>
27216
+ </marker>`;
27217
+ }
27218
+ /**
27219
+ * Converts a Kritzel object to its SVG element representation.
27220
+ * @param object - The object to convert
27221
+ * @param theme - Theme for color resolution
27222
+ * @param imageDataUrls - Optional map of image id -> inlined data URL
27223
+ * @returns SVG element string, or empty string if object type is not supported
27224
+ */
27225
+ static objectToSvgElement(object, theme, imageDataUrls, rasterSafeForCanvas = false) {
27226
+ if (schema_constants.KritzelClassHelper.isInstanceOf(object, 'KritzelPath')) {
27227
+ return this.pathToSvg(object, theme);
27228
+ }
27229
+ if (schema_constants.KritzelClassHelper.isInstanceOf(object, 'KritzelLine')) {
27230
+ return this.lineToSvg(object, theme);
27231
+ }
27232
+ if (schema_constants.KritzelClassHelper.isInstanceOf(object, 'KritzelShape')) {
27233
+ return this.shapeToSvg(object, theme);
27234
+ }
27235
+ if (schema_constants.KritzelClassHelper.isInstanceOf(object, 'KritzelText')) {
27236
+ return this.textToSvg(object, theme, rasterSafeForCanvas);
27237
+ }
27238
+ if (schema_constants.KritzelClassHelper.isInstanceOf(object, 'KritzelImage')) {
27239
+ return this.imageToSvg(object, imageDataUrls);
27240
+ }
27241
+ if (schema_constants.KritzelClassHelper.isInstanceOf(object, 'KritzelCustomElement')) {
27242
+ return this.customElementToSvg(object, rasterSafeForCanvas);
27243
+ }
27244
+ if (schema_constants.KritzelClassHelper.isInstanceOf(object, 'KritzelGroup')) {
27245
+ return this.groupToSvg(object, theme, imageDataUrls, rasterSafeForCanvas);
27246
+ }
27247
+ return '';
27248
+ }
27249
+ /**
27250
+ * Converts a KritzelPath to SVG.
27251
+ * @param path - The path object
27252
+ * @param theme - Theme for color resolution
27253
+ * @returns SVG element string
27254
+ */
27255
+ static pathToSvg(path, theme) {
27256
+ const transform = this.buildTransform(path);
27257
+ const fill = schema_constants.KritzelColorHelper.resolveThemeColor(path.fill, theme);
27258
+ const stroke = schema_constants.KritzelColorHelper.resolveThemeColor(path.stroke, theme);
27259
+ const opacity = path.opacity !== 1 ? ` opacity="${path.opacity}"` : '';
27260
+ const scale = this.getObjectScale(path);
27261
+ // Path needs to be wrapped in a g with translation since path.d uses local coordinates
27262
+ return `<g transform="${transform}"${opacity}>
27263
+ <svg viewBox="${path.viewBox}" width="${path.totalWidth / scale}" height="${path.totalHeight / scale}" overflow="visible">
27264
+ <path d="${path.d}" fill="${fill || 'none'}" stroke="${stroke || 'none'}"${path.strokeWidth ? ` stroke-width="${path.strokeWidth}"` : ''}/>
27265
+ </svg>
27266
+ </g>`;
27267
+ }
27268
+ /**
27269
+ * Converts a KritzelLine to SVG.
27270
+ * @param line - The line object
27271
+ * @param theme - Theme for color resolution
27272
+ * @returns SVG element string
27273
+ */
27274
+ static lineToSvg(line, theme) {
27275
+ const transform = this.buildTransform(line);
27276
+ const stroke = schema_constants.KritzelColorHelper.resolveThemeColor(line.stroke, theme);
27277
+ const opacity = line.opacity !== 1 ? ` opacity="${line.opacity}"` : '';
27278
+ const scale = this.getObjectScale(line);
27279
+ const markerStart = line.hasStartArrow ? ` marker-start="url(#${line.startMarkerId})"` : '';
27280
+ const markerEnd = line.hasEndArrow ? ` marker-end="url(#${line.endMarkerId})"` : '';
27281
+ return `<g transform="${transform}"${opacity}>
27282
+ <svg viewBox="${line.viewBox}" width="${line.totalWidth / scale}" height="${line.totalHeight / scale}" overflow="visible">
27283
+ <path d="${line.d}" fill="none" stroke="${stroke}" stroke-width="${line.strokeWidth}" stroke-linecap="round"${markerStart}${markerEnd}/>
27284
+ </svg>
27285
+ </g>`;
27286
+ }
27287
+ /**
27288
+ * Converts a KritzelShape to SVG.
27289
+ * @param shape - The shape object
27290
+ * @param theme - Theme for color resolution
27291
+ * @returns SVG element string
27292
+ */
27293
+ static shapeToSvg(shape, theme) {
27294
+ const transform = this.buildTransform(shape);
27295
+ const fill = schema_constants.KritzelColorHelper.resolveThemeColor(shape.fillColor, theme);
27296
+ const stroke = schema_constants.KritzelColorHelper.resolveThemeColor(shape.strokeColor, theme);
27297
+ const opacity = shape.opacity !== 1 ? ` opacity="${shape.opacity}"` : '';
27298
+ const scale = this.getObjectScale(shape);
27299
+ // Get the SVG path for the shape
27300
+ const pathD = shape.getSvgPath();
27301
+ // Handle text content if present
27302
+ let textContent = '';
27303
+ if (shape.editor && !shape.editor.state.doc.textContent.trim()) ;
27304
+ else if (shape.content) {
27305
+ const textHtml = this.prosemirrorToHtml(shape.content, shape, theme);
27306
+ if (textHtml) {
27307
+ const fontColor = schema_constants.KritzelColorHelper.resolveThemeColor(shape.fontColor, theme);
27308
+ textContent = `
27309
+ <foreignObject x="${shape.x}" y="${shape.y}" width="${shape.width}" height="${shape.height}">
27310
+ <div xmlns="http://www.w3.org/1999/xhtml" style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; text-align: center; font-family: ${shape.fontFamily}; font-size: ${shape.fontSize}px; color: ${fontColor}; overflow: hidden;">
27311
+ ${textHtml}
27312
+ </div>
27313
+ </foreignObject>`;
27314
+ }
27315
+ }
27316
+ return `<g transform="${transform}"${opacity}>
27317
+ <svg viewBox="${shape.viewBox}" width="${shape.totalWidth / scale}" height="${shape.totalHeight / scale}" overflow="visible" preserveAspectRatio="none">
27318
+ <path d="${pathD}" fill="${fill || 'transparent'}" stroke="${stroke}" stroke-width="${shape.strokeWidth}"/>${textContent}
27319
+ </svg>
27320
+ </g>`;
27321
+ }
27322
+ /**
27323
+ * Converts a KritzelText to SVG using foreignObject for rich text support.
27324
+ * @param text - The text object
27325
+ * @param theme - Theme for color resolution
27326
+ * @returns SVG element string
27327
+ */
27328
+ static textToSvg(text, theme, rasterSafeForCanvas = false) {
27329
+ if (rasterSafeForCanvas) {
27330
+ return this.textToSvgRasterSafe(text, theme);
27331
+ }
27332
+ const transform = this.buildTransform(text);
27333
+ const opacity = text.opacity !== 1 ? ` opacity="${text.opacity}"` : '';
27334
+ const fontColor = schema_constants.KritzelColorHelper.resolveThemeColor(text.fontColor, theme);
27335
+ const bgColor = schema_constants.KritzelColorHelper.resolveThemeColor(text.backgroundColor, theme);
27336
+ const scale = this.getObjectScale(text);
27337
+ // Convert ProseMirror content to HTML
27338
+ const htmlContent = this.prosemirrorToHtml(text.content, text, theme);
27339
+ // Calculate dimensions in SVG world units (world units = CSS px in a 1:1 SVG).
27340
+ const width = text.totalWidth / scale;
27341
+ const height = text.totalHeight / scale;
27342
+ // Convert the font size from pt to SVG world units (px equivalent).
27343
+ // text.fontSize is stored in CSS pt; 1 pt = 4/3 px at 96 dpi.
27344
+ // We also bake in scaleFactor (user resize) and divide by the object scale so
27345
+ // that the rendered font aligns with the world-unit dimensions above.
27346
+ // The CSS transform: scale(scaleFactor) is removed from the div because the
27347
+ // adjusted font-size already accounts for it – keeping both would double-scale
27348
+ // the content and overflow the foreignObject.
27349
+ const svgFontSizePx = (text.fontSize * (4 / 3) * (text.scaleFactor || 1) / scale).toFixed(4);
27350
+ return `<g transform="${transform}"${opacity}>
27351
+ <foreignObject x="0" y="0" width="${width}" height="${height}" overflow="visible">
27352
+ <div xmlns="http://www.w3.org/1999/xhtml" style="font-family: ${text.fontFamily}; font-size: ${svgFontSizePx}px; color: ${fontColor}; background-color: ${bgColor || 'transparent'}; white-space: pre-wrap; word-wrap: break-word; width: 100%; margin: 0; padding: 0; box-sizing: border-box;">${htmlContent.trim()}</div>
27353
+ </foreignObject>
27354
+ </g>`;
27355
+ }
27356
+ /**
27357
+ * Converts a KritzelText to SVG using pure SVG text primitives.
27358
+ * This path avoids `<foreignObject>`, which taints canvases when the
27359
+ * generated SVG is rasterized through `<img>` and `drawImage`.
27360
+ */
27361
+ static textToSvgRasterSafe(text, theme) {
27362
+ const transform = this.buildTransform(text);
27363
+ const opacity = text.opacity !== 1 ? ` opacity="${text.opacity}"` : '';
27364
+ const fontColor = schema_constants.KritzelColorHelper.resolveThemeColor(text.fontColor, theme);
27365
+ const bgColor = schema_constants.KritzelColorHelper.resolveThemeColor(text.backgroundColor, theme);
27366
+ const scale = this.getObjectScale(text);
27367
+ const width = text.totalWidth / scale;
27368
+ const height = text.totalHeight / scale;
27369
+ const lines = this.prosemirrorToPlainTextLines(text.content);
27370
+ const resolvedLines = lines.length > 0 ? lines : [''];
27371
+ // Convert the font size from pt to SVG world units (px equivalent).
27372
+ // text.fontSize is stored in CSS pt; 1 pt = 4/3 px at 96 dpi.
27373
+ // We also bake in scaleFactor (user resize) and divide by the object scale so
27374
+ // that the rendered lines align with the world-unit bounding box dimensions above.
27375
+ const resolvedFontSizePx = Math.max(1, text.fontSize * (4 / 3) * (text.scaleFactor || 1) / scale);
27376
+ const lineHeight = resolvedFontSizePx * 1.2;
27377
+ const startY = resolvedFontSizePx;
27378
+ const bgRect = bgColor && bgColor !== 'transparent'
27379
+ ? `<rect x="0" y="0" width="${width}" height="${height}" fill="${bgColor}"/>`
27380
+ : '';
27381
+ const tspans = resolvedLines
27382
+ .map((line, index) => {
27383
+ const y = startY + index * lineHeight;
27384
+ return `<tspan x="0" y="${y}" xml:space="preserve">${this.escapeHtml(line)}</tspan>`;
27385
+ })
27386
+ .join('');
27387
+ return `<g transform="${transform}"${opacity}>
27388
+ ${bgRect}
27389
+ <text font-family="${this.escapeHtml(text.fontFamily)}" font-size="${resolvedFontSizePx}" fill="${fontColor}">${tspans}</text>
27390
+ </g>`;
27456
27391
  }
27457
- canvas.style.width = `${canvasWidth}`;
27458
- canvas.style.height = `${canvasHeight}`;
27459
- if (options.backgroundColor) {
27460
- context.fillStyle = options.backgroundColor;
27461
- context.fillRect(0, 0, canvas.width, canvas.height);
27392
+ /**
27393
+ * Converts a KritzelImage to SVG.
27394
+ *
27395
+ * Prefers an inlined `data:` URL from `imageDataUrls` (keyed by object
27396
+ * id) when provided, so the embedded bytes survive rasterization to
27397
+ * PNG. Falls back to `resolvedSrc` (populated by the renderer via the
27398
+ * asset resolver) and finally the legacy inline `src` field for
27399
+ * documents persisted before the asset layer existed.
27400
+ *
27401
+ * Note: a `blob:` `resolvedSrc` renders correctly on-canvas but becomes
27402
+ * a broken-image placeholder once the SVG is loaded into an `<img>` and
27403
+ * drawn to a canvas. Callers that rasterize the SVG must pass
27404
+ * `imageDataUrls` (see {@link KritzelSvgExportHelper.resolveImageDataUrls}).
27405
+ * @param image - The image object
27406
+ * @param imageDataUrls - Optional map of image id -> inlined data URL
27407
+ * @returns SVG element string
27408
+ */
27409
+ static imageToSvg(image, imageDataUrls) {
27410
+ const transform = this.buildTransform(image);
27411
+ const opacity = image.opacity !== 1 ? ` opacity="${image.opacity}"` : '';
27412
+ const scale = this.getObjectScale(image);
27413
+ // Calculate dimensions
27414
+ const width = image.totalWidth / scale;
27415
+ const height = image.totalHeight / scale;
27416
+ const href = imageDataUrls?.get(image.id) || image.resolvedSrc || image.src || '';
27417
+ return `<g transform="${transform}"${opacity}>
27418
+ <image href="${href}" x="0" y="0" width="${width}" height="${height}" preserveAspectRatio="xMidYMid meet"/>
27419
+ </g>`;
27462
27420
  }
27463
- context.drawImage(img, 0, 0, canvas.width, canvas.height);
27464
- return canvas;
27465
- }
27466
- async function toPng(node, options = {}) {
27467
- const canvas = await toCanvas(node, options);
27468
- return canvas.toDataURL();
27469
- }
27470
-
27471
- class KritzelWheelHelper {
27472
- static shouldKeepWheelInCustomElement(ev, customElement) {
27473
- // Keep Ctrl+wheel available for viewport zoom gestures.
27474
- if (ev.ctrlKey || customElement.isSelected !== true) {
27475
- return false;
27476
- }
27477
- const path = typeof ev.composedPath === 'function' ? ev.composedPath() : [];
27478
- const scrollContainer = this.getScrollableContainerFromEventPath(path, customElement);
27479
- // No scroll container found inside the custom element: let the wheel
27480
- // event pass through so the viewport can continue handling it.
27481
- if (!scrollContainer) {
27482
- return false;
27421
+ /**
27422
+ * Converts a KritzelCustomElement to SVG.
27423
+ * SVG mode: embeds HTML in foreignObject (best-effort).
27424
+ * PNG/raster mode: renders placeholder rect with warning.
27425
+ * @param obj - The custom element object
27426
+ * @param rasterSafeForCanvas - If true, render placeholder (for PNG export)
27427
+ * @returns SVG element string
27428
+ */
27429
+ static customElementToSvg(obj, rasterSafeForCanvas = false) {
27430
+ const transform = this.buildTransform(obj);
27431
+ const opacity = obj.opacity !== 1 ? ` opacity="${obj.opacity}"` : '';
27432
+ const scale = this.getObjectScale(obj);
27433
+ const width = obj.totalWidth / scale;
27434
+ const height = obj.totalHeight / scale;
27435
+ if (rasterSafeForCanvas) {
27436
+ // PNG export: render placeholder rect (foreignObject doesn't rasterize to PNG well)
27437
+ console.warn('[KritzelCustomElement SVG Export] Custom elements do not serialize well to PNG. Use SVG export or screenshot instead.');
27438
+ return `<g transform="${transform}"${opacity}>
27439
+ <rect x="0" y="0" width="${width}" height="${height}" fill="#f0f0f0" stroke="#999" stroke-width="1" />
27440
+ <text x="${width / 2}" y="${height / 2}" text-anchor="middle" dominant-baseline="middle" font-size="12" fill="#999">[Custom Element]</text>
27441
+ </g>`;
27483
27442
  }
27484
- return this.canElementConsumeWheel(scrollContainer, ev.deltaX, ev.deltaY);
27443
+ // SVG export: use foreignObject to embed HTML
27444
+ const html = obj.element?.outerHTML || '<div style="border:1px dashed #ccc">Empty</div>';
27445
+ return `<g transform="${transform}"${opacity}>
27446
+ <foreignObject x="0" y="0" width="${width}" height="${height}">
27447
+ <div xmlns="http://www.w3.org/1999/xhtml" style="width:100%;height:100%;overflow:hidden;">
27448
+ ${html}
27449
+ </div>
27450
+ </foreignObject>
27451
+ </g>`;
27485
27452
  }
27486
- static getScrollableContainerFromEventPath(path, customElement) {
27487
- const potentialRoots = [];
27488
- if (customElement.element instanceof HTMLElement) {
27489
- potentialRoots.push(customElement.element);
27490
- }
27491
- if (customElement.elementRef instanceof HTMLElement) {
27492
- potentialRoots.push(customElement.elementRef);
27493
- }
27494
- const legacyElementRef = customElement.elmeentRef;
27495
- if (legacyElementRef instanceof HTMLElement) {
27496
- potentialRoots.push(legacyElementRef);
27497
- }
27498
- for (const root of potentialRoots) {
27499
- const rootIndex = path.findIndex(entry => entry === root);
27500
- if (rootIndex === -1) {
27501
- continue;
27502
- }
27503
- for (const entry of path.slice(0, rootIndex + 1)) {
27504
- if (!(entry instanceof HTMLElement)) {
27505
- continue;
27506
- }
27507
- if (this.isScrollableElement(entry)) {
27508
- return entry;
27509
- }
27510
- }
27511
- }
27512
- // Fallback for synthetic/custom wheel events where composedPath is absent
27513
- // or does not include the custom-element subtree.
27514
- for (const root of potentialRoots) {
27515
- if (this.isScrollableElement(root)) {
27516
- return root;
27517
- }
27518
- const firstChild = root.firstElementChild;
27519
- if (firstChild instanceof HTMLElement && this.isScrollableElement(firstChild)) {
27520
- return firstChild;
27521
- }
27453
+ /**
27454
+ * Converts a KritzelGroup to SVG by recursively converting children.
27455
+ * @param group - The group object
27456
+ * @param theme - Theme for color resolution
27457
+ * @param imageDataUrls - Optional map of image id -> inlined data URL
27458
+ * @returns SVG element string
27459
+ */
27460
+ static groupToSvg(group, theme, imageDataUrls, rasterSafeForCanvas = false) {
27461
+ const children = group.children
27462
+ .sort((a, b) => a.zIndex - b.zIndex)
27463
+ .map(child => this.objectToSvgElement(child, theme, imageDataUrls, rasterSafeForCanvas))
27464
+ .filter(Boolean)
27465
+ .join('\n ');
27466
+ if (!children) {
27467
+ return '';
27522
27468
  }
27523
- return null;
27469
+ const opacity = group.opacity !== 1 ? ` opacity="${group.opacity}"` : '';
27470
+ return `<g${opacity}>
27471
+ ${children}
27472
+ </g>`;
27524
27473
  }
27525
- static isScrollableElement(element) {
27526
- const canScrollVertically = element.scrollHeight - element.clientHeight > 0;
27527
- const canScrollHorizontally = element.scrollWidth - element.clientWidth > 0;
27528
- if (!canScrollVertically && !canScrollHorizontally) {
27529
- return false;
27474
+ /**
27475
+ * Builds a transform attribute string for an object.
27476
+ * Combines translation and rotation.
27477
+ * @param object - The object to build transform for
27478
+ * @returns Transform attribute value string
27479
+ */
27480
+ static buildTransform(object) {
27481
+ const transforms = [];
27482
+ const scale = this.getObjectScale(object);
27483
+ // Translation
27484
+ transforms.push(`translate(${object.translateX}, ${object.translateY})`);
27485
+ // Rotation around center
27486
+ if (object.rotation !== 0) {
27487
+ const centerX = object.totalWidth / 2 / scale;
27488
+ const centerY = object.totalHeight / 2 / scale;
27489
+ const degrees = object.rotation * (180 / Math.PI);
27490
+ transforms.push(`rotate(${degrees}, ${centerX}, ${centerY})`);
27530
27491
  }
27531
- const computedStyle = window.getComputedStyle(element);
27532
- const overflowX = computedStyle.overflowX;
27533
- const overflowY = computedStyle.overflowY;
27534
- const allowsVerticalScroll = overflowY !== 'visible' && overflowY !== 'clip';
27535
- const allowsHorizontalScroll = overflowX !== 'visible' && overflowX !== 'clip';
27536
- return (canScrollVertically && allowsVerticalScroll) || (canScrollHorizontally && allowsHorizontalScroll);
27492
+ return transforms.join(' ');
27537
27493
  }
27538
- static canElementConsumeWheel(element, deltaX, deltaY) {
27539
- const canConsumeVertical = this.canElementConsumeVerticalWheel(element, deltaY);
27540
- const canConsumeHorizontal = this.canElementConsumeHorizontalWheel(element, deltaX);
27541
- // Keep wheel events inside the custom element whenever at least one
27542
- // requested axis can still be consumed by its scroll container.
27543
- return canConsumeVertical || canConsumeHorizontal;
27494
+ /**
27495
+ * Returns a safe non-zero scale for export geometry calculations.
27496
+ */
27497
+ static getObjectScale(object) {
27498
+ const scale = object.scale ?? 1;
27499
+ return scale === 0 ? 1 : scale;
27544
27500
  }
27545
- static canElementConsumeVerticalWheel(element, deltaY) {
27546
- if (deltaY === 0) {
27547
- return false;
27501
+ /**
27502
+ * Converts ProseMirror JSON content to HTML string.
27503
+ * @param content - ProseMirror document JSON
27504
+ * @param context - The text or shape object for styling context
27505
+ * @param theme - Theme for color resolution
27506
+ * @returns HTML string
27507
+ */
27508
+ static prosemirrorToHtml(content, _context, theme) {
27509
+ if (!content || !content.content) {
27510
+ return '';
27548
27511
  }
27549
- const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
27550
- if (maxScrollTop === 0) {
27551
- return false;
27512
+ return content.content
27513
+ .map((node) => this.nodeToHtml(node, theme))
27514
+ .join('');
27515
+ }
27516
+ /**
27517
+ * Converts a ProseMirror node to HTML.
27518
+ * @param node - ProseMirror node JSON
27519
+ * @param theme - Theme for color resolution
27520
+ * @returns HTML string
27521
+ */
27522
+ static nodeToHtml(node, theme) {
27523
+ if (!node)
27524
+ return '';
27525
+ switch (node.type) {
27526
+ case 'paragraph':
27527
+ const pContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
27528
+ return `<p style="margin: 0;">${pContent || '<br/>'}</p>`;
27529
+ case 'text':
27530
+ let text = this.escapeHtml(node.text || '');
27531
+ if (node.marks) {
27532
+ for (const mark of node.marks) {
27533
+ text = this.applyMark(text, mark, theme);
27534
+ }
27535
+ }
27536
+ return text;
27537
+ case 'hard_break':
27538
+ return '<br/>';
27539
+ case 'bullet_list':
27540
+ const blContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
27541
+ return `<ul style="margin: 0; padding-left: 1.5em;">${blContent}</ul>`;
27542
+ case 'ordered_list':
27543
+ const olContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
27544
+ return `<ol style="margin: 0; padding-left: 1.5em;">${olContent}</ol>`;
27545
+ case 'list_item':
27546
+ const liContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
27547
+ return `<li>${liContent}</li>`;
27548
+ default:
27549
+ // For unknown node types, try to render content
27550
+ if (node.content) {
27551
+ return node.content.map((n) => this.nodeToHtml(n, theme)).join('');
27552
+ }
27553
+ return '';
27552
27554
  }
27553
- const epsilon = 1;
27554
- const scrollingDown = deltaY > 0;
27555
- const scrollingUp = deltaY < 0;
27556
- if (scrollingDown) {
27557
- return element.scrollTop < maxScrollTop - epsilon;
27555
+ }
27556
+ /**
27557
+ * Converts ProseMirror JSON content to plain-text lines.
27558
+ * Used by raster-safe SVG export, which cannot depend on HTML/foreignObject.
27559
+ */
27560
+ static prosemirrorToPlainTextLines(content) {
27561
+ if (!content || !Array.isArray(content.content)) {
27562
+ return [];
27558
27563
  }
27559
- if (scrollingUp) {
27560
- return element.scrollTop > epsilon;
27564
+ const plainText = content.content
27565
+ .map((node) => this.nodeToPlainText(node))
27566
+ .join('')
27567
+ .replace(/\r\n/g, '\n')
27568
+ .replace(/\n+$/, '');
27569
+ if (!plainText) {
27570
+ return [];
27561
27571
  }
27562
- return false;
27572
+ return plainText.split('\n');
27563
27573
  }
27564
- static canElementConsumeHorizontalWheel(element, deltaX) {
27565
- if (deltaX === 0) {
27566
- return false;
27567
- }
27568
- const maxScrollLeft = Math.max(0, element.scrollWidth - element.clientWidth);
27569
- if (maxScrollLeft === 0) {
27570
- return false;
27574
+ /**
27575
+ * Converts a ProseMirror node tree into plain text.
27576
+ */
27577
+ static nodeToPlainText(node) {
27578
+ if (!node) {
27579
+ return '';
27571
27580
  }
27572
- const epsilon = 1;
27573
- const scrollingRight = deltaX > 0;
27574
- const scrollingLeft = deltaX < 0;
27575
- if (scrollingRight) {
27576
- return element.scrollLeft < maxScrollLeft - epsilon;
27581
+ switch (node.type) {
27582
+ case 'text':
27583
+ return node.text || '';
27584
+ case 'hard_break':
27585
+ return '\n';
27586
+ case 'paragraph': {
27587
+ const paragraph = Array.isArray(node.content)
27588
+ ? node.content.map((child) => this.nodeToPlainText(child)).join('')
27589
+ : '';
27590
+ return `${paragraph}\n`;
27591
+ }
27592
+ case 'list_item': {
27593
+ const item = Array.isArray(node.content)
27594
+ ? node.content.map((child) => this.nodeToPlainText(child)).join('')
27595
+ : '';
27596
+ return `- ${item.trim()}\n`;
27597
+ }
27598
+ default:
27599
+ return Array.isArray(node.content)
27600
+ ? node.content.map((child) => this.nodeToPlainText(child)).join('')
27601
+ : '';
27577
27602
  }
27578
- if (scrollingLeft) {
27579
- return element.scrollLeft > epsilon;
27603
+ }
27604
+ /**
27605
+ * Applies a ProseMirror mark to text.
27606
+ * @param text - The text to wrap
27607
+ * @param mark - The mark to apply
27608
+ * @param theme - Theme for color resolution
27609
+ * @returns Wrapped HTML string
27610
+ */
27611
+ static applyMark(text, mark, _theme) {
27612
+ switch (mark.type) {
27613
+ case 'bold':
27614
+ case 'strong':
27615
+ return `<strong>${text}</strong>`;
27616
+ case 'italic':
27617
+ case 'em':
27618
+ return `<em>${text}</em>`;
27619
+ case 'underline':
27620
+ return `<u>${text}</u>`;
27621
+ case 'strike':
27622
+ return `<s>${text}</s>`;
27623
+ case 'code':
27624
+ return `<code>${text}</code>`;
27625
+ case 'link':
27626
+ const href = mark.attrs?.href || '#';
27627
+ return `<a href="${this.escapeHtml(href)}">${text}</a>`;
27628
+ default:
27629
+ return text;
27580
27630
  }
27581
- return false;
27631
+ }
27632
+ /**
27633
+ * Escapes HTML special characters.
27634
+ * @param str - String to escape
27635
+ * @returns Escaped string
27636
+ */
27637
+ static escapeHtml(str) {
27638
+ return str
27639
+ .replace(/&/g, '&amp;')
27640
+ .replace(/</g, '&lt;')
27641
+ .replace(/>/g, '&gt;')
27642
+ .replace(/"/g, '&quot;')
27643
+ .replace(/'/g, '&#039;');
27582
27644
  }
27583
27645
  }
27584
27646
 
@@ -28775,10 +28837,7 @@ const KritzelEngine = class {
28775
28837
  console.error('Failed to export viewport as PNG: screenshot could not be generated');
28776
28838
  return;
28777
28839
  }
28778
- const link = document.createElement('a');
28779
- link.download = filename;
28780
- link.href = dataUrl;
28781
- link.click();
28840
+ this.triggerDataUrlDownload(dataUrl, filename);
28782
28841
  }
28783
28842
  catch (error) {
28784
28843
  console.error('Failed to export viewport as PNG:', error);
@@ -28800,129 +28859,57 @@ const KritzelEngine = class {
28800
28859
  console.error('Failed to export viewport as SVG: screenshot could not be generated');
28801
28860
  return;
28802
28861
  }
28803
- const link = document.createElement('a');
28804
- link.download = filename;
28805
- link.href = dataUrl;
28806
- link.click();
28862
+ this.triggerDataUrlDownload(dataUrl, filename);
28807
28863
  }
28808
28864
  catch (error) {
28809
28865
  console.error('Failed to export viewport as SVG:', error);
28810
28866
  }
28811
28867
  }
28812
28868
  /**
28813
- * Generates an SVG string from the currently selected objects.
28814
- * Creates clean, standards-compliant SVG markup by directly serializing object properties.
28815
- * @param options - Optional export settings (theme, padding, raster-safe mode for canvas export).
28816
- * @returns SVG markup string, or null if no objects are selected.
28869
+ * Returns export formats supported by the currently selected object.
28870
+ * Only returns formats when exactly one object is selected.
28817
28871
  */
28818
- async getSelectedObjectsAsSvgString(options) {
28819
- const selectedObjects = await this.getSelectedObjects();
28820
- if (!selectedObjects || selectedObjects.length === 0) {
28821
- return null;
28872
+ async getSelectedObjectSupportedExportFormats() {
28873
+ const selected = this.getSingleSelectedExportableObject();
28874
+ if (!selected) {
28875
+ return [];
28822
28876
  }
28823
- const theme = options?.theme ?? this.core.themeManager.getStoredTheme();
28824
- // Inline image bytes as data URLs so they survive both standalone SVG
28825
- // files and rasterization to PNG (a `blob:` href is not loaded when an
28826
- // SVG is drawn into a canvas, which otherwise yields a broken image).
28827
- const imageDataUrls = await KritzelSvgExportHelper.resolveImageDataUrls(selectedObjects, this.core.assetResolver);
28828
- return KritzelSvgExportHelper.generateSvg(selectedObjects, {
28829
- theme,
28830
- padding: options?.padding ?? 0,
28831
- includeXmlDeclaration: true,
28832
- rasterSafeForCanvas: options?.rasterSafeForCanvas ?? false,
28833
- imageDataUrls,
28834
- });
28877
+ return selected.getSupportedExportFormats();
28835
28878
  }
28836
28879
  /**
28837
- * Exports the currently selected objects as an SVG file and triggers a browser download.
28838
- * Creates clean, standards-compliant SVG markup suitable for import into vector editors.
28839
- * @param options - Optional export settings (theme, padding).
28880
+ * Checks whether the currently selected object supports a specific export format.
28881
+ * Returns false when none or multiple objects are selected.
28840
28882
  */
28841
- async exportSelectedObjectsAsSvg(options) {
28842
- try {
28843
- const svgString = await this.getSelectedObjectsAsSvgString(options);
28844
- if (!svgString) {
28845
- console.warn('No objects selected for SVG export');
28846
- return;
28847
- }
28848
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
28849
- const filename = `selection-${timestamp}.svg`;
28850
- const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
28851
- const url = URL.createObjectURL(blob);
28852
- const link = document.createElement('a');
28853
- link.download = filename;
28854
- link.href = url;
28855
- link.click();
28856
- URL.revokeObjectURL(url);
28857
- }
28858
- catch (error) {
28859
- console.error('Failed to export selected objects as SVG:', error);
28883
+ async canExportSelectedObjectAs(format) {
28884
+ const selected = this.getSingleSelectedExportableObject();
28885
+ if (!selected) {
28886
+ return false;
28860
28887
  }
28888
+ return selected.getSupportedExportFormats().includes(format);
28861
28889
  }
28862
28890
  /**
28863
- * Generates a PNG data URL from the currently selected objects.
28864
- * Converts the SVG representation to a rasterized PNG image.
28865
- * @param options - Optional export settings (theme, padding, scale for resolution).
28866
- * @returns PNG data URL string, or null if no objects are selected.
28891
+ * Exports the currently selected object in the requested format and triggers a file download.
28892
+ * Only supports single selection.
28867
28893
  */
28868
- async getSelectedObjectsAsPngDataUrl(options) {
28869
- const svgString = await this.getSelectedObjectsAsSvgString({
28870
- theme: options?.theme,
28871
- padding: options?.padding,
28872
- // Avoid foreignObject-based text serialization, which taints the
28873
- // canvas in SVG->PNG conversion paths.
28874
- rasterSafeForCanvas: true,
28875
- });
28876
- if (!svgString) {
28877
- return null;
28894
+ async exportSelectedObject(format) {
28895
+ const selected = this.getSingleSelectedExportableObject();
28896
+ if (!selected) {
28897
+ return;
28878
28898
  }
28879
- const scale = options?.scale ?? 2; // Default 2x for better quality
28880
- return new Promise((resolve, reject) => {
28881
- const img = new Image();
28882
- const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
28883
- const url = URL.createObjectURL(svgBlob);
28884
- img.onload = () => {
28885
- const canvas = document.createElement('canvas');
28886
- canvas.width = img.width * scale;
28887
- canvas.height = img.height * scale;
28888
- const ctx = canvas.getContext('2d');
28889
- if (!ctx) {
28890
- URL.revokeObjectURL(url);
28891
- reject(new Error('Failed to get canvas context'));
28892
- return;
28893
- }
28894
- ctx.scale(scale, scale);
28895
- ctx.drawImage(img, 0, 0);
28896
- URL.revokeObjectURL(url);
28897
- resolve(canvas.toDataURL('image/png'));
28898
- };
28899
- img.onerror = () => {
28900
- URL.revokeObjectURL(url);
28901
- reject(new Error('Failed to load SVG for PNG conversion'));
28902
- };
28903
- img.src = url;
28904
- });
28905
- }
28906
- /**
28907
- * Exports the currently selected objects as a PNG file and triggers a browser download.
28908
- * @param options - Optional export settings (theme, padding, scale for resolution).
28909
- */
28910
- async exportSelectedObjectsAsPng(options) {
28911
28899
  try {
28912
- const dataUrl = await this.getSelectedObjectsAsPngDataUrl(options);
28900
+ const dataUrl = await selected.exportAs(format, this.getObjectExportContext());
28913
28901
  if (!dataUrl) {
28914
- console.warn('No objects selected for PNG export');
28902
+ console.error(`Failed to export selected object as ${format}: export returned no data URL`);
28915
28903
  return;
28916
28904
  }
28905
+ const activeWorkspaceName = this.core.store.state?.activeWorkspace?.name || 'workspace';
28906
+ const objectName = selected.__class__.replace(/^Kritzel/, '').toLowerCase() || 'object';
28917
28907
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
28918
- const filename = `selection-${timestamp}.png`;
28919
- const link = document.createElement('a');
28920
- link.download = filename;
28921
- link.href = dataUrl;
28922
- link.click();
28908
+ const filename = `${activeWorkspaceName}-${objectName}-${timestamp}.${format}`;
28909
+ this.triggerDataUrlDownload(dataUrl, filename);
28923
28910
  }
28924
28911
  catch (error) {
28925
- console.error('Failed to export selected objects as PNG:', error);
28912
+ console.error(`Failed to export selected object as ${format}:`, error);
28926
28913
  }
28927
28914
  }
28928
28915
  /**
@@ -29117,6 +29104,73 @@ const KritzelEngine = class {
29117
29104
  const response = await fetch(dataUrl);
29118
29105
  return response.blob();
29119
29106
  }
29107
+ triggerDataUrlDownload(dataUrl, filename) {
29108
+ const link = document.createElement('a');
29109
+ link.download = filename;
29110
+ link.href = dataUrl;
29111
+ link.click();
29112
+ }
29113
+ getSingleSelectedExportableObject() {
29114
+ const selectedObjects = this.core.store.selectionGroup?.objects ?? [];
29115
+ if (selectedObjects.length !== 1) {
29116
+ return null;
29117
+ }
29118
+ const [selected] = selectedObjects;
29119
+ if (!schema_constants.ObjectHelper.isKritzelExportable(selected)) {
29120
+ return null;
29121
+ }
29122
+ return selected;
29123
+ }
29124
+ getObjectExportContext() {
29125
+ return {
29126
+ getObjectAsSvgDataUrl: async (object, options) => {
29127
+ const svg = await this.getObjectAsSvgMarkup(object, options?.rasterSafeForCanvas ?? false);
29128
+ return svg ? this.svgMarkupToDataUrl(svg) : null;
29129
+ },
29130
+ getObjectAsPngDataUrl: async (object) => {
29131
+ const svg = await this.getObjectAsSvgMarkup(object, true);
29132
+ return svg ? this.svgMarkupToPngDataUrl(svg) : null;
29133
+ },
29134
+ };
29135
+ }
29136
+ async getObjectAsSvgMarkup(object, rasterSafeForCanvas) {
29137
+ const imageDataUrls = await KritzelSvgExportHelper.resolveImageDataUrls([object], this.core.assetResolver);
29138
+ const svg = KritzelSvgExportHelper.generateSvg([object], {
29139
+ theme: this.theme,
29140
+ rasterSafeForCanvas,
29141
+ imageDataUrls,
29142
+ });
29143
+ return svg || null;
29144
+ }
29145
+ svgMarkupToDataUrl(svgMarkup) {
29146
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgMarkup)}`;
29147
+ }
29148
+ async svgMarkupToPngDataUrl(svgMarkup) {
29149
+ const svgBlob = new Blob([svgMarkup], { type: 'image/svg+xml;charset=utf-8' });
29150
+ const svgBlobUrl = URL.createObjectURL(svgBlob);
29151
+ try {
29152
+ const img = await new Promise((resolve, reject) => {
29153
+ const element = new Image();
29154
+ element.onload = () => resolve(element);
29155
+ element.onerror = err => reject(err);
29156
+ element.src = svgBlobUrl;
29157
+ });
29158
+ const width = Math.max(1, Math.ceil(img.naturalWidth || img.width));
29159
+ const height = Math.max(1, Math.ceil(img.naturalHeight || img.height));
29160
+ const canvas = document.createElement('canvas');
29161
+ canvas.width = width;
29162
+ canvas.height = height;
29163
+ const context = canvas.getContext('2d');
29164
+ if (!context) {
29165
+ throw new Error('Could not create 2D canvas context for PNG export');
29166
+ }
29167
+ context.drawImage(img, 0, 0, width, height);
29168
+ return canvas.toDataURL('image/png');
29169
+ }
29170
+ finally {
29171
+ URL.revokeObjectURL(svgBlobUrl);
29172
+ }
29173
+ }
29120
29174
  getImportedWorkspaceName(rawName) {
29121
29175
  if (typeof rawName === 'string' && rawName.trim().length > 0) {
29122
29176
  return rawName;
@@ -29993,7 +30047,7 @@ const KritzelEngine = class {
29993
30047
  this.emitObjectsInViewportChange();
29994
30048
  }
29995
30049
  }
29996
- return (index.h(index.Host, null, index.h("kritzel-loading-overlay", { visible: this.core.store.state.isLoading, text: this.core.localizationManager.translate('engine.loading') }), index.h("kritzel-sync-indicator", { visible: this.isSyncing, text: "Syncing..." }), this.core.store.state.debugInfo.showViewportInfo && (index.h("div", { class: "debug-panel", style: {
30050
+ return (index.h(index.Host, null, this.core.store.state.debugInfo.showViewportInfo && (index.h("div", { class: "debug-panel", style: {
29997
30051
  color: 'var(--kritzel-global-text-primary)',
29998
30052
  } }, index.h("div", null, "ActiveWorkspaceId: ", this.core.store.state?.activeWorkspace?.id), index.h("div", null, "ActiveWorkspaceName: ", this.core.store.state?.activeWorkspace?.name), index.h("div", null, "TranslateX: ", this.core.store.state?.translateX), index.h("div", null, "TranslateY: ", this.core.store.state?.translateY), index.h("div", null, "ViewportWidth: ", this.core.store.state?.viewportWidth), index.h("div", null, "ViewportHeight: ", this.core.store.state?.viewportHeight), index.h("div", null, "PointerCount: ", this.core.store.state.pointers.size), index.h("div", null, "Scale: ", this.core.store.state?.scale), index.h("div", null, "ActiveTool: ", this.core.store.state?.activeTool?.name), index.h("div", null, "HasViewportChanged: ", this.core.store.state?.hasViewportChanged ? 'true' : 'false'), index.h("div", null, "IsEnabled: ", this.core.store.state?.isEnabled ? 'true' : 'false'), index.h("div", null, "IsScaling: ", this.core.store.state?.isScaling ? 'true' : 'false'), index.h("div", null, "IsPanning: ", this.core.store.state?.isPanning ? 'true' : 'false'), index.h("div", null, "IsSelecting: ", this.isSelecting ? 'true' : 'false'), index.h("div", null, "IsSelectionActive: ", this.isSelectionActive ? 'true' : 'false'), index.h("div", null, "IsResizeHandleSelected: ", this.core.store.state.isResizeHandleSelected ? 'true' : 'false'), index.h("div", null, "IsRotationHandleSelected: ", this.core.store.state.isRotationHandleSelected ? 'true' : 'false'), index.h("div", null, "IsRotationHandleHovered: ", this.core.store.state.isRotationHandleHovered ? 'true' : 'false'), index.h("div", null, "IsDrawing: ", this.core.store.state.isDrawing ? 'true' : 'false'), index.h("div", null, "IsWriting: ", this.core.store.state.isWriting ? 'true' : 'false'), index.h("div", null, "IsPointerDown: ", this.core.store.isPointerDown ? 'true' : 'false'), index.h("div", null, "PointerX: ", this.core.store.state?.pointerX), index.h("div", null, "PointerY: ", this.core.store.state?.pointerY), index.h("div", null, "TotalObjects: ", this.core.store.totalObjectCount), index.h("div", null, "ObjectsInViewport: ", this.core.store.objectsInViewport.length), index.h("div", null, "SelectedObjects: ", this.core.store.selectionGroup?.objects.length || 0), index.h("div", null, "ViewportCenter: (", viewportCenterX.toFixed(2), ", ", viewportCenterY.toFixed(2), ")"))), index.h("div", { id: "origin", class: "origin", style: {
29999
30053
  transform: `matrix(${this.core.store.state?.scale}, 0, 0, ${this.core.store.state?.scale}, ${this.core.store.state?.translateX}, ${this.core.store.state?.translateY})`,
@@ -30588,7 +30642,7 @@ const KritzelExport = class {
30588
30642
  return (index.h("div", { class: "export-tab-content" }, index.h("kritzel-input", { label: this.terms['export.filename.label'] ?? 'Filename', value: this.exportFilename, placeholder: this.terms['export.filename.placeholder'] ?? 'Enter filename', suffix: ".json", onValueChange: this.handleFilenameChange })));
30589
30643
  }
30590
30644
  render() {
30591
- return (index.h(index.Host, { key: '0bc45e80716be4a7b19ae12a6464a641b5bd4550' }, index.h("kritzel-dialog", { key: 'f336f8b1e4ae10954d48ca67da88504faf06b295', isOpen: this.isDialogOpen, dialogTitle: this.terms['export.dialogTitle'] ?? 'Export', closable: true, contained: true, onDialogClose: this.closeDialog }, index.h("div", { key: 'e2d1209652bb54fb7f623e50bc4bbbb703083b76', class: "export-content" }, index.h("kritzel-pill-tabs", { key: 'dd78430b78d1e8c4ac0a76e8a6a5d48bebf5e889', tabs: this.tabs, value: this.activeTab, onValueChange: this.handleTabChange }), this.activeTab === 'viewport' && this.renderViewportExport(), this.activeTab === 'workspace' && this.renderWorkspaceExport(), index.h("button", { key: 'e06564889d991afc7a4ac43bb1e5504e98c94c52', class: "export-primary-button", onClick: this.handleExport }, this.terms['export.exportButton'] ?? 'Export')))));
30645
+ return (index.h(index.Host, { key: '64a9957ef87799215f9274213ead958a895dd8ef' }, index.h("kritzel-dialog", { key: '59ad70cc7d2834f5ecac3a659189390015cd26cb', isOpen: this.isDialogOpen, dialogTitle: this.terms['export.dialogTitle'] ?? 'Export', closable: true, contained: true, onDialogClose: this.closeDialog }, index.h("div", { key: 'fc9247f5b3848def87e6f6a6044d3fb1e63594ac', class: "export-content" }, index.h("kritzel-pill-tabs", { key: '54da74bbbd206b5e596865081cc5fe65bf8a776f', tabs: this.tabs, value: this.activeTab, onValueChange: this.handleTabChange }), this.activeTab === 'viewport' && this.renderViewportExport(), this.activeTab === 'workspace' && this.renderWorkspaceExport()), index.h("div", { key: 'b8e748034496936bf000a8eb111f52d58bf28d20', slot: "footer" }, index.h("button", { key: 'b7a59f3882146b45a0eb8395ea1c45cb41af0f34', class: "export-primary-button", onClick: this.handleExport }, this.terms['export.exportButton'] ?? 'Export')))));
30592
30646
  }
30593
30647
  };
30594
30648
  KritzelExport.style = kritzelExportCss();
@@ -30603,7 +30657,7 @@ const KritzelFont = class {
30603
30657
  size = 24;
30604
30658
  color = '#000000';
30605
30659
  render() {
30606
- return (index.h(index.Host, { key: '054371cac30a2433611139a670406b0a1e3b99ff' }, index.h("div", { key: 'b4f2142b3cb31d067f39952a38b6f665f28f63bf', class: "font-preview", style: {
30660
+ return (index.h(index.Host, { key: '42f058426e2afcc49c788f2385e23a5ff153e77c' }, index.h("div", { key: '962dfc48ba84c5f5d2b98eb2464c69c04764ab0f', class: "font-preview", style: {
30607
30661
  fontFamily: this.fontFamily,
30608
30662
  fontSize: `${this.size}px`,
30609
30663
  color: this.color
@@ -30687,7 +30741,7 @@ const KritzelFontSize = class {
30687
30741
  render() {
30688
30742
  const color = 'var(--kritzel-global-text-primary)';
30689
30743
  const sizes = this.sizes ?? [];
30690
- return (index.h(index.Host, { key: '56eb19367abe312bac3b626cfcae3b1ff6e90d21' }, sizes.map(size => (index.h("div", { tabIndex: 0, class: {
30744
+ return (index.h(index.Host, { key: '9d6592f049c25e9dfa2f6f3a270feb2440aa7f35' }, sizes.map(size => (index.h("div", { tabIndex: 0, class: {
30691
30745
  'size-container': true,
30692
30746
  'selected': this.selectedSize === size,
30693
30747
  }, onClick: () => this.handleSizeClick(size), onKeyDown: event => this.handleKeyDown(event, size) }, index.h("kritzel-font", { fontFamily: this.fontFamily, size: size, color: color }))))));
@@ -30754,7 +30808,7 @@ const KritzelInput = class {
30754
30808
  this.valueChange.emit(input.value);
30755
30809
  };
30756
30810
  render() {
30757
- return (index.h(index.Host, { key: '46a0a48902a06ad85edcbaad3679ce3fe9ba850c' }, index.h("div", { key: 'a6beaf920614aa959d2fbd68b7268afba8637358', class: "input-container" }, this.label && index.h("label", { key: 'ca6d3fb3c22f1540795dbb0695ddd9b87cc05eee', class: "input-label" }, this.label), index.h("div", { key: '78d116e82531a9cca996d594792245449bcaf9c8', class: { 'input-wrapper': true, 'has-suffix': !!this.suffix } }, index.h("input", { key: '0d10d6356dea3a11338172bbe8c3c878eefcc708', type: this.type, class: "text-input", value: this.inputValue, placeholder: this.placeholder, disabled: this.disabled, onInput: this.handleInput }), this.suffix && index.h("span", { key: '0e6166ba8728efc1035302d7fb048306efbe5a68', class: "input-suffix" }, this.suffix)))));
30811
+ return (index.h(index.Host, { key: 'a4d86101322da32c87a62ac8bb05edb7015ede2a' }, index.h("div", { key: 'abc5b007af0f6f9353e0b680636c92813ee1ef49', class: "input-container" }, this.label && index.h("label", { key: 'f79e02a925aa6bd1b6cd48fc4225e441695ff893', class: "input-label" }, this.label), index.h("div", { key: 'e2937d2ad1def996e1937a9dd711578a3f9b6494', class: { 'input-wrapper': true, 'has-suffix': !!this.suffix } }, index.h("input", { key: '8bc45cc6f2603bec30f2299286d03beacd8c07fb', type: this.type, class: "text-input", value: this.inputValue, placeholder: this.placeholder, disabled: this.disabled, onInput: this.handleInput }), this.suffix && index.h("span", { key: '917fb6fa6c5fe62ab0f1e5c83f09aedd22623353', class: "input-suffix" }, this.suffix)))));
30758
30812
  }
30759
30813
  static get watchers() { return {
30760
30814
  "value": [{
@@ -30847,7 +30901,7 @@ const KritzelLineEndings = class {
30847
30901
  };
30848
30902
  KritzelLineEndings.style = kritzelLineEndingsCss();
30849
30903
 
30850
- const kritzelLoadingOverlayCss = () => `:host{display:contents}.loading-overlay{position:absolute;inset:0;z-index:var(--kritzel-loading-overlay-z-index, 9999);display:flex;align-items:center;justify-content:center;gap:10px;background-color:var(--kritzel-loading-overlay-background, rgba(255, 255, 255, 0.6));color:var(--kritzel-loading-overlay-color, #333);font-family:var(--kritzel-font-family, sans-serif);font-size:1.25rem;opacity:0;pointer-events:none;visibility:hidden}.loading-overlay.visible{pointer-events:all;visibility:visible;animation:kritzel-loading-fade-in 200ms ease-out var(--kritzel-loading-overlay-delay, 300ms) forwards}.loading-spinner{width:20px;height:20px;box-sizing:border-box;display:block;flex-shrink:0;border:2px solid var(--kritzel-loading-overlay-spinner-color, #cccccc);border-top-color:var(--kritzel-loading-overlay-spinner-active-color, #333333);border-radius:50%;animation:kritzel-loading-spin 0.6s linear infinite}@keyframes kritzel-loading-spin{to{transform:rotate(360deg)}}@keyframes kritzel-loading-fade-in{to{opacity:1}}`;
30904
+ const kritzelLoadingOverlayCss = () => `:host{display:contents}.loading-overlay{position:absolute;inset:0;z-index:var(--kritzel-editor-loading-overlay-z-index, 9999);display:flex;align-items:center;justify-content:center;gap:10px;background-color:var(--kritzel-editor-loading-overlay-background, rgba(255, 255, 255, 0.6));color:var(--kritzel-editor-loading-overlay-color, #333);font-family:var(--kritzel-font-family, sans-serif);font-size:1.25rem;opacity:0;pointer-events:none;visibility:hidden}.loading-overlay.visible{pointer-events:all;visibility:visible;animation:kritzel-loading-fade-in 200ms ease-out var(--kritzel-editor-loading-overlay-delay, 300ms) forwards}.loading-spinner{width:20px;height:20px;box-sizing:border-box;display:block;flex-shrink:0;border:2px solid var(--kritzel-editor-loading-overlay-spinner-color, #cccccc);border-top-color:var(--kritzel-editor-loading-overlay-spinner-active-color, #333333);border-radius:50%;animation:kritzel-loading-spin 0.6s linear infinite}@keyframes kritzel-loading-spin{to{transform:rotate(360deg)}}@keyframes kritzel-loading-fade-in{to{opacity:1}}`;
30851
30905
 
30852
30906
  const KritzelLoadingOverlay = class {
30853
30907
  constructor(hostRef) {
@@ -30862,7 +30916,7 @@ const KritzelLoadingOverlay = class {
30862
30916
  */
30863
30917
  text = 'Loading...';
30864
30918
  render() {
30865
- return (index.h(index.Host, { key: '331e4c9eb30e6f7b98023c23218b3caf5093c34b' }, index.h("div", { key: 'f994fec0db325a2bd687e5acc68263752e7a6acd', class: { 'loading-overlay': true, visible: this.visible }, "aria-hidden": this.visible ? 'false' : 'true' }, index.h("span", { key: 'b6087dc1e59845ab95946e654c7bb8e568b42dda', class: "loading-spinner" }), this.text)));
30919
+ return (index.h(index.Host, { key: '6fc1886217516966fa3599473989748151d93efd' }, index.h("div", { key: 'c6f2aa1c7f81bbab65411292934d185b4bf69e5a', class: { 'loading-overlay': true, visible: this.visible }, "aria-hidden": this.visible ? 'false' : 'true' }, index.h("span", { key: '21bae0b6f9b9a08f814aec7e6635a904018ab839', class: "loading-spinner" }), this.text)));
30866
30920
  }
30867
30921
  };
30868
30922
  KritzelLoadingOverlay.style = kritzelLoadingOverlayCss();
@@ -30914,7 +30968,7 @@ const KritzelLoginDialog = class {
30914
30968
  this.dialogClosed.emit();
30915
30969
  };
30916
30970
  render() {
30917
- return (index.h(index.Host, { key: 'bcd7f30bbfeb07bbca732c5f1034bcabbd5e4f80' }, index.h("kritzel-dialog", { key: '61b5c5054d43433894747f37c571e68ecf08fc4c', dialogTitle: this.dialogTitle, isOpen: this.isDialogOpen, onDialogClose: this.closeDialog, size: "small", contained: true }, index.h("div", { key: '3ddcb858b6148e8be4bd121b2bd8ca38c66e0505', class: "login-content" }, this.subtitle && (index.h("p", { key: '6f10f890af9f6025a3aa2d69f8f4d916821fd653', class: "login-subtitle" }, this.subtitle)), index.h("div", { key: '0d043ac1ac751dc3d0a561bc16e52a2cfaffc578', class: "login-providers" }, this.providers.map(provider => (index.h("button", { key: provider.name, class: {
30971
+ return (index.h(index.Host, { key: '97532a2eeb125a2692117228ec3c77907c90d021' }, index.h("kritzel-dialog", { key: '60597c3d75f532a3c73aab983bed683bea243eac', dialogTitle: this.dialogTitle, isOpen: this.isDialogOpen, onDialogClose: this.closeDialog, size: "small", contained: true }, index.h("div", { key: 'cb086e90c5981f8c3d5fc65e7b94675884a255dc', class: "login-content" }, this.subtitle && (index.h("p", { key: '0ac0e31a5bf8d8a3e24b7df1d388f1cdc6951bd7', class: "login-subtitle" }, this.subtitle)), index.h("div", { key: '6057615670a1758581d0c518bb2809b88af0672e', class: "login-providers" }, this.providers.map(provider => (index.h("button", { key: provider.name, class: {
30918
30972
  'provider-button': true,
30919
30973
  'is-loading': this.loadingProvider === provider.name,
30920
30974
  'is-disabled': this.loadingProvider !== null && this.loadingProvider !== provider.name,
@@ -31020,15 +31074,15 @@ const KritzelMasterDetail = class {
31020
31074
  const selectedItem = this.items.find(item => item.id === this.selectedItemId);
31021
31075
  const panelId = 'master-detail-panel';
31022
31076
  const selectedTabId = selectedItem ? `tab-${selectedItem.id}` : undefined;
31023
- return (index.h(index.Host, { key: '0edf50b0b853633b88bf481f97f22aceba9e163e' }, index.h("div", { key: '21a0a3f81e5a60889ccd152c905cb41f52c3e037', class: {
31077
+ return (index.h(index.Host, { key: '0166376f16ebb098049cd2ef5539a86086a5570e' }, index.h("div", { key: 'f5091fe672bc4751c8f66d43baa5c1fc86fca034', class: {
31024
31078
  'master-detail-container': true,
31025
31079
  'is-mobile-detail-visible': this.showMobileDetail,
31026
- } }, index.h("nav", { key: '41f88248cc372c8cae4cb50a19edf462a01e43f2', class: "master-menu", role: "tablist", "aria-orientation": "vertical", "aria-label": "Settings categories" }, this.items.map((item, index$1) => (index.h("button", { key: item.id, id: `tab-${item.id}`, ref: el => this.setTabRef(el, index$1), class: {
31080
+ } }, index.h("nav", { key: '66f5c61ea0308ea9ae1339526e86dcf1ee51fc0a', class: "master-menu", role: "tablist", "aria-orientation": "vertical", "aria-label": "Settings categories" }, this.items.map((item, index$1) => (index.h("button", { key: item.id, id: `tab-${item.id}`, ref: el => this.setTabRef(el, index$1), class: {
31027
31081
  'menu-item': true,
31028
31082
  'is-selected': item.id === this.selectedItemId,
31029
31083
  'is-disabled': !!item.disabled,
31030
31084
  'is-focused': index$1 === this.focusedIndex,
31031
- }, role: "tab", "aria-selected": item.id === this.selectedItemId ? 'true' : 'false', "aria-controls": panelId, "aria-disabled": item.disabled ? 'true' : undefined, tabIndex: this.getTabIndex(item, index$1), disabled: item.disabled, onClick: () => this.handleItemClick(item), onKeyDown: e => this.handleKeyDown(e, item, index$1), onFocus: () => this.handleFocus(index$1), onBlur: this.handleBlur }, item.icon && (index.h("kritzel-icon", { name: item.icon, size: 20, class: "menu-item-icon" })), index.h("span", { class: "menu-item-label" }, item.label), index.h("span", { class: "menu-item-chevron", "aria-hidden": "true" }, index.h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }, index.h("path", { d: "m9 18 6-6-6-6" }))))))), index.h("div", { key: 'f8447facf3dfdbd7772823654847e1b90b03f73e', id: panelId, class: "detail-panel", role: "tabpanel", "aria-labelledby": selectedTabId }, index.h("button", { key: '3edc9784267613e6b3a706739bbb537161e4c747', class: "mobile-back-button", onClick: this.handleBackClick, "aria-label": "Back to menu" }, index.h("kritzel-icon", { key: 'ec0b4691735578525e5863842271366e04b13fb7', name: "chevron-left", size: 20, class: "mobile-back-icon" }), "Back"), index.h("slot", { key: 'c21fbaff78f98859b9756427125b60504d4bb630' })))));
31085
+ }, role: "tab", "aria-selected": item.id === this.selectedItemId ? 'true' : 'false', "aria-controls": panelId, "aria-disabled": item.disabled ? 'true' : undefined, tabIndex: this.getTabIndex(item, index$1), disabled: item.disabled, onClick: () => this.handleItemClick(item), onKeyDown: e => this.handleKeyDown(e, item, index$1), onFocus: () => this.handleFocus(index$1), onBlur: this.handleBlur }, item.icon && (index.h("kritzel-icon", { name: item.icon, size: 20, class: "menu-item-icon" })), index.h("span", { class: "menu-item-label" }, item.label), index.h("span", { class: "menu-item-chevron", "aria-hidden": "true" }, index.h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }, index.h("path", { d: "m9 18 6-6-6-6" }))))))), index.h("div", { key: 'e8f762ad87fdcf401ec717538bf1e979b22a9f15', id: panelId, class: "detail-panel", role: "tabpanel", "aria-labelledby": selectedTabId }, index.h("button", { key: '958360d57a1fad0953bc3865503e86c1d8aa24e3', class: "mobile-back-button", onClick: this.handleBackClick, "aria-label": "Back to menu" }, index.h("kritzel-icon", { key: 'c65eb947f80aafec99b367dae7ea72b1a77acdac', name: "chevron-left", size: 20, class: "mobile-back-icon" }), "Back"), index.h("slot", { key: '44557fddf513fffd86c7a1b28b71a41fef7701f1' })))));
31032
31086
  }
31033
31087
  static get watchers() { return {
31034
31088
  "selectedItemId": [{
@@ -31101,7 +31155,7 @@ const KritzelMenu = class {
31101
31155
  this.itemCloseChildMenu.emit(event.detail);
31102
31156
  };
31103
31157
  render() {
31104
- return (index.h(index.Host, { key: '27a90a3a8549517a2d6e341a5da13496bf714970', tabIndex: 0, onClick: e => e.stopPropagation() }, this.openChildMenuItem && index.h("div", { key: 'c0c2351fac389f6676b38481eaacee553ca6867a', class: "has-open-child-overlay", onClick: this.onOverlayClick }), this.items.map(item => (index.h("kritzel-menu-item", { key: item.id, "data-testid": `menu-item-${item.id}`, item: item, parent: this.parent, style: { pointerEvents: this.editingMenuItem && !item.isEditing ? 'none' : 'auto' }, onItemSelect: this.handleItemSelect, onItemSave: this.handleSave, onItemCancel: this.handleCancel, onItemToggleChildMenu: this.handleToggleChildMenu, onItemCloseChildMenu: this.handleCloseChildMenu })))));
31158
+ return (index.h(index.Host, { key: '804ed34eb5340ecc1611ada01f608f7a0cf2f619', tabIndex: 0, onClick: e => e.stopPropagation() }, this.openChildMenuItem && index.h("div", { key: '6de5954a05b7f28961fa33cf8d34df3a81aae5f8', class: "has-open-child-overlay", onClick: this.onOverlayClick }), this.items.map(item => (index.h("kritzel-menu-item", { key: item.id, "data-testid": `menu-item-${item.id}`, item: item, parent: this.parent, style: { pointerEvents: this.editingMenuItem && !item.isEditing ? 'none' : 'auto' }, onItemSelect: this.handleItemSelect, onItemSave: this.handleSave, onItemCancel: this.handleCancel, onItemToggleChildMenu: this.handleToggleChildMenu, onItemCloseChildMenu: this.handleCloseChildMenu })))));
31105
31159
  }
31106
31160
  };
31107
31161
  KritzelMenu.style = kritzelMenuCss();
@@ -31206,12 +31260,12 @@ const KritzelMenuItem = class {
31206
31260
  ];
31207
31261
  }
31208
31262
  render() {
31209
- return (index.h(index.Host, { key: '9d6653a630da5a3da38291d89d6d7d59d6994e02', tabIndex: this.item.isDisabled ? -1 : 0, class: {
31263
+ return (index.h(index.Host, { key: '9b35e5631cfa14aca391636f06598ef6f2f26550', tabIndex: this.item.isDisabled ? -1 : 0, class: {
31210
31264
  'selected': this.item.isSelected,
31211
31265
  'editing': this.item.isEditing,
31212
31266
  'disabled': this.item.isDisabled,
31213
31267
  'child-open': this.item.isChildMenuOpen,
31214
- }, onClick: this.handleItemSelect }, index.h("div", { key: '48a1678296e672c6c03fb3e1473c42d3a246d481', class: "menu-item-overlay" }), this.item.isEditing ? this.renderEditMode() : this.renderViewMode()));
31268
+ }, onClick: this.handleItemSelect }, index.h("div", { key: '175258699f76fdad5dcf3c7481e7882c6765bf33', class: "menu-item-overlay" }), this.item.isEditing ? this.renderEditMode() : this.renderViewMode()));
31215
31269
  }
31216
31270
  static get watchers() { return {
31217
31271
  "item": [{
@@ -31259,6 +31313,14 @@ const KritzelMoreMenu = class {
31259
31313
  itemSelect;
31260
31314
  menuAnchor = null;
31261
31315
  isTouchDevice = schema_constants.KritzelDevicesHelper.isTouchDevice();
31316
+ visibleItems = [];
31317
+ visibilityResolveVersion = 0;
31318
+ onItemsChange() {
31319
+ void this.resolveVisibleItems();
31320
+ }
31321
+ componentWillLoad() {
31322
+ void this.resolveVisibleItems();
31323
+ }
31262
31324
  toggleMenu = (event) => {
31263
31325
  event.stopPropagation();
31264
31326
  if (this.menuAnchor) {
@@ -31267,13 +31329,84 @@ const KritzelMoreMenu = class {
31267
31329
  else {
31268
31330
  window.dispatchEvent(new CustomEvent('kritzel-dismiss-menus'));
31269
31331
  this.menuAnchor = this.host;
31332
+ this.visibleItems = this.filterVisibleItemsSync(this.items ?? []);
31333
+ void this.resolveVisibleItems();
31270
31334
  }
31271
31335
  };
31272
31336
  closeMenu = () => {
31273
31337
  this.menuAnchor = null;
31274
31338
  };
31275
- get visibleItems() {
31276
- return this.items.filter(item => item.isVisible !== false);
31339
+ async evaluateVisibility(visibility) {
31340
+ if (typeof visibility === 'function') {
31341
+ try {
31342
+ return (await visibility()) !== false;
31343
+ }
31344
+ catch {
31345
+ return false;
31346
+ }
31347
+ }
31348
+ return visibility !== false;
31349
+ }
31350
+ evaluateVisibilitySync(visibility) {
31351
+ if (typeof visibility === 'function') {
31352
+ try {
31353
+ const result = visibility();
31354
+ return result instanceof Promise ? null : result !== false;
31355
+ }
31356
+ catch {
31357
+ return false;
31358
+ }
31359
+ }
31360
+ return visibility !== false;
31361
+ }
31362
+ filterVisibleItemsSync(items) {
31363
+ return items.reduce((visibleItems, item) => {
31364
+ const isVisible = this.evaluateVisibilitySync(item.isVisible);
31365
+ if (isVisible === null || !isVisible) {
31366
+ return visibleItems;
31367
+ }
31368
+ if (!item.children?.length) {
31369
+ visibleItems.push(item);
31370
+ return visibleItems;
31371
+ }
31372
+ visibleItems.push({
31373
+ ...item,
31374
+ children: this.filterVisibleItemsSync(item.children),
31375
+ });
31376
+ return visibleItems;
31377
+ }, []);
31378
+ }
31379
+ async filterVisibleItems(items) {
31380
+ const resolved = await Promise.all(items.map(async (item) => {
31381
+ const isVisible = await this.evaluateVisibility(item.isVisible);
31382
+ if (!isVisible) {
31383
+ return null;
31384
+ }
31385
+ if (!item.children?.length) {
31386
+ return item;
31387
+ }
31388
+ return {
31389
+ ...item,
31390
+ children: await this.filterVisibleItems(item.children),
31391
+ };
31392
+ }));
31393
+ return resolved.filter((item) => !!item);
31394
+ }
31395
+ async resolveVisibleItems() {
31396
+ const version = ++this.visibilityResolveVersion;
31397
+ const items = this.items ?? [];
31398
+ const resolvedItems = await this.filterVisibleItems(items);
31399
+ if (version !== this.visibilityResolveVersion) {
31400
+ return;
31401
+ }
31402
+ this.visibleItems = resolvedItems;
31403
+ }
31404
+ resolveMenuLabels(items) {
31405
+ return items.map(item => ({
31406
+ ...item,
31407
+ label: this.terms[item.label] ?? item.label,
31408
+ children: item.children ? this.resolveMenuLabels(item.children) : undefined,
31409
+ }));
31277
31410
  }
31278
31411
  handleMenuItemSelect = (event) => {
31279
31412
  const { item, parent } = event.detail;
@@ -31284,8 +31417,13 @@ const KritzelMoreMenu = class {
31284
31417
  this.closeMenu();
31285
31418
  };
31286
31419
  render() {
31287
- return (index.h(index.Host, { key: '96451062021ac2ebcb44b65384a7e9be67f1563b', class: { mobile: this.isTouchDevice }, style: { display: this.visible ? '' : 'none' } }, index.h("div", { key: 'e776398742284ea6984925d4a5a99e732674d640', class: { 'more-menu-wrapper': true, visible: this.visible } }, index.h("button", { key: '4371b2e5cbf7d35fac09a06ee2e54cd49072929b', class: "more-menu-button", "data-testid": "more-menu-button", onClick: this.toggleMenu, "aria-label": this.terms['moreMenu.ariaLabel'] ?? 'More options' }, index.h("kritzel-icon", { key: '076513ce393222b6f604036ec646a8309bc3a3ac', name: this.icon, size: this.iconSize })), index.h("kritzel-portal", { key: 'fde8071b3199735cc7827a2d6f2eaf79d0e1b997', anchor: this.menuAnchor, offsetY: this.offsetY, onClose: this.closeMenu }, index.h("kritzel-menu", { key: '2d49ad8a228c4b55a34c388b794bfdf500fec3c8', items: this.visibleItems, onItemSelect: this.handleMenuItemSelect })))));
31420
+ return (index.h(index.Host, { key: '3eaf5dac0beb1f08100c9f57f63a2bdbef6e592d', class: { mobile: this.isTouchDevice }, style: { display: this.visible ? '' : 'none' } }, index.h("div", { key: '30e4426b7d3a06ec45f0159f6403862bf4106723', class: { 'more-menu-wrapper': true, visible: this.visible } }, index.h("button", { key: '037b715fadad3b674f07bd9225acc4075b2d41b3', class: "more-menu-button", "data-testid": "more-menu-button", onClick: this.toggleMenu, "aria-label": this.terms['moreMenu.ariaLabel'] ?? 'More options' }, index.h("kritzel-icon", { key: 'b68b306b5b913ff36d220604a70626aa8468215c', name: this.icon, size: this.iconSize })), index.h("kritzel-portal", { key: 'dc86c2679956f2dfc66d000b183d99865291b6d3', anchor: this.menuAnchor, offsetY: this.offsetY, onClose: this.closeMenu }, index.h("kritzel-menu", { key: '9bd661fceea95df4d307e497d64365fcb854e8ce', items: this.resolveMenuLabels(this.visibleItems), onItemSelect: this.handleMenuItemSelect })))));
31288
31421
  }
31422
+ static get watchers() { return {
31423
+ "items": [{
31424
+ "onItemsChange": 0
31425
+ }]
31426
+ }; }
31289
31427
  };
31290
31428
  KritzelMoreMenu.style = kritzelMoreMenuCss();
31291
31429
 
@@ -31451,7 +31589,7 @@ const KritzelNumericInput = class {
31451
31589
  this.valueChange.emit(newValue);
31452
31590
  };
31453
31591
  render() {
31454
- return (index.h(index.Host, { key: '8901e0b56ccbc344420483a4a56663f7cd74d9e8' }, index.h("div", { key: '5f3d6b5c56007e11383aa69f60f22031b0e8f332', class: "input-container" }, this.label && index.h("label", { key: '2cb102a5b6c16d068f4f304fbcc6241f79fc0ce8', class: "input-label" }, this.label), index.h("div", { key: '96441095acc3d9a8e77b6d2bac2ceda683c9c976', class: "input-wrapper" }, index.h("input", { key: '523a5485111077e20faded235df6362e5ea9cfd4', type: "number", class: "numeric-input", title: "", min: this.min === Number.MIN_SAFE_INTEGER ? undefined : this.min, max: this.max === Number.MAX_SAFE_INTEGER ? undefined : this.max, step: this.step, value: this.inputValue, placeholder: this.placeholder, onInput: this.handleInput, onBlur: this.handleBlur, onKeyDown: this.handleKeyDown, onInvalid: this.handleInvalid }), index.h("div", { key: '2071e98125a8d014513e93052bd8c1fee17932a5', class: "spinner-buttons" }, index.h("button", { key: '12422e5285dfb532dbbb6eb1a36dbcb3de50f2bc', type: "button", class: "spinner-button spinner-up", onClick: this.handleIncrement, tabIndex: -1, "aria-label": "Increase value" }, index.h("svg", { key: '49ad5625c80fae6706ba4ab91f47bd550481ef74', viewBox: "0 0 10 6", class: "spinner-icon" }, index.h("path", { key: 'ff7d19b61baeb5ba58bb60f2cc629bcec6eb4411', d: "M1 5L5 1L9 5", stroke: "currentColor", "stroke-width": "1.5", fill: "none", "stroke-linecap": "round", "stroke-linejoin": "round" }))), index.h("button", { key: 'e4fec28e4fc997e007127f2b30b00cc752433221', type: "button", class: "spinner-button spinner-down", onClick: this.handleDecrement, tabIndex: -1, "aria-label": "Decrease value" }, index.h("svg", { key: '27bf06cde8de11f4a97090911afd6b8993463112', viewBox: "0 0 10 6", class: "spinner-icon" }, index.h("path", { key: 'ed7071dd79bfe509ebb319ce43ecb3e982dd4157', d: "M1 1L5 5L9 1", stroke: "currentColor", "stroke-width": "1.5", fill: "none", "stroke-linecap": "round", "stroke-linejoin": "round" }))))))));
31592
+ return (index.h(index.Host, { key: '68e6fecd79c3c970d1742b1a0bb8667ca2d31729' }, index.h("div", { key: 'efcf623186d221690bbb956088ec51343de07f3a', class: "input-container" }, this.label && index.h("label", { key: '300de793f02008ad037da021003b46fc34fc5239', class: "input-label" }, this.label), index.h("div", { key: '9ff424901fddfcbdac4835e2b2089df2e0966d0f', class: "input-wrapper" }, index.h("input", { key: 'fc77e6ad1c0b15e3dd3f960b6448ddab1a9beeb1', type: "number", class: "numeric-input", title: "", min: this.min === Number.MIN_SAFE_INTEGER ? undefined : this.min, max: this.max === Number.MAX_SAFE_INTEGER ? undefined : this.max, step: this.step, value: this.inputValue, placeholder: this.placeholder, onInput: this.handleInput, onBlur: this.handleBlur, onKeyDown: this.handleKeyDown, onInvalid: this.handleInvalid }), index.h("div", { key: 'c286606fa0617cc3acbbe40a27e40b154002e01d', class: "spinner-buttons" }, index.h("button", { key: 'e3a65a327975918fe604cd196ffb087fc4d3ca47', type: "button", class: "spinner-button spinner-up", onClick: this.handleIncrement, tabIndex: -1, "aria-label": "Increase value" }, index.h("svg", { key: '62b2ff4ed67ca8b6fc220ff9c63ceca90f75baea', viewBox: "0 0 10 6", class: "spinner-icon" }, index.h("path", { key: '803d5c30826d40d95f282071f3f88356366a9faf', d: "M1 5L5 1L9 5", stroke: "currentColor", "stroke-width": "1.5", fill: "none", "stroke-linecap": "round", "stroke-linejoin": "round" }))), index.h("button", { key: '3c54a48e7082f5aaa9ed923357d0cc4a722018b5', type: "button", class: "spinner-button spinner-down", onClick: this.handleDecrement, tabIndex: -1, "aria-label": "Decrease value" }, index.h("svg", { key: '9f6cb5f34bbb0e70b5773aecec519b7d8b742712', viewBox: "0 0 10 6", class: "spinner-icon" }, index.h("path", { key: '6539b0d4f4bd26e463fa9152cf58757497355cf6', d: "M1 1L5 5L9 1", stroke: "currentColor", "stroke-width": "1.5", fill: "none", "stroke-linecap": "round", "stroke-linejoin": "round" }))))))));
31455
31593
  }
31456
31594
  static get watchers() { return {
31457
31595
  "value": [{
@@ -31490,7 +31628,7 @@ const KritzelOpacitySlider = class {
31490
31628
  }
31491
31629
  render() {
31492
31630
  const percentage = this.getPercentage();
31493
- return (index.h(index.Host, { key: '2dadd8a7c4d1af748e91a6005433288ab231f2e8' }, index.h("div", { key: '65ae90d95bffff244d97329754cf0cb8f73331fa', class: "opacity-container" }, index.h("div", { key: '90e95347f7015882112f0653a25347e715233227', class: "slider-wrapper" }, index.h("input", { key: '15836bc089c1725062016677d3b95384d1958431', type: "range", class: "opacity-slider", min: this.min, max: this.max, step: this.step, value: this.value, onInput: (e) => this.handleInput(e), style: {
31631
+ return (index.h(index.Host, { key: '895ae10ad71e846a1bc62b794751332816650727' }, index.h("div", { key: '22ea167ce0d6eb17c733d789cde10477c48a2a0e', class: "opacity-container" }, index.h("div", { key: '2be4fbf8c1f8ddb1fde8f9da727ed0947d21646c', class: "slider-wrapper" }, index.h("input", { key: 'ed3621baf0adc54aa513a0296ede2f413378a06f', type: "range", class: "opacity-slider", min: this.min, max: this.max, step: this.step, value: this.value, onInput: (e) => this.handleInput(e), style: {
31494
31632
  '--slider-progress': `${percentage}%`,
31495
31633
  '--kritzel-opacity-slider-thumb-border-color': this.previewColor,
31496
31634
  } })))));
@@ -31843,7 +31981,7 @@ const KritzelPortal = class {
31843
31981
  this.portal.style.visibility = 'visible';
31844
31982
  }
31845
31983
  render() {
31846
- return (index.h(index.Host, { key: '72273b4c7811473072ee27668836a5ddddcfd618', style: { display: this.anchor ? 'block' : 'none' } }, index.h("slot", { key: 'b4f37ca27c47ab950a4639162fa31a3c2362d6f7' })));
31984
+ return (index.h(index.Host, { key: '489a752e48d96c2607defa40a39477675685b60a', style: { display: this.anchor ? 'block' : 'none' } }, index.h("slot", { key: '13c6b7567039b8c8c78de27e3d9aeda30a798de1' })));
31847
31985
  }
31848
31986
  static get watchers() { return {
31849
31987
  "anchor": [{
@@ -31857,7 +31995,7 @@ const KritzelPortal = class {
31857
31995
  * This file is auto-generated by the version bump scripts.
31858
31996
  * Do not modify manually.
31859
31997
  */
31860
- const KRITZEL_VERSION = '0.4.8';
31998
+ const KRITZEL_VERSION = '0.4.10';
31861
31999
 
31862
32000
  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)}`;
31863
32001
 
@@ -32069,7 +32207,7 @@ const KritzelSettings = class {
32069
32207
  return (index.h("div", { class: "settings-categories-wrapper" }, index.h("div", { class: { 'settings-tab-panel': true, 'is-active': this.selectedCategoryId === 'general' } }, index.h("div", { class: "settings-content" }, index.h("h3", null, this.t('settings.general.title', 'General Settings')), index.h("div", { class: "settings-group" }, index.h("div", { class: "settings-item" }, index.h("label", { class: "settings-label" }, this.t('settings.general.theme.label', 'Theme')), index.h("p", { class: "settings-description" }, this.t('settings.general.theme.description', 'Select a registered color theme for the editor interface.')), index.h("kritzel-dropdown", { options: this.availableThemes.map(t => ({ value: t, label: t })), value: this.theme, onValueChanged: this.handleThemeChange })), this.availableLocales.length > 0 && (index.h("div", { class: "settings-item" }, index.h("label", { class: "settings-label" }, this.t('settings.general.language.label', 'Language')), index.h("p", { class: "settings-description" }, this.t('settings.general.language.description', 'Select the display language for the editor interface.')), index.h("kritzel-dropdown", { options: this.availableLocales.map(l => ({ value: l.code, label: l.label })), value: this.locale, onValueChanged: this.handleLocaleChange }))), index.h("div", { class: "settings-item" }, index.h("label", { class: "settings-label" }, this.t('settings.general.lockDrawingScale.label', 'Lock Drawing Scale')), index.h("p", { class: "settings-description" }, this.t('settings.general.lockDrawingScale.description', 'When enabled, drawn objects maintain a fixed visual size regardless of the current zoom level.')), index.h("kritzel-slide-toggle", { checked: this.lockDrawingScale, label: this.t('settings.general.lockDrawingScale.label', 'Lock Drawing Scale'), onCheckedChange: this.handleLockDrawingScaleChange }))))), index.h("div", { class: { 'settings-tab-panel': true, 'is-active': this.selectedCategoryId === 'viewport' } }, index.h("div", { class: "settings-content" }, index.h("h3", null, this.t('settings.viewport.title', 'Viewport Settings')), index.h("div", { class: "settings-group" }, index.h("div", { class: "settings-item" }, index.h("label", { class: "settings-label" }, this.t('settings.viewport.minZoom.label', 'Minimum Zoom Level')), index.h("p", { class: "settings-description" }, this.t('settings.viewport.minZoom.description', 'Sets the minimum zoom level. Lower values allow zooming out further to see more of the canvas.')), index.h("kritzel-numeric-input", { value: this.scaleMin, min: 0.0001, max: 1, step: 0.0001, onValueChange: this.handleScaleMinChange })), index.h("div", { class: "settings-item" }, index.h("label", { class: "settings-label" }, this.t('settings.viewport.maxZoom.label', 'Maximum Zoom Level')), index.h("p", { class: "settings-description" }, this.t('settings.viewport.maxZoom.description', 'Sets the maximum zoom level. Higher values allow zooming in closer for detailed work.')), index.h("kritzel-numeric-input", { value: this.scaleMax, min: 1, max: 1000, step: 1, onValueChange: this.handleScaleMaxChange })), index.h("div", { class: "settings-item" }, index.h("label", { class: "settings-label" }, this.t('settings.viewport.boundaryLeft.label', 'Viewport Boundary Left')), index.h("p", { class: "settings-description" }, this.t('settings.viewport.boundaryLeft.description', 'Left boundary in world coordinates. Set to limit how far left the viewport can pan.')), index.h("kritzel-numeric-input", { value: this.viewportBoundaryLeft, step: 100, placeholder: this.t('settings.viewport.boundaryPlaceholder', 'Infinite'), onValueChange: this.handleViewportBoundaryLeftChange })), index.h("div", { class: "settings-item" }, index.h("label", { class: "settings-label" }, this.t('settings.viewport.boundaryRight.label', 'Viewport Boundary Right')), index.h("p", { class: "settings-description" }, this.t('settings.viewport.boundaryRight.description', 'Right boundary in world coordinates. Set to limit how far right the viewport can pan.')), index.h("kritzel-numeric-input", { value: this.viewportBoundaryRight, step: 100, placeholder: this.t('settings.viewport.boundaryPlaceholder', 'Infinite'), onValueChange: this.handleViewportBoundaryRightChange })), index.h("div", { class: "settings-item" }, index.h("label", { class: "settings-label" }, this.t('settings.viewport.boundaryTop.label', 'Viewport Boundary Top')), index.h("p", { class: "settings-description" }, this.t('settings.viewport.boundaryTop.description', 'Top boundary in world coordinates. Set to limit how far up the viewport can pan.')), index.h("kritzel-numeric-input", { value: this.viewportBoundaryTop, step: 100, placeholder: this.t('settings.viewport.boundaryPlaceholder', 'Infinite'), onValueChange: this.handleViewportBoundaryTopChange })), index.h("div", { class: "settings-item" }, index.h("label", { class: "settings-label" }, this.t('settings.viewport.boundaryBottom.label', 'Viewport Boundary Bottom')), index.h("p", { class: "settings-description" }, this.t('settings.viewport.boundaryBottom.description', 'Bottom boundary in world coordinates. Set to limit how far down the viewport can pan.')), index.h("kritzel-numeric-input", { value: this.viewportBoundaryBottom, step: 100, placeholder: this.t('settings.viewport.boundaryPlaceholder', 'Infinite'), onValueChange: this.handleViewportBoundaryBottomChange }))))), index.h("div", { class: { 'settings-tab-panel': true, 'is-active': this.selectedCategoryId === 'shortcuts' } }, index.h("div", { class: "settings-content" }, index.h("h3", null, this.t('settings.shortcuts.title', 'Keyboard Shortcuts')), index.h("div", { class: "shortcuts-list" }, Array.from(this.groupShortcutsByCategory()).map(([category, shortcuts]) => (index.h("div", { class: "shortcuts-category", key: category }, index.h("h4", { class: "shortcuts-category-title" }, category), index.h("div", { class: "shortcuts-group" }, shortcuts.map(shortcut => (index.h("div", { class: "shortcut-item", key: shortcut.key + shortcut.label }, index.h("span", { class: "shortcut-label" }, shortcut.label), index.h("kbd", { class: "shortcut-key" }, this.formatKeyCombo(shortcut)))))))))))), index.h("div", { class: { 'settings-tab-panel': true, 'is-active': this.selectedCategoryId === 'developer' } }, index.h("div", { class: "settings-content" }, index.h("h3", null, this.t('settings.developer.title', 'Developer Options')), index.h("div", { class: "settings-group" }, index.h("div", { class: "settings-item" }, index.h("label", { class: "settings-label" }, this.t('settings.developer.showViewportInfo.label', 'Show Viewport Info')), index.h("p", { class: "settings-description" }, this.t('settings.developer.showViewportInfo.description', 'Display viewport debug information such as position, zoom level, and boundaries.')), index.h("kritzel-slide-toggle", { checked: this.debugInfo.showViewportInfo, label: this.t('settings.developer.showViewportInfo.label', 'Show Viewport Info'), onCheckedChange: this.handleDebugInfoChange('showViewportInfo') })), index.h("div", { class: "settings-item" }, index.h("label", { class: "settings-label" }, this.t('settings.developer.showObjectInfo.label', 'Show Object Info')), index.h("p", { class: "settings-description" }, this.t('settings.developer.showObjectInfo.description', 'Display debug information about objects on the canvas.')), index.h("kritzel-slide-toggle", { checked: this.debugInfo.showObjectInfo, label: this.t('settings.developer.showObjectInfo.label', 'Show Object Info'), onCheckedChange: this.handleDebugInfoChange('showObjectInfo') })), index.h("div", { class: "settings-item" }, index.h("label", { class: "settings-label" }, this.t('settings.developer.showSyncProviderInfo.label', 'Show Sync Provider Info')), index.h("p", { class: "settings-description" }, this.t('settings.developer.showSyncProviderInfo.description', 'Display debug information about the sync provider connection status.')), index.h("kritzel-slide-toggle", { checked: this.debugInfo.showSyncProviderInfo, label: this.t('settings.developer.showSyncProviderInfo.label', 'Show Sync Provider Info'), onCheckedChange: this.handleDebugInfoChange('showSyncProviderInfo') })), index.h("div", { class: "settings-item" }, index.h("label", { class: "settings-label" }, this.t('settings.developer.showMigrationInfo.label', 'Show Migration Info')), index.h("p", { class: "settings-description" }, this.t('settings.developer.showMigrationInfo.description', 'Display debug information about data migrations.')), index.h("kritzel-slide-toggle", { checked: this.debugInfo.showMigrationInfo, label: this.t('settings.developer.showMigrationInfo.label', 'Show Migration Info'), onCheckedChange: this.handleDebugInfoChange('showMigrationInfo') }))))), index.h("div", { class: { 'settings-tab-panel': true, 'is-active': this.selectedCategoryId === 'about' } }, index.h("div", { class: "settings-content" }, index.h("h3", null, this.t('settings.about.title', 'About')), index.h("p", null, this.t('settings.about.description', 'Kritzel - A drawing application')), index.h("p", { class: "version-info" }, "Version ", KRITZEL_VERSION), index.h("p", { class: "version-info" }, "App-State Schema v", schema_constants.CURRENT_APP_STATE_SCHEMA_VERSION), index.h("p", { class: "version-info" }, "Workspace Schema v", schema_constants.CURRENT_WORKSPACE_SCHEMA_VERSION)))));
32070
32208
  }
32071
32209
  render() {
32072
- return (index.h(index.Host, { key: '36bca8be04b34a37630d6aa5394595a8b394d180' }, index.h("kritzel-dialog", { key: '1e257904178fd9afa2bfbe4a758663fb366c8847', isOpen: this.isDialogOpen, dialogTitle: this.t('settings.dialogTitle', 'Settings'), size: "large", contained: true, onDialogClose: this.closeDialog }, index.h("kritzel-master-detail", { key: 'b7d9ad97d9b126ccf6e800e6c9032749635eb988', items: this.categories, selectedItemId: this.selectedCategoryId, onItemSelect: this.handleCategorySelect }, this.renderCategoryContent()))));
32210
+ return (index.h(index.Host, { key: '71f8a12325a781212dd6f2c8b794563fcb4eee64' }, index.h("kritzel-dialog", { key: '1abe7c1dedcf79433a95d60dadce342f46e62c3a', isOpen: this.isDialogOpen, dialogTitle: this.t('settings.dialogTitle', 'Settings'), size: "large", contained: true, onDialogClose: this.closeDialog }, index.h("kritzel-master-detail", { key: '9e8b3a11d97c1a10c6693434eb2e8bfc1d3fda7d', items: this.categories, selectedItemId: this.selectedCategoryId, onItemSelect: this.handleCategorySelect }, this.renderCategoryContent()))));
32073
32211
  }
32074
32212
  static get watchers() { return {
32075
32213
  "settings": [{
@@ -35309,9 +35447,9 @@ const KritzelShareDialog = class {
35309
35447
  this.dialogClosed.emit();
35310
35448
  };
35311
35449
  render() {
35312
- return (index.h(index.Host, { key: 'd30a468b9443f76731b207cb5caec1911f378c15' }, index.h("kritzel-dialog", { key: '4a4ec0a2becba58a49a87fc411a82e26557d8232', dialogTitle: this.terms['share.dialogTitle'] ?? 'Share Workspace', size: "small", isOpen: this.isDialogOpen, onDialogClose: this.closeDialog, contained: true }, index.h("div", { key: '0b2fec3db6c039752429768fb4bd6a2150a4345e', class: "share-content" }, index.h("div", { key: 'bd0b85f8418d0c46d14b718024ed62b9f72e4f52', class: "share-section" }, index.h("div", { key: 'bf62503a593454ffacd30d532fcd79dcbf938659', class: "share-row" }, index.h("div", { key: 'dfd9e83166b886d00fb5b097d0ccd77169c011bd', class: "share-label-group" }, index.h("label", { key: '67dba950ded47a08eb006c1628d14038b18171b5', class: "share-label" }, this.terms['share.linkSharing.label'] ?? 'Link sharing'), index.h("p", { key: '926f8670b35dab1e3e2a6496f44b5d6e0ee46417', class: "share-description" }, this.internalIsPublic
35450
+ return (index.h(index.Host, { key: 'f6028ad618f04e3be65c816f4b53c4ccaa93fc71' }, index.h("kritzel-dialog", { key: '7dabab65955f287650de4ba484e37f4ccdd7c57e', dialogTitle: this.terms['share.dialogTitle'] ?? 'Share Workspace', size: "small", isOpen: this.isDialogOpen, onDialogClose: this.closeDialog, contained: true }, index.h("div", { key: '321e2b17048dea20528c0f34f39a462548c64689', class: "share-content" }, index.h("div", { key: 'b31bbe2f686f5fe2fdc03189ffb98082683fff63', class: "share-section" }, index.h("div", { key: '2c864e00aa4c2da0a7f8ee6c83f149e6e93610a5', class: "share-row" }, index.h("div", { key: 'cbee03ac7ec9ee82f22c77d3681d4a540a0780a3', class: "share-label-group" }, index.h("label", { key: '3be7c8dc2ac2f7dbfe6458a902b14a887895fdc3', class: "share-label" }, this.terms['share.linkSharing.label'] ?? 'Link sharing'), index.h("p", { key: 'c7401bc76fdccbafc6c7c4e37380fc75f35b3a99', class: "share-description" }, this.internalIsPublic
35313
35451
  ? (this.terms['share.linkSharing.enabledDescription'] ?? 'Anyone with the link can access this workspace.')
35314
- : (this.terms['share.linkSharing.disabledDescription'] ?? 'Link sharing is disabled. Only you can access this workspace.'))), index.h("kritzel-slide-toggle", { key: 'e42fabecb37fd21cc4907b966e2f50113b945eda', checked: this.internalIsPublic, onCheckedChange: this.handleToggleChange, label: this.terms['share.linkSharing.toggleLabel'] ?? 'Enable link sharing' }))), this.internalIsPublic && (index.h("div", { key: '40afacc81e0bbcf4c404ae32491e5142a32bca66', class: "share-section" }, index.h("div", { key: '57b6952670b1cbc789770362b573623035b50cf3', class: "share-url-container" }, index.h("input", { key: '0ee836655ca26c1e073d5d86014322939527c74b', type: "text", class: "share-url-input", value: this.getShareUrl(), readOnly: true, onClick: (e) => e.target.select() }), index.h("button", { key: '2d4661c1718bd8390dbe1146e8f9afe035934635', class: { 'copy-button': true, 'copy-success': this.copySuccess }, onClick: this.handleCopyUrl, title: this.copySuccess ? (this.terms['share.copyLink.copied'] ?? 'Copied!') : (this.terms['share.copyLink.title'] ?? 'Copy link') }, index.h("kritzel-icon", { key: 'b43bde94601c352b6065c89c026807d7efb3bcc3', name: this.copySuccess ? 'check' : 'copy', size: 18 }))), this.qrCodeDataUrl && (index.h("div", { key: 'f4d657aa918abbb3cf3ed039332d8c67247b83e8', class: "share-qr-container" }, index.h("img", { key: 'b165a446df3fba87918a19928734d39af1e6bfcf', class: "share-qr-image", src: this.qrCodeDataUrl, alt: "QR code for share link" })))))))));
35452
+ : (this.terms['share.linkSharing.disabledDescription'] ?? 'Link sharing is disabled. Only you can access this workspace.'))), index.h("kritzel-slide-toggle", { key: '52ae14da0d79797e9ad73f1ffbac003fd1395f47', checked: this.internalIsPublic, onCheckedChange: this.handleToggleChange, label: this.terms['share.linkSharing.toggleLabel'] ?? 'Enable link sharing' }))), this.internalIsPublic && (index.h("div", { key: 'bd44618680db14d7d95f0c1bbc4441a103e9f15a', class: "share-section" }, index.h("div", { key: '659cd6b2e2e719d76f692a8dc5a8c0855e5d246d', class: "share-url-container" }, index.h("input", { key: '76be432ecaad753cb7043aad912d8aa1770afb61', type: "text", class: "share-url-input", value: this.getShareUrl(), readOnly: true, onClick: (e) => e.target.select() }), index.h("button", { key: 'ef75a04964fb481fae2f6e924497eb2adedc100d', class: { 'copy-button': true, 'copy-success': this.copySuccess }, onClick: this.handleCopyUrl, title: this.copySuccess ? (this.terms['share.copyLink.copied'] ?? 'Copied!') : (this.terms['share.copyLink.title'] ?? 'Copy link') }, index.h("kritzel-icon", { key: '06bbdc32d4348ca6d75dbbe11ba74d94e1c86882', name: this.copySuccess ? 'check' : 'copy', size: 18 }))), this.qrCodeDataUrl && (index.h("div", { key: '9246e86b26a9794738a29e6a6bfe6c4812339449', class: "share-qr-container" }, index.h("img", { key: '90e18eee230c23e0bfd773c9b8a6967803ae10dc', class: "share-qr-image", src: this.qrCodeDataUrl, alt: "QR code for share link" })))))))));
35315
35453
  }
35316
35454
  static get watchers() { return {
35317
35455
  "isPublic": [{
@@ -35352,7 +35490,7 @@ const KritzelSlideToggle = class {
35352
35490
  }
35353
35491
  };
35354
35492
  render() {
35355
- return (index.h(index.Host, { key: '00160c9cfdafa0d53f3acfb1fb235b29abf2622a', class: { checked: this.checked, disabled: this.disabled }, tabIndex: this.disabled ? -1 : 0, role: "switch", "aria-checked": this.checked ? 'true' : 'false', "aria-disabled": this.disabled ? 'true' : 'false', "aria-label": this.label, onClick: this.handleToggle, onKeyDown: this.handleKeyDown }, index.h("div", { key: '385008948ffbdc0f26249d2d833aae8f04eeb6a0', class: "toggle-track" }, index.h("div", { key: '43929e94335589feb24ee45f1c34a270653dd412', class: "toggle-thumb" }))));
35493
+ return (index.h(index.Host, { key: 'e310e7bc8c115feadaa2494a6a543719f1b25245', class: { checked: this.checked, disabled: this.disabled }, tabIndex: this.disabled ? -1 : 0, role: "switch", "aria-checked": this.checked ? 'true' : 'false', "aria-disabled": this.disabled ? 'true' : 'false', "aria-label": this.label, onClick: this.handleToggle, onKeyDown: this.handleKeyDown }, index.h("div", { key: 'fef37a1b373bf438cbacbf6537640269c2ab6b59', class: "toggle-track" }, index.h("div", { key: '4e140f83a8802e04c0b42e76f5e5719efd5a1cbc', class: "toggle-thumb" }))));
35356
35494
  }
35357
35495
  };
35358
35496
  KritzelSlideToggle.style = kritzelSlideToggleCss();
@@ -35452,7 +35590,7 @@ const KritzelSplitButton = class {
35452
35590
  this.menuScrollTop = event.target.scrollTop;
35453
35591
  };
35454
35592
  render() {
35455
- return (index.h(index.Host, { key: 'fa22dc1185ef07d8127d9ee9f6dbe482d126f934', class: { mobile: this.isTouchDevice } }, index.h("button", { key: 'a48aba702b70e9b24ad5486dde61fa9f9c8f7cdc', class: "split-main-button", tabIndex: 0, onClick: this.handleButtonClick, disabled: this.mainButtonDisabled, "aria-label": "Main action" }, this.buttonIcon && index.h("kritzel-icon", { key: '0d1c9a3e9f744541dd6ebe491105f6ee65233543', name: this.buttonIcon })), index.h("div", { key: 'c9db29f0b9c9cf1974d78f4f60be9e1b0e26c5d2', class: "split-divider" }), index.h("button", { key: '57addfd5228b7c7668be902c53683d12c976b89b', ref: el => (this.splitMenuButtonRef = el), class: "split-menu-button", tabIndex: 0, onClick: this.toggleMenu, disabled: this.menuButtonDisabled, "aria-label": "Open menu" }, index.h("kritzel-icon", { key: 'd860cc503043425860b1234b5d34693bfdd1d5ab', name: this.dropdownIcon })), index.h("kritzel-portal", { key: 'cd6dda8869aea4205d922fc639073019bbd2b102', anchor: this.anchorElement, offsetY: 4, onClose: this.closeMenu }, index.h("kritzel-menu", { key: '1ef1c2b319662e7a2af15b46362addcc75bceb68', ref: el => (this.menuRef = el), items: this.items, onItemSelect: this.handleItemSelect, onItemSave: this.handleItemSave, onItemCancel: this.handleItemCancel, onItemToggleChildMenu: this.handleItemToggleChildMenu, onItemCloseChildMenu: this.handleItemCloseChildMenu, onClose: this.closeMenu, onScroll: this.handleScroll }))));
35593
+ return (index.h(index.Host, { key: 'f7f5019b32f4514a4ccf5bd07742658e3d05020a', class: { mobile: this.isTouchDevice } }, index.h("button", { key: '361007b4281edfe2bf18f28f7c4f19ba1f251b49', class: "split-main-button", tabIndex: 0, onClick: this.handleButtonClick, disabled: this.mainButtonDisabled, "aria-label": "Main action" }, this.buttonIcon && index.h("kritzel-icon", { key: '17d4fad6cfa070225daa67dfc648d4ac64091e90', name: this.buttonIcon })), index.h("div", { key: 'c18107b7d557525092c28ff5d7336ebb3648b672', class: "split-divider" }), index.h("button", { key: 'c7a9a67d832f026fa8e21eff7ea5935cd72862ed', ref: el => (this.splitMenuButtonRef = el), class: "split-menu-button", tabIndex: 0, onClick: this.toggleMenu, disabled: this.menuButtonDisabled, "aria-label": "Open menu" }, index.h("kritzel-icon", { key: '5b40284c8f04fba80ca606b0af201040efe53880', name: this.dropdownIcon })), index.h("kritzel-portal", { key: '62bd7f26c020497f2131759d0d1ef9d18e817b37', anchor: this.anchorElement, offsetY: 4, onClose: this.closeMenu }, index.h("kritzel-menu", { key: 'a1a11a0ae33f9626db3cd56fbb881c613f1ab78c', ref: el => (this.menuRef = el), items: this.items, onItemSelect: this.handleItemSelect, onItemSave: this.handleItemSave, onItemCancel: this.handleItemCancel, onItemToggleChildMenu: this.handleItemToggleChildMenu, onItemCloseChildMenu: this.handleItemCloseChildMenu, onClose: this.closeMenu, onScroll: this.handleScroll }))));
35456
35594
  }
35457
35595
  };
35458
35596
  KritzelSplitButton.style = kritzelSplitButtonCss();
@@ -35473,7 +35611,7 @@ const KritzelStrokeSize = class {
35473
35611
  }
35474
35612
  render() {
35475
35613
  const sizes = this.sizes ?? [];
35476
- return (index.h(index.Host, { key: 'ba181d798d2bf7b60308746635d2077ce1353b83' }, index.h("div", { key: '048fc8934190359b9337747567844ca88925a6ce', class: "size-grid" }, sizes.map(size => (index.h("div", { tabIndex: 0, class: {
35614
+ return (index.h(index.Host, { key: 'c0307e527e6f9d5d663ae3f8f605162cdde2a339' }, index.h("div", { key: '9533e0486f8078d25b7e8fc3323126d77700ac72', class: "size-grid" }, sizes.map(size => (index.h("div", { tabIndex: 0, class: {
35477
35615
  'size-container': true,
35478
35616
  'selected': this.selectedSize === size,
35479
35617
  }, onClick: () => this.handleSizeClick(size) }, index.h("kritzel-color", { value: 'var(--kritzel-global-text-primary)', size: size })))))));
@@ -35481,7 +35619,7 @@ const KritzelStrokeSize = class {
35481
35619
  };
35482
35620
  KritzelStrokeSize.style = kritzelStrokeSizeCss();
35483
35621
 
35484
- const kritzelSyncIndicatorCss = () => `:host{display:contents}.indicator{position:absolute;top:12px;left:50%;transform:translateX(-50%);z-index:var(--kritzel-sync-indicator-z-index, 10001);pointer-events:none;display:inline-flex;align-items:center;gap:8px;padding:6px 10px;border-radius:999px;background:var(--kritzel-sync-indicator-background, rgba(255, 255, 255, 0.92));color:var(--kritzel-sync-indicator-color, #334155);box-shadow:0 2px 10px rgba(0, 0, 0, 0.12);font-family:var(--kritzel-font-family, sans-serif);font-size:12px;line-height:1;font-weight:500;opacity:0;visibility:hidden}.indicator.visible{opacity:1;visibility:visible}.spinner{width:12px;height:12px;border-radius:50%;border:2px solid var(--kritzel-sync-indicator-spinner-track, rgba(148, 163, 184, 0.35));border-top-color:var(--kritzel-sync-indicator-spinner-color, #2563eb);animation:kritzel-sync-indicator-spin 0.8s linear infinite}@keyframes kritzel-sync-indicator-spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}`;
35622
+ const kritzelSyncIndicatorCss = () => `:host{display:contents}.indicator{position:absolute;top:12px;left:50%;transform:translateX(-50%);z-index:var(--kritzel-editor-sync-indicator-z-index, 10001);pointer-events:none;display:inline-flex;align-items:center;gap:8px;padding:6px 10px;border-radius:999px;background:var(--kritzel-editor-sync-indicator-background, rgba(255, 255, 255, 0.92));color:var(--kritzel-editor-sync-indicator-color, #334155);box-shadow:0 2px 10px rgba(0, 0, 0, 0.12);font-family:var(--kritzel-font-family, sans-serif);font-size:12px;line-height:1;font-weight:500;opacity:0;visibility:hidden}.indicator.visible{opacity:1;visibility:visible}.spinner{width:12px;height:12px;border-radius:50%;border:2px solid var(--kritzel-editor-sync-indicator-spinner-track, rgba(148, 163, 184, 0.35));border-top-color:var(--kritzel-editor-sync-indicator-spinner-color, #2563eb);animation:kritzel-sync-indicator-spin 0.8s linear infinite}@keyframes kritzel-sync-indicator-spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}`;
35485
35623
 
35486
35624
  const KritzelSyncIndicator = class {
35487
35625
  constructor(hostRef) {
@@ -35492,7 +35630,7 @@ const KritzelSyncIndicator = class {
35492
35630
  /** The text displayed next to the spinner. */
35493
35631
  text = 'Syncing...';
35494
35632
  render() {
35495
- return (index.h(index.Host, { key: '585d5fd13116c093e5221164e36e6e986a63da58' }, index.h("div", { key: 'd4e0d9e51db002b8ea201a0564c22916809c346d', class: { indicator: true, visible: this.visible }, role: "status", "aria-live": "polite", "aria-label": "Synchronizing workspace" }, index.h("span", { key: 'b82f4c363ee31ae697e435fc8eb6ce0e2f3df1ab', class: "spinner", "aria-hidden": "true" }), index.h("span", { key: 'f39e04332aaf622dad69d0e5d6b6e90ee5df3ed1' }, this.text))));
35633
+ return (index.h(index.Host, { key: '05e825ee1c08fe02008b8e1adfe9e09a7996f221' }, index.h("div", { key: 'ea1635896f643c16713d35d6efd3abf486b1fbe2', class: { indicator: true, visible: this.visible }, role: "status", "aria-live": "polite", "aria-label": "Synchronizing workspace" }, index.h("span", { key: '7b57ea67d6f2c40dc74e966c16ebe9d95c75394a', class: "spinner", "aria-hidden": "true" }), index.h("span", { key: 'fc16405b02d96213d0025239b6e863883192ada5' }, this.text))));
35496
35634
  }
35497
35635
  };
35498
35636
  KritzelSyncIndicator.style = kritzelSyncIndicatorCss();
@@ -35905,14 +36043,14 @@ const KritzelTooltip = class {
35905
36043
  }
35906
36044
  }
35907
36045
  render() {
35908
- return (index.h(index.Host, { key: '227146703158aa65ecffcce348e568d559cee829', style: {
36046
+ return (index.h(index.Host, { key: '93486d37c5dcaaf9a3d0d1c53d6308ddd5712560', style: {
35909
36047
  position: 'fixed',
35910
36048
  zIndex: '9999',
35911
36049
  transition: 'opacity 0.3s ease-in-out, transform 0.3s ease-in-out',
35912
36050
  visibility: this.isVisible ? 'visible' : 'hidden',
35913
36051
  left: `${this.positionX}px`,
35914
36052
  bottom: `${this.positionY}px`,
35915
- } }, index.h("div", { key: '573e97ed81920066e8fbf50f790224c23b37ec21', class: "tooltip-content", onClick: event => event.stopPropagation(), onPointerDown: event => event.stopPropagation(), onMouseDown: event => event.stopPropagation() }, index.h("slot", { key: 'c489b45928cf6720c6827df73b9eedf08bc9d5e2' }))));
36053
+ } }, index.h("div", { key: '4b5cf451f75d09459145b874eb3c7faffafa87e0', class: "tooltip-content", onClick: event => event.stopPropagation(), onPointerDown: event => event.stopPropagation(), onMouseDown: event => event.stopPropagation() }, index.h("slot", { key: '6c7fda100473a7d4fc533edc52d67bf6909bdc0e' }))));
35916
36054
  }
35917
36055
  static get watchers() { return {
35918
36056
  "triggerElement": [{
@@ -35953,7 +36091,7 @@ const KritzelUtilityPanel = class {
35953
36091
  this.redo.emit();
35954
36092
  }
35955
36093
  render() {
35956
- return (index.h(index.Host, { key: '3e86e2c4c480ab56f4646a4f288ad37a63e29642' }, index.h("button", { key: 'e5282e42994f2de061ec36bf892f4da3fc01d2e5', class: "utility-button", "data-testid": "utility-undo", disabled: !this.undoState?.canUndo, onClick: event => this.handleUndo(event), "aria-label": this.terms['utility.undo'] ?? 'Undo' }, index.h("kritzel-icon", { key: '5a2b903cc3e5f9c554928f11a1c3098d569cbc4d', name: "undo" })), index.h("button", { key: '466cdff71501f34f0236b4dc60263817966d7b4a', class: "utility-button", "data-testid": "utility-redo", disabled: !this.undoState?.canRedo, onClick: event => this.handleRedo(event), "aria-label": this.terms['utility.redo'] ?? 'Redo' }, index.h("kritzel-icon", { key: '19209a95787f4475eda9dc0712c2bf36a573c953', name: "redo" })), index.h("div", { key: '7a463d5ac1f78017fb728abcd2c33061d8545978', class: "utility-separator" }), index.h("button", { key: '155c2bb50d70223e4f4d5377bacced56ffda68ad', class: "utility-button", "data-testid": "utility-delete", onClick: () => this.delete.emit(), "aria-label": this.terms['utility.delete'] ?? 'Delete selected items' }, index.h("kritzel-icon", { key: '47bd8469b9efe395e9185b3e7375c7375811c0aa', name: "delete" }))));
36094
+ return (index.h(index.Host, { key: '57c1a6746e4d5b319bb368c9d0bfc52a31d8c8de' }, index.h("button", { key: '5fb00d62b68de41feda9fafb3eec46cc0b85ba0e', class: "utility-button", "data-testid": "utility-undo", disabled: !this.undoState?.canUndo, onClick: event => this.handleUndo(event), "aria-label": this.terms['utility.undo'] ?? 'Undo' }, index.h("kritzel-icon", { key: '5ed2c36405ac7113ac697de9102aaf7067127e2a', name: "undo" })), index.h("button", { key: '0d58c8cbbb8e9547b9416dbf81a2c8011208dc6c', class: "utility-button", "data-testid": "utility-redo", disabled: !this.undoState?.canRedo, onClick: event => this.handleRedo(event), "aria-label": this.terms['utility.redo'] ?? 'Redo' }, index.h("kritzel-icon", { key: '70264118cbe0f54335a02941fdc868f63476cd1c', name: "redo" })), index.h("div", { key: '6eed6a603b8bc62e55b1f6fe82e7d035d1f5833c', class: "utility-separator" }), index.h("button", { key: 'd11195da7a86496afcca26105a6a0f066b1b2340', class: "utility-button", "data-testid": "utility-delete", onClick: () => this.delete.emit(), "aria-label": this.terms['utility.delete'] ?? 'Delete selected items' }, index.h("kritzel-icon", { key: '5e4c731f90863b4db917c67ac7e87c2945c7bcaa', name: "delete" }))));
35957
36095
  }
35958
36096
  };
35959
36097
  KritzelUtilityPanel.style = kritzelUtilityPanelCss();
@@ -35974,7 +36112,7 @@ const KritzelWatermark = class {
35974
36112
  label;
35975
36113
  render() {
35976
36114
  const label = this.label ?? this.core.localizationManager.translate('watermark.poweredBy');
35977
- return (index.h(index.Host, { key: '0eccc75e51c7ac199e23d8f3a871593f1fb65a17' }, index.h("a", { key: '2029f8b56ba8609ca621447eac0df9d30ebfbbbe', class: "watermark-link", href: KRITZEL_WEBSITE_URL, target: "_blank", rel: "noopener noreferrer", part: "watermark", onPointerDown: (ev) => ev.stopPropagation(), onPointerUp: (ev) => ev.stopPropagation() }, label)));
36115
+ return (index.h(index.Host, { key: 'ae00d98f16912afcb7a90d47a9fc4d55fc7468f0' }, index.h("a", { key: '8781f94c9efe44b0e7a7a8c20a119160cb9feb8e', class: "watermark-link", href: KRITZEL_WEBSITE_URL, target: "_blank", rel: "noopener noreferrer", part: "watermark", onPointerDown: (ev) => ev.stopPropagation(), onPointerUp: (ev) => ev.stopPropagation() }, label)));
35978
36116
  }
35979
36117
  };
35980
36118
  KritzelWatermark.style = kritzelWatermarkCss();
@@ -36162,7 +36300,7 @@ const KritzelZoomPanel = class {
36162
36300
  return Number.isFinite(this.zoomPercent) ? Math.max(1, Math.round(this.zoomPercent)) : 100;
36163
36301
  }
36164
36302
  render() {
36165
- return (index.h(index.Host, { key: '054e24893ed609c29b02cf574a68cf9311a3f025' }, index.h("div", { key: '42ad21dca9486ae350d5814b949e0c577a6ed996', class: { panel: true, visible: this.visible } }, index.h("button", { key: 'e360884e5a2edaada46d232cae8121f2c5f45bb6', class: "zoom-button", type: "button", "aria-label": this.terms['zoom.zoomOut'] ?? 'Zoom out', disabled: this.disabled, onClick: this.handleZoomOut }, index.h("kritzel-icon", { key: '8c8bb23c4ec4b78fd04aee5c0edd3371c4d14125', name: "minus" })), index.h("span", { key: '42ab93551c2fbb6d8d4ef076d66405277ba941bf', class: "zoom-level", "aria-live": "polite" }, this.normalizedZoomPercent, "%"), index.h("button", { key: '232f0f74b748999c6892549978bbe085636ca189', class: "zoom-button", type: "button", "aria-label": this.terms['zoom.zoomIn'] ?? 'Zoom in', disabled: this.disabled, onClick: this.handleZoomIn }, index.h("kritzel-icon", { key: '6f9b8ddd4c666573d9716d7bc455c2a3b1035ede', name: "plus" })))));
36303
+ return (index.h(index.Host, { key: 'ac5b3f37fbc298b1522f0ff80c26734164b5358a' }, index.h("div", { key: '8ac3f3600c45c064d9c4c4c9efa78397790edf4b', class: { panel: true, visible: this.visible } }, index.h("button", { key: '63b2a3fce804a310146d95cc9c2a874707f9e265', class: "zoom-button", type: "button", "aria-label": this.terms['zoom.zoomOut'] ?? 'Zoom out', disabled: this.disabled, onClick: this.handleZoomOut }, index.h("kritzel-icon", { key: '86ebb85dfc2b746cedd0ed63cd2ae90378f6bbb8', name: "minus" })), index.h("span", { key: '5191cc1aa9966234c1634882af145403acd32211', class: "zoom-level", "aria-live": "polite" }, this.normalizedZoomPercent, "%"), index.h("button", { key: '47a7ee5b6c1cf1c99cd1826877e0a1c4ce224442', class: "zoom-button", type: "button", "aria-label": this.terms['zoom.zoomIn'] ?? 'Zoom in', disabled: this.disabled, onClick: this.handleZoomIn }, index.h("kritzel-icon", { key: '09782e094534fbc5bbcc612f24eea359568d6132', name: "plus" })))));
36166
36304
  }
36167
36305
  };
36168
36306
  KritzelZoomPanel.style = kritzelZoomPanelCss();