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,5 +1,5 @@
1
1
  import { r as registerInstance, h, H as Host, c as createEvent, g as getElement } from './p-Dhio9uis.js';
2
- import { c as KritzelPath, e as KritzelLine, T as KritzelColorHelper, U as KritzelDevicesHelper, V as KritzelMouseButton, X as KritzelHTMLHelper, Y as DEFAULT_STROKE_SIZES, Z as DEFAULT_COLOR_PALETTE, S as ShapeType, q as KritzelSelectionTool, D as DEFAULT_BRUSH_CONFIG, j as KritzelBrushTool, l as KritzelEraserTool, B as DEFAULT_LINE_TOOL_CONFIG, k as KritzelLineTool, o as KritzelShapeTool, z as DEFAULT_TEXT_CONFIG, n as KritzelTextTool, m as KritzelImageTool, M as KritzelAlignment, C as DEFAULT_ASSET_STORAGE_CONFIG, _ as ObjectHelper, $ as resolveTextToolAvailableFonts, a0 as ThemeHelper, F as darkTheme, E as lightTheme, a1 as KritzelSelectionGroup, a2 as KritzelSelectionBox, a3 as KritzelIconRegistry, y as KritzelFontRegistry, a4 as KritzelKeyboardHelper, a5 as KritzelBaseHandler, s as KritzelWorkspace, f as KritzelGroup, h as KritzelCustomElement, d as KritzelImage, g as KritzelShape, b as KritzelText, N as runMigrations, R as CURRENT_WORKSPACE_SCHEMA_VERSION, P as WORKSPACE_MIGRATIONS, a6 as KritzelClassHelper, Q as CURRENT_APP_STATE_SCHEMA_VERSION, O as APP_STATE_MIGRATIONS, p as KritzelCursorHelper, a7 as KritzelImageFileHelper, a8 as imageCompression, t as KritzelAnchorManager, u as KritzelThemeManager, v as KritzelLocalizationManager, w as KritzelLicenseManager, r as KritzelAssetResolver, a9 as KritzelEventHelper, W as WORKSPACE_EXPORT_VERSION, i as KritzelBaseTool } from './p-Rpm1NA9u.js';
2
+ import { c as KritzelPath, e as KritzelLine, U as KritzelColorHelper, V as KritzelDevicesHelper, X as KritzelMouseButton, Y as KritzelHTMLHelper, Z as DEFAULT_STROKE_SIZES, _ as DEFAULT_COLOR_PALETTE, S as ShapeType, q as KritzelSelectionTool, D as DEFAULT_BRUSH_CONFIG, j as KritzelBrushTool, l as KritzelEraserTool, C as DEFAULT_LINE_TOOL_CONFIG, k as KritzelLineTool, o as KritzelShapeTool, B as DEFAULT_TEXT_CONFIG, n as KritzelTextTool, m as KritzelImageTool, N as KritzelAlignment, z as hasRemoteSyncProvider, E as DEFAULT_ASSET_STORAGE_CONFIG, $ as ObjectHelper, a0 as resolveTextToolAvailableFonts, a1 as ThemeHelper, G as darkTheme, F as lightTheme, a2 as KritzelSelectionGroup, a3 as KritzelSelectionBox, a4 as KritzelIconRegistry, y as KritzelFontRegistry, a5 as KritzelKeyboardHelper, a6 as KritzelBaseHandler, s as KritzelWorkspace, f as KritzelGroup, h as KritzelCustomElement, d as KritzelImage, g as KritzelShape, b as KritzelText, O as runMigrations, T as CURRENT_WORKSPACE_SCHEMA_VERSION, Q as WORKSPACE_MIGRATIONS, a7 as KritzelClassHelper, R as CURRENT_APP_STATE_SCHEMA_VERSION, P as APP_STATE_MIGRATIONS, p as KritzelCursorHelper, a8 as KritzelImageFileHelper, a9 as imageCompression, t as KritzelAnchorManager, u as KritzelThemeManager, v as KritzelLocalizationManager, w as KritzelLicenseManager, r as KritzelAssetResolver, aa as KritzelEventHelper, W as WORKSPACE_EXPORT_VERSION, i as KritzelBaseTool } from './p-C-oQbvCY.js';
3
3
  import * as Y from 'yjs';
4
4
  import 'y-indexeddb';
5
5
  import 'y-websocket';
@@ -136,16 +136,16 @@ const KritzelAvatar = class {
136
136
  height: `${this.size}px`,
137
137
  fontSize: `${Math.round(this.size * 0.4)}px`,
138
138
  };
139
- return (h(Host, { key: '116725d55e4443e80216b3b3251e957d2009b43e', style: containerStyles, class: {
139
+ return (h(Host, { key: '90ce966bb2a374e68c544906c8c746df6cc01038', style: containerStyles, class: {
140
140
  'has-image': !!showImage,
141
141
  'has-initials': !!showInitials,
142
142
  'has-default': !!showDefaultIcon,
143
- }, role: "img", "aria-label": this.getDisplayName() || 'User avatar' }, showImage && (h("img", { key: 'd70ad4eb484404b18c13e066e8c10b3c86918ee5', src: imageUrl, alt: "", class: "avatar-image", ref: (el) => {
143
+ }, role: "img", "aria-label": this.getDisplayName() || 'User avatar' }, showImage && (h("img", { key: '6799f4ad45c7d40ebaf640f191171ed6a5f14ae9', src: imageUrl, alt: "", class: "avatar-image", ref: (el) => {
144
144
  if (el) {
145
145
  el.referrerPolicy = 'no-referrer';
146
146
  el.crossOrigin = 'anonymous';
147
147
  }
148
- }, onError: this.handleImageError })), showInitials && (h("span", { key: '21a9022cffdb69e0c3626e513cb3c036a9d3a16d', class: "avatar-initials", style: { backgroundColor: this.getBackgroundColor() } }, initials)), showDefaultIcon && (h("span", { key: '43e676af007bf254b1921f3efcbc7fff4e204359', class: "avatar-default" }, h("svg", { key: 'a7378795539a902e28ca644a80f1ca00cc9f818a', viewBox: "0 0 24 24", fill: "currentColor" }, 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" }))))));
148
+ }, onError: this.handleImageError })), showInitials && (h("span", { key: '98cdcda64b900994b61a03179be9f2af17c489e8', class: "avatar-initials", style: { backgroundColor: this.getBackgroundColor() } }, initials)), showDefaultIcon && (h("span", { key: '64e6a6ee67017c927dce0b51810a5bb02fc0d6d1', class: "avatar-default" }, h("svg", { key: '42f99637a3c23aaf10fa7c99753347ede1d518c0', viewBox: "0 0 24 24", fill: "currentColor" }, 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" }))))));
149
149
  }
150
150
  static get watchers() { return {
151
151
  "user": [{
@@ -470,7 +470,7 @@ const KritzelBackToContent = class {
470
470
  this.backToContent.emit();
471
471
  };
472
472
  render() {
473
- return (h(Host, { key: '86a94bdc5bb02977214f999b7b880ce3193b6d8f' }, h("button", { key: '6a07c13c9986ebef6100140f7e702a1dfb5c2033', class: { 'back-to-content-button': true, visible: this.visible }, onClick: this.handleClick, "aria-label": this.text }, h("kritzel-icon", { key: 'ebadc99b15f2f015ada8795fa05a3a64ccac9cb5', name: "chevrons-left" }))));
473
+ return (h(Host, { key: '2576489b9f97c77d1931e7c8496bae968d998f9d' }, h("button", { key: 'da891543ce8a9b5ccd2f4a461bdcdc7f3f9ad266', class: { 'back-to-content-button': true, visible: this.visible }, onClick: this.handleClick, "aria-label": this.text }, h("kritzel-icon", { key: '4559cc932918707d5961c6237f3c5e23c9e4cc9d', name: "chevrons-left" }))));
474
474
  }
475
475
  };
476
476
  KritzelBackToContent.style = kritzelBackToContentCss();
@@ -497,11 +497,11 @@ const KritzelButton = class {
497
497
  this.buttonClick.emit();
498
498
  };
499
499
  render() {
500
- return (h(Host, { key: 'b9de2a1261ca0cb491f6980db6a596c48fa45b3a' }, h("button", { key: 'a9507ea0f6a1d9fcb19a7df16bea54cc0ead24f9', type: this.type, class: {
500
+ return (h(Host, { key: '51621006f6b085db2b43accf52192f0674d2c50c' }, h("button", { key: '01ab2d96abe5ad34285e59f6fec66f6b21e91849', type: this.type, class: {
501
501
  'kritzel-button': true,
502
502
  [this.variant]: true,
503
503
  'disabled': this.disabled,
504
- }, disabled: this.disabled, onClick: this.handleClick }, h("slot", { key: '8449a26d4f9fe0226c6150a6639c522486b59780' }))));
504
+ }, disabled: this.disabled, onClick: this.handleClick }, h("slot", { key: '4b085139ea85ad9f836059f4cf2dde15b948e0e2' }))));
505
505
  }
506
506
  };
507
507
  KritzelButton.style = kritzelButtonCss();
@@ -549,13 +549,13 @@ const KritzelColorComponent = class {
549
549
  render() {
550
550
  const resolvedColor = this.resolveColor();
551
551
  const isColorVeryLight = this.isLightColor(resolvedColor);
552
- return (h(Host, { key: 'e39c77c55942b9c615e404efabf4939b2cf05a34' }, h("div", { key: 'ac343295592ea80ca7d3da61ca1244dd1aaa50db', class: "checkerboard-bg", style: {
552
+ return (h(Host, { key: 'e797bfa6a2aed82d1913ec2f02f8b25a063564e5' }, h("div", { key: '5b62fcdbb2985b90fe0d7c7b65b507ec1c55e240', class: "checkerboard-bg", style: {
553
553
  width: `${this.size}px`,
554
554
  height: `${this.size}px`,
555
555
  borderRadius: '50%',
556
556
  display: 'inline-block',
557
557
  position: 'relative',
558
- } }, h("div", { key: '4c87934b3febd4c41874cd290d86a9a9a557e3e5', class: {
558
+ } }, h("div", { key: '2d62717ebf81be93219ebda7071c0ba992d7441a', class: {
559
559
  'color-circle': true,
560
560
  'white': isColorVeryLight,
561
561
  }, style: {
@@ -611,7 +611,7 @@ const KritzelColorPalette = class {
611
611
  render() {
612
612
  const displayedColors = this.isExpanded ? this.colors : this.colors.slice(0, 6);
613
613
  const expandedHeight = this.isExpanded ? this.calculateHeight() : '32px';
614
- return (h(Host, { key: '16aa405a43b3943473e577c5a6ab86adf4ae1ba9' }, h("div", { key: 'f65bb286889700dc1f0e8d84ff59721071b7b73a', class: {
614
+ return (h(Host, { key: '9a637b3a6946384a889f54dc3a635da7ff014439' }, h("div", { key: '4db8bbc7a234bfed26b60ed306a5f999875e2195', class: {
615
615
  'color-grid': true,
616
616
  'expanded': this.isExpanded,
617
617
  }, style: {
@@ -1180,13 +1180,13 @@ const KritzelControls = class {
1180
1180
  // Separate tool controls from config control
1181
1181
  const toolControls = this.internalControls.filter(c => c.type === 'tool' || c.type === 'separator');
1182
1182
  const configControl = this.internalControls.find(c => c.type === 'config' && c.name === this.firstConfig?.name);
1183
- return (h(Host, { key: 'a72f9b1befd185b67d24f6d7a4987ebeb4d6742d', style: { display: this.visible ? '' : 'none' }, class: {
1183
+ return (h(Host, { key: '2a7595b72594df3e8ff9a103c3ff7dd70f0a8045', style: { display: this.visible ? '' : 'none' }, class: {
1184
1184
  mobile: this.isTouchDevice,
1185
- } }, this.isUtilityPanelVisible && (h("kritzel-utility-panel", { key: '4e69324b23542878919119d10db98fc44b0dc536', style: {
1185
+ } }, this.isUtilityPanelVisible && (h("kritzel-utility-panel", { key: '1c2d9f14c53944f291cf49cbcd1bf282ad8a8683', style: {
1186
1186
  position: 'absolute',
1187
1187
  bottom: '56px',
1188
1188
  left: '12px',
1189
- }, undoState: this.undoState, terms: this.terms, onUndo: () => this.kritzelEngine?.undo(), onRedo: () => this.kritzelEngine?.redo(), onDelete: () => this.kritzelEngine?.delete() })), h("div", { key: 'f4c7b461a433055512f5b6d397ab69d470531285', class: "kritzel-controls" }, h("div", { key: '53c27ef4ac2a9121f07ce91c82e63de5d7029de6', class: { 'scroll-indicator-left': true, 'visible': this.canScrollLeft } }), h("div", { key: '43d90c6031e164843dd43563d4fe75410b990278', class: "kritzel-tools-scroll", ref: el => (this.toolsScrollRef = el), onScroll: this.handleToolsScroll }, toolControls.map(control => {
1189
+ }, undoState: this.undoState, terms: this.terms, onUndo: () => this.kritzelEngine?.undo(), onRedo: () => this.kritzelEngine?.redo(), onDelete: () => this.kritzelEngine?.delete() })), h("div", { key: '8ac30e183885bc106033ca163aa7e5927802e67c', class: "kritzel-controls" }, h("div", { key: '74b2808a506e671682aed91107c7c9e27aa4b2c7', class: { 'scroll-indicator-left': true, 'visible': this.canScrollLeft } }), h("div", { key: '3fd693c993d5b11eadd2eec7481214fbc1550301', class: "kritzel-tools-scroll", ref: el => (this.toolsScrollRef = el), onScroll: this.handleToolsScroll }, toolControls.map(control => {
1190
1190
  // Check if this control has sub-options (split-button)
1191
1191
  if (control.subOptions?.length) {
1192
1192
  const selectedSubOption = this.getSelectedSubOption(control);
@@ -1216,10 +1216,10 @@ const KritzelControls = class {
1216
1216
  'kritzel-control': true,
1217
1217
  'selected': this.activeControl?.name === control?.name,
1218
1218
  }, key: control.name, "data-testid": `tool-${control.name}`, onClick: _event => this.handleControlClick?.(control), "aria-label": control.name.charAt(0).toUpperCase() + control.name.slice(1) }, h("kritzel-icon", { name: control.icon })));
1219
- })), h("div", { key: '9a8bd38a8266f1bb5feddcb4e396bb20c25512fa', class: { 'scroll-indicator-right': true, 'visible': this.canScrollRight && !(configControl && this.activeControl && hasConfigUI) } }), configControl && this.activeControl && (h("div", { class: {
1219
+ })), h("div", { key: '6f16a8332dc6a082f2e6aedbec3e31398f2a8b66', class: { 'scroll-indicator-right': true, 'visible': this.canScrollRight && !(configControl && this.activeControl && hasConfigUI) } }), configControl && this.activeControl && (h("div", { class: {
1220
1220
  'kritzel-config-container': true,
1221
1221
  'visible': hasConfigUI,
1222
- }, key: configControl.name }, h("div", { key: 'c889744c924d29f1e65cf67263be9ff708916610', class: { 'config-gradient-left': true, 'visible': this.needsScrolling } }), h("kritzel-tooltip", { key: '9df35a6967a9738e24d328865b07b50328cef811', anchorElement: this.host.shadowRoot?.querySelector('.kritzel-config-container'), triggerElement: this.configTriggerRef }, 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%' } })), h("div", { key: 'a6af0ad5cc2fd0bbef0ade522f82ead53b30f92b', tabIndex: hasConfigUI ? 0 : -1, class: "kritzel-config", "data-testid": "tool-config", ref: el => {
1222
+ }, key: configControl.name }, h("div", { key: '554d743fccfb62d8c368f2a5cdca232ae9842b7f', class: { 'config-gradient-left': true, 'visible': this.needsScrolling } }), h("kritzel-tooltip", { key: 'af97d986d82c961d66c910f7edabd6e597ec59ab', anchorElement: this.host.shadowRoot?.querySelector('.kritzel-config-container'), triggerElement: this.configTriggerRef }, 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%' } })), h("div", { key: 'c6e3fe4a4a9b781db394073c8f929533487be760', tabIndex: hasConfigUI ? 0 : -1, class: "kritzel-config", "data-testid": "tool-config", ref: el => {
1223
1223
  if (el)
1224
1224
  this.configTriggerRef = el;
1225
1225
  }, onKeyDown: event => {
@@ -1228,7 +1228,7 @@ const KritzelControls = class {
1228
1228
  }
1229
1229
  }, style: {
1230
1230
  cursor: 'pointer',
1231
- } }, this.displayValues && (h("div", { key: '07a7fb2e0dc5c53a389da7f742322fdf489e0bd7', class: "color-container" }, h("kritzel-color", { key: '2987b76ca66d58a12ee71dd4c7d82a9c42480690', value: this.displayValues.color, theme: this.theme, size: 18, style: {
1231
+ } }, this.displayValues && (h("div", { key: '9eeb3a9fc29f4848ee9f325d50b4cbfc5bfe5f1c', class: "color-container" }, h("kritzel-color", { key: '1d45edb7ed07a8d12ab55e374ea456b8e95c8d41', value: this.displayValues.color, theme: this.theme, size: 18, style: {
1232
1232
  borderRadius: '50%',
1233
1233
  border: 'none',
1234
1234
  } })))))))));
@@ -1262,7 +1262,7 @@ const KritzelCurrentUser = class {
1262
1262
  /** Resolved localized strings keyed by term key, supplied by the editor. */
1263
1263
  terms = {};
1264
1264
  render() {
1265
- return (h(Host, { key: '3c23677e98ba459144fafde994dc66a14068c5b1' }, h("kritzel-avatar", { key: 'e5c04bf871f7808ab0e380770a2a94ddab8eac83', user: this.user, size: this.avatarSize })));
1265
+ return (h(Host, { key: '19551eb12507f1384aedf3ca4ce5212eca509c24' }, h("kritzel-avatar", { key: 'e39405e8e6c2e77a8acf5f286fa3890e28e8ab17', user: this.user, size: this.avatarSize })));
1266
1266
  }
1267
1267
  };
1268
1268
  KritzelCurrentUser.style = kritzelCurrentUserCss();
@@ -1303,7 +1303,7 @@ const KritzelCurrentUserDialog = class {
1303
1303
  }
1304
1304
  render() {
1305
1305
  const displayName = this.getDisplayName();
1306
- return (h(Host, { key: 'd5be0723f615c45bac897d4f2a2ebe5c67b4769c' }, h("kritzel-dialog", { key: '4de42a3df3d18f96ddfbc362c05e98ade4aeaaf8', dialogTitle: this.terms['currentUser.dialogTitle'] ?? 'Account', isOpen: this.isDialogOpen, onDialogClose: this.closeDialog, size: "small", contained: true }, h("div", { key: '7908d833617b67a6663e41fd91599356ab66619b', class: "user-info" }, h("kritzel-avatar", { key: '93fd9ddb633ad9ad7e17a7c4292861b86ef132f1', user: this.user, size: 80 }), displayName && h("div", { key: 'dff873e3c8b643e7a52f212a3e00167adc301cf7', class: "user-name" }, displayName), this.user?.email && h("div", { key: '7b30a9930cd454ca040181cb11c1d000e408bfc8', class: "user-email" }, this.user.email), h("kritzel-button", { key: '04ea8cbadd90b149ee8f5bbac071040b74eb12b3', class: "logout-button", variant: "primary", onButtonClick: this.handleLogout }, this.terms['menu.logout'] ?? 'Logout')))));
1306
+ return (h(Host, { key: '8f8e63ac6f789f2d0ee43b8b7439634411a91ac9' }, h("kritzel-dialog", { key: '4022a7251881f669f290e0a8beb9a39ef6242b19', dialogTitle: this.terms['currentUser.dialogTitle'] ?? 'Account', isOpen: this.isDialogOpen, onDialogClose: this.closeDialog, size: "small", contained: true }, h("div", { key: '4671f884c121cfaefdd8a47bd52b30acc2d7b345', class: "user-info" }, h("kritzel-avatar", { key: 'a1847b4b67302f3b7179e4e2b751f7f2106b76d4', user: this.user, size: 80 }), displayName && h("div", { key: 'b3d09b4c99d76014a2e36260edce52c39f1081e1', class: "user-name" }, displayName), this.user?.email && h("div", { key: 'a76ce19cbd24ad536b022264dab6a83f4d1b36c2', class: "user-email" }, this.user.email), h("kritzel-button", { key: 'f125164a9d9362eedfbfff4fc2629f7ec055fc14', class: "logout-button", variant: "primary", onButtonClick: this.handleLogout }, this.terms['menu.logout'] ?? 'Logout')))));
1307
1307
  }
1308
1308
  };
1309
1309
  KritzelCurrentUserDialog.style = kritzelCurrentUserDialogCss();
@@ -2243,6 +2243,19 @@ const KritzelEditor = class {
2243
2243
  group: 'clipboard',
2244
2244
  action: (menu, _) => this.engineRef.paste(menu.x, menu.y),
2245
2245
  },
2246
+ {
2247
+ label: 'menu.export',
2248
+ icon: 'upload',
2249
+ group: 'clipboard',
2250
+ visible: (_, objects) => this.canExportObjects(objects),
2251
+ action: (_, objects) => {
2252
+ const format = this.getPreferredExportFormat(objects);
2253
+ if (!format) {
2254
+ return;
2255
+ }
2256
+ void this.engineRef.exportSelectedObject(format);
2257
+ },
2258
+ },
2246
2259
  {
2247
2260
  label: 'menu.order',
2248
2261
  icon: 'ordering',
@@ -2284,16 +2297,43 @@ const KritzelEditor = class {
2284
2297
  },
2285
2298
  ],
2286
2299
  },
2300
+ { label: 'menu.delete', icon: 'delete', group: 'edit', action: () => this.engineRef.delete() },
2301
+ ];
2302
+ moreMenuItems = [
2303
+ {
2304
+ id: 'share',
2305
+ label: 'menu.share',
2306
+ icon: 'share',
2307
+ isVisible: async () => hasRemoteSyncProvider(this.syncConfig),
2308
+ action: () => {
2309
+ if (!this.isLoggedIn && this.loginConfig) {
2310
+ this.openLoginDialog();
2311
+ return;
2312
+ }
2313
+ this.shareDialogRef.open();
2314
+ },
2315
+ },
2287
2316
  {
2317
+ id: 'export',
2288
2318
  label: 'menu.export',
2319
+ icon: 'upload',
2320
+ action: async () => {
2321
+ const preview = await this.engineRef.getScreenshot('png');
2322
+ this.exportRef.open(preview ?? undefined);
2323
+ },
2324
+ },
2325
+ {
2326
+ id: 'import',
2327
+ label: 'menu.import',
2289
2328
  icon: 'download',
2290
- group: 'export',
2291
- children: [
2292
- { label: 'menu.exportAsSvg', icon: 'download', action: () => this.engineRef.exportSelectedObjectsAsSvg() },
2293
- { label: 'menu.exportAsPng', icon: 'download', action: () => this.engineRef.exportSelectedObjectsAsPng() },
2294
- ],
2329
+ action: () => this.engineRef.importFromFile(),
2330
+ },
2331
+ {
2332
+ id: 'settings',
2333
+ label: 'menu.settings',
2334
+ icon: 'settings',
2335
+ action: () => this.settingsRef.open(),
2295
2336
  },
2296
- { label: 'menu.delete', icon: 'delete', group: 'edit', action: () => this.engineRef.delete() },
2297
2337
  ];
2298
2338
  themes;
2299
2339
  theme = 'light';
@@ -2365,6 +2405,9 @@ const KritzelEditor = class {
2365
2405
  isNotificationDismissing = false;
2366
2406
  contextMenuState;
2367
2407
  normalizedControls = [];
2408
+ resolvedMoreMenuItems = [];
2409
+ isSyncing = false;
2410
+ isLoadingOverlayVisible = false;
2368
2411
  notificationDismissTimeout;
2369
2412
  notificationDismissAnimationTimeout;
2370
2413
  notificationDisplayDurationMs = 5000;
@@ -2407,6 +2450,18 @@ const KritzelEditor = class {
2407
2450
  onControlsChange(newValue) {
2408
2451
  this.normalizedControls = this.normalizeControls(newValue);
2409
2452
  }
2453
+ onMoreMenuItemsChange() {
2454
+ this.recomputeMoreMenuItems();
2455
+ }
2456
+ onSyncConfigChange() {
2457
+ this.recomputeMoreMenuItems();
2458
+ }
2459
+ onLoginConfigChange() {
2460
+ this.recomputeMoreMenuItems();
2461
+ }
2462
+ onUserChange() {
2463
+ this.recomputeMoreMenuItems();
2464
+ }
2410
2465
  onActiveWorkspaceIdChange(newId) {
2411
2466
  // If no ID is provided, do nothing (allows other logic to control activeWorkspace)
2412
2467
  if (!newId) {
@@ -2456,6 +2511,7 @@ const KritzelEditor = class {
2456
2511
  }
2457
2512
  this.resolvedTerms = await this.engineRef.getResolvedTerms();
2458
2513
  this.availableLocaleOptions = await this.engineRef.getAvailableLocaleOptions();
2514
+ this.recomputeMoreMenuItems();
2459
2515
  }
2460
2516
  onTouchStart(event) {
2461
2517
  if (event.cancelable) {
@@ -2616,11 +2672,14 @@ const KritzelEditor = class {
2616
2672
  async exportViewportAsSvg(options) {
2617
2673
  return this.engineRef.exportViewportAsSvg(options);
2618
2674
  }
2619
- async exportSelectedObjectsAsPng() {
2620
- return this.engineRef.exportSelectedObjectsAsPng();
2675
+ async getSelectedObjectSupportedExportFormats() {
2676
+ return this.engineRef.getSelectedObjectSupportedExportFormats();
2621
2677
  }
2622
- async exportSelectedObjectsAsSvg() {
2623
- return this.engineRef.exportSelectedObjectsAsSvg();
2678
+ async canExportSelectedObjectAs(format) {
2679
+ return this.engineRef.canExportSelectedObjectAs(format);
2680
+ }
2681
+ async exportSelectedObject(format) {
2682
+ return this.engineRef.exportSelectedObject(format);
2624
2683
  }
2625
2684
  async downloadAsJson(filename) {
2626
2685
  return this.engineRef.downloadAsJson(filename);
@@ -2687,6 +2746,7 @@ const KritzelEditor = class {
2687
2746
  currentUserDialogRef;
2688
2747
  componentWillLoad() {
2689
2748
  this.normalizedControls = this.normalizeControls(this.controls);
2749
+ this.recomputeMoreMenuItems();
2690
2750
  this.loadSettingsFromStorage();
2691
2751
  this.applyTheme();
2692
2752
  }
@@ -2707,13 +2767,18 @@ const KritzelEditor = class {
2707
2767
  };
2708
2768
  });
2709
2769
  }
2770
+ recomputeMoreMenuItems() {
2771
+ if (this.moreMenuItems) {
2772
+ this.resolvedMoreMenuItems = this.moreMenuItems;
2773
+ return;
2774
+ }
2775
+ }
2710
2776
  applyTheme() {
2711
2777
  const themeObj = this.resolveThemeObject();
2712
2778
  ThemeHelper.applyThemeToElement(this.host, themeObj);
2713
2779
  }
2714
2780
  resolveThemeObject() {
2715
- return this.themes?.find(t => t.name === this.theme)
2716
- ?? (this.theme === 'dark' ? darkTheme : lightTheme);
2781
+ return this.themes?.find(t => t.name === this.theme) ?? (this.theme === 'dark' ? darkTheme : lightTheme);
2717
2782
  }
2718
2783
  componentDidLoad() {
2719
2784
  this.registerCustomSvgIcons();
@@ -2748,7 +2813,7 @@ const KritzelEditor = class {
2748
2813
  syncConfig: this.syncConfig,
2749
2814
  assetStorageConfig: this.assetStorageConfig,
2750
2815
  loginConfig: this.loginConfig,
2751
- theme: this.theme
2816
+ theme: this.theme,
2752
2817
  });
2753
2818
  }
2754
2819
  async onEngineReady(event) {
@@ -2911,43 +2976,6 @@ const KritzelEditor = class {
2911
2976
  }
2912
2977
  this.themeChange.emit(event.detail.theme);
2913
2978
  }
2914
- get moreMenuItems() {
2915
- return [
2916
- {
2917
- id: 'share',
2918
- label: this.resolvedTerms['menu.share'] ?? 'Share',
2919
- icon: 'share',
2920
- action: () => {
2921
- if (!this.isLoggedIn && this.loginConfig) {
2922
- this.openLoginDialog();
2923
- return;
2924
- }
2925
- this.shareDialogRef.open();
2926
- },
2927
- },
2928
- {
2929
- id: 'export',
2930
- label: this.resolvedTerms['menu.export'] ?? 'Export',
2931
- icon: 'upload',
2932
- action: async () => {
2933
- const preview = await this.engineRef.getScreenshot('png');
2934
- this.exportRef.open(preview ?? undefined);
2935
- },
2936
- },
2937
- {
2938
- id: 'import',
2939
- label: this.resolvedTerms['menu.import'] ?? 'Import',
2940
- icon: 'download',
2941
- action: () => this.engineRef.importFromFile(),
2942
- },
2943
- {
2944
- id: 'settings',
2945
- label: this.resolvedTerms['menu.settings'] ?? 'Settings',
2946
- icon: 'settings',
2947
- action: () => this.settingsRef.open(),
2948
- }
2949
- ];
2950
- }
2951
2979
  handleToggleIsPublic = async (event) => {
2952
2980
  const isPublic = event.detail;
2953
2981
  const activeWorkspace = await this.engineRef.getActiveWorkspace();
@@ -2977,6 +3005,28 @@ const KritzelEditor = class {
2977
3005
  get isLoggedIn() {
2978
3006
  return !!this.user && !this.user.isGuest;
2979
3007
  }
3008
+ canExportObjectsAs(objects, format) {
3009
+ if (!objects || objects.length !== 1) {
3010
+ return false;
3011
+ }
3012
+ const [selected] = objects;
3013
+ if (!ObjectHelper.isKritzelExportable(selected)) {
3014
+ return false;
3015
+ }
3016
+ return selected.getSupportedExportFormats().includes(format);
3017
+ }
3018
+ canExportObjects(objects) {
3019
+ return this.canExportObjectsAs(objects, 'png') || this.canExportObjectsAs(objects, 'svg');
3020
+ }
3021
+ getPreferredExportFormat(objects) {
3022
+ if (this.canExportObjectsAs(objects, 'png')) {
3023
+ return 'png';
3024
+ }
3025
+ if (this.canExportObjectsAs(objects, 'svg')) {
3026
+ return 'svg';
3027
+ }
3028
+ return null;
3029
+ }
2980
3030
  async openLoginDialog() {
2981
3031
  this.loginDialogRef?.open();
2982
3032
  }
@@ -3153,41 +3203,41 @@ const KritzelEditor = class {
3153
3203
  const isLoggedIn = this.isLoggedIn;
3154
3204
  const shouldShowCurrentUser = isLoggedIn;
3155
3205
  const shouldShowLoginButton = this.isReady && !!this.loginConfig && !isLoggedIn;
3156
- return (h(Host, { key: 'ab06876e3a07e97c3788113ffd0483bbb2819d5b' }, h("div", { key: 'e393eacfad2b224a720e5a97b047af8dd3ce97a5', class: "editor-content", style: {
3206
+ return (h(Host, { key: 'd9d87b5ca40972d7ab2c5986f340278347f78c6e' }, h("div", { key: 'd8da2ed24d19c550420689c6720dc491fe0d3a22', class: "editor-content", style: {
3157
3207
  opacity: this.isEditorVisible ? '1' : '0',
3158
3208
  visibility: this.isEditorVisible ? 'visible' : 'hidden',
3159
3209
  transition: 'opacity 0.2s ease-in-out, visibility 0.2s ease-in-out',
3160
- } }, h("div", { key: 'd9dcbd6859bea28d75c7f8b0cc26ea3ba58b2433', class: "top-left-buttons" }, 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) }), h("kritzel-back-to-content", { key: '8b345bf6ce0ec33ca35f1f630af2050b95bc6b74', visible: this.isBackToContentButtonVisible, text: this.resolvedTerms['backToContent.label'] ?? 'Back to content', onBackToContent: () => this.backToContent() })), this.activeNotification && (h("div", { key: '1b965de38a7f1f40d5d91431801e873289416924', class: "top-center-notification-layer", role: "presentation" }, h("div", { key: 'f165dc7acc5e1bc7923fec217ebc16fe4a3f2ece', class: { 'top-center-notification-container': true, 'is-dismissing': this.isNotificationDismissing }, role: "presentation" }, 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 && (h("kritzel-context-menu", { key: 'fc3417276f0034e7fc7e8360d3f3ce57106146b2', class: "context-menu", items: this.contextMenuState.items, objects: this.contextMenuState.objects, style: {
3210
+ } }, h("div", { key: '82a5d216e7c95307b1bdbd1f06fdfe881c1c50d4', class: "top-left-buttons" }, h("kritzel-workspace-manager", { key: 'de13673384b44bb6bd309a927a2866a9b78b519e', visible: this.isWorkspaceManagerVisible, workspaces: this.workspaces, activeWorkspace: this.activeWorkspace, terms: this.resolvedTerms, onWorkspaceChange: event => (this.activeWorkspace = event.detail), onIsWorkspaceManagerReady: () => (this.isWorkspaceManagerReady = true) }), h("kritzel-back-to-content", { key: 'f68610d7d91713f7a0ec5f0135d208769f7fab9c', visible: this.isBackToContentButtonVisible, text: this.resolvedTerms['backToContent.label'] ?? 'Back to content', onBackToContent: () => this.backToContent() })), this.activeNotification && (h("div", { key: '9d1f972fb67050b1f996e3088710713fa1219b9a', class: "top-center-notification-layer", role: "presentation" }, h("div", { key: '295eff0a02f7cc79ad8e80cda23baa3c576ce156', class: { 'top-center-notification-container': true, 'is-dismissing': this.isNotificationDismissing }, role: "presentation" }, h("kritzel-notification-card", { key: '1d1bf140ddfb7b927d30fb1d77e7b887240d7deb', notification: this.activeNotification, locale: this.locale, onDismiss: this.dismissNotification, onHoverChange: event => this.handleNotificationHoverChange(event) })))), this.contextMenuState && this.contextMenuState.items.length > 0 && (h("kritzel-context-menu", { key: '1062bc9554e5834ef2002fbfe953f7938866fb84', class: "context-menu", items: this.contextMenuState.items, objects: this.contextMenuState.objects, style: {
3161
3211
  position: 'absolute',
3162
3212
  left: `${this.contextMenuState.position.x}px`,
3163
3213
  top: `${this.contextMenuState.position.y}px`,
3164
3214
  zIndex: '10002',
3165
- }, onActionSelected: event => this.handleContextMenuActionSelected(event), onClose: () => this.hideContextMenu() })), h("kritzel-engine", { key: '0c9df0386d38a87f50d324cfc51307a1cb947a58', ref: el => {
3215
+ }, onActionSelected: event => this.handleContextMenuActionSelected(event), onClose: () => this.hideContextMenu() })), h("kritzel-sync-indicator", { key: '0094a026828473988f857d82cfccb811f36f049c', visible: this.isSyncing, text: "Syncing..." }), h("kritzel-loading-overlay", { key: '680096e20b6175104a866b9a92ca4c1d0087c93a', visible: this.isLoadingOverlayVisible, text: this.resolvedTerms['editor.loading'] ?? 'Loading...' }), h("kritzel-engine", { key: '0698a44fcbf2d6ea786d53502350ac0c9ffb9d66', ref: el => {
3166
3216
  if (el) {
3167
3217
  this.engineRef = el;
3168
3218
  }
3169
- }, 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) }), h("kritzel-controls", { key: '46d8e9c45c7bed24fc02d17c8fef836d596088d7', visible: this.isControlsVisible, class: { 'keyboard-open': this.isVirtualKeyboardOpen }, ref: el => {
3219
+ }, workspace: this.activeWorkspace, activeWorkspaceId: this.activeWorkspaceId, editorId: this.editorId, syncConfig: this.syncConfig, assetStorageConfig: this.assetStorageConfig, user: this.user, scaleMax: this.scaleMax, lockDrawingScale: this.lockDrawingScale, isObjectDistanceFadingActive: this.isObjectDistanceFadingActive, scaleMin: this.scaleMin, cursorTarget: this.cursorTarget, isLoading: this.isLoading, viewportBoundaryLeft: this.viewportBoundaryLeft, viewportBoundaryRight: this.viewportBoundaryRight, viewportBoundaryTop: this.viewportBoundaryTop, viewportBoundaryBottom: this.viewportBoundaryBottom, isPanningEnabled: this.isPanningEnabled, isZoomingEnabled: this.isZoomingEnabled, theme: this.theme, themes: this.themes, licenseKey: this.licenseKey, locale: this.locale, locales: this.locales, fallbackLocale: this.fallbackLocale, debugInfo: this.debugInfo, globalContextMenuItems: this.globalContextMenuItems, objectContextMenuItems: this.objectContextMenuItems, onIsEngineReady: event => this.onEngineReady(event), onWorkspacesChange: event => this.handleWorkspacesChange(event), onActiveWorkspaceChange: event => this.handleActiveWorkspaceChange(event), onObjectsChange: event => this.handleObjectsChange(event), onObjectsAdded: event => this.handleObjectsAdded(event), onObjectsRemoved: event => this.handleObjectsRemoved(event), onObjectsUpdated: event => this.handleObjectsUpdated(event), onUndoStateChange: event => this.handleUndoStateChange(event), onObjectsInViewportChange: event => this.handleObjectsInViewportChange(event), onViewportChange: event => this.handleViewportChange(event), onAwarenessChange: event => this.handleAwarenessChange(event), onNotificationsChange: event => this.handleNotificationsChange(event), onContextMenuStateChange: event => this.handleContextMenuStateChange(event), onSyncingChange: event => (this.isSyncing = event.detail), onLoadingChange: event => (this.isLoadingOverlayVisible = event.detail) }), h("kritzel-controls", { key: '765a670807111ad68e9f8d14bdca2651a33704d4', visible: this.isControlsVisible, class: { 'keyboard-open': this.isVirtualKeyboardOpen }, ref: el => {
3170
3220
  if (el) {
3171
3221
  this.controlsRef = el;
3172
3222
  }
3173
- }, controls: this.normalizedControls, isUtilityPanelVisible: this.isUtilityPanelVisible, undoState: this.undoState ?? undefined, theme: this.theme, terms: this.resolvedTerms, onIsControlsReady: () => (this.isControlsReady = true) }), h("div", { key: 'e3ef94509a82fe435f827b66cf1813969b7dec66', class: "bottom-left-buttons" }, h("kritzel-zoom-panel", { key: '4e6cbdf85f550e6e3037dce3b910c0582ea0320b', visible: this.isZoomPanelVisible, disabled: !this.isZoomingEnabled, zoomPercent: this.currentZoomPercent, terms: this.resolvedTerms, onZoomIn: () => this.zoomIn(), onZoomOut: () => this.zoomOut() })), h("div", { key: 'a62df7f8a68cbed728b701894a5284c2bff39e1e', class: "top-right-buttons" }, h("kritzel-settings", { key: '0b6daf9ff43551df9f86431d398a32913a845925', ref: el => {
3223
+ }, controls: this.normalizedControls, isUtilityPanelVisible: this.isUtilityPanelVisible, undoState: this.undoState ?? undefined, theme: this.theme, terms: this.resolvedTerms, onIsControlsReady: () => (this.isControlsReady = true) }), h("div", { key: 'e7c5ebab4a40b7ab4fa5d19d203129326c0009ed', class: "bottom-left-buttons" }, h("kritzel-zoom-panel", { key: '0aa3891b553d2abcd06e635227fd6bb6656ed739', visible: this.isZoomPanelVisible, disabled: !this.isZoomingEnabled, zoomPercent: this.currentZoomPercent, terms: this.resolvedTerms, onZoomIn: () => this.zoomIn(), onZoomOut: () => this.zoomOut() })), h("div", { key: '824244bbd78784c1d78ab89cc12e206ca61d84cc', class: "top-right-buttons" }, h("kritzel-settings", { key: 'baf4830b6b9ccb6241b1a1baa5cc6ff9d0295fb9', ref: el => {
3174
3224
  if (el) {
3175
3225
  this.settingsRef = el;
3176
3226
  }
3177
- }, shortcuts: this.shortcuts, availableThemes: this.themes && this.themes.length > 0 ? this.themes.map(t => t.name) : ['light', 'dark'], availableLocales: this.availableLocaleOptions, settings: this.currentSettingsConfig, terms: this.resolvedTerms, onSettingsChange: event => this.handleSettingsChange(event) }), h("kritzel-export", { key: '29d02ffd3358fe91d27435cc3fdd5f8ac2ad7cf5', ref: el => {
3227
+ }, shortcuts: this.shortcuts, availableThemes: this.themes && this.themes.length > 0 ? this.themes.map(t => t.name) : ['light', 'dark'], availableLocales: this.availableLocaleOptions, settings: this.currentSettingsConfig, terms: this.resolvedTerms, onSettingsChange: event => this.handleSettingsChange(event) }), h("kritzel-export", { key: '57e2682450c71803cd49bfe65f16e02d9d3358bc', ref: el => {
3178
3228
  if (el) {
3179
3229
  this.exportRef = el;
3180
3230
  }
3181
- }, workspaceName: this.activeWorkspace?.name || 'workspace', terms: this.resolvedTerms, onExportPng: () => this.engineRef.exportViewportAsPng(), onExportSvg: () => this.engineRef.exportViewportAsSvg(), onExportJson: event => this.engineRef.downloadAsJson(event.detail) }), h("kritzel-active-users", { key: '729ca599e46654bc1429b5d91ff73f26d9b6e7bb', users: this.activeUsers }), shouldShowCurrentUser && h("kritzel-current-user", { key: '84c07268072418a82cf504f16803c59a1e37c0ae', user: this.user, terms: this.resolvedTerms, onClick: () => this.currentUserDialogRef?.open() }), shouldShowCurrentUser && (h("kritzel-current-user-dialog", { key: '55189e3da7899628b252e7995b20bc422fce2d5c', ref: el => {
3231
+ }, workspaceName: this.activeWorkspace?.name || 'workspace', terms: this.resolvedTerms, onExportPng: () => this.engineRef.exportViewportAsPng(), onExportSvg: () => this.engineRef.exportViewportAsSvg(), onExportJson: event => this.engineRef.downloadAsJson(event.detail) }), h("kritzel-active-users", { key: '955d2a61b98d937f8eec39ab19f1b73e702c9fd7', users: this.activeUsers }), shouldShowCurrentUser && h("kritzel-current-user", { key: 'c0885e0c8cab1a4cb50c8f6d4b17e03f32cc2b87', user: this.user, terms: this.resolvedTerms, onClick: () => this.currentUserDialogRef?.open() }), shouldShowCurrentUser && (h("kritzel-current-user-dialog", { key: '8209b2562ffb821036bb8e4dacf052cacbe99bee', ref: el => {
3182
3232
  if (el) {
3183
3233
  this.currentUserDialogRef = el;
3184
3234
  this.currentUserDialogRef.addEventListener('logoutRequest', this.handleCurrentUserLogout);
3185
3235
  }
3186
- }, user: this.user, terms: this.resolvedTerms })), shouldShowLoginButton && (h("kritzel-button", { key: 'a55d85769bad635fcc7934f584bf1bf74c1e7ea6', onButtonClick: () => this.loginDialogRef?.open() }, this.resolvedTerms['login.dialogTitle'] ?? 'Sign in')), h("kritzel-more-menu", { key: '67dd879ec3c55c98e32f6112ca53d925f47b56b4', items: this.moreMenuItems, visible: this.isMoreMenuVisible, terms: this.resolvedTerms }), h("kritzel-share-dialog", { key: '5f13130099110eb472022099219e15877ceceb57', ref: el => {
3236
+ }, user: this.user, terms: this.resolvedTerms })), shouldShowLoginButton && h("kritzel-button", { key: '9cec12de2207e8931a08c0a4f6bda39d039dfd1f', onButtonClick: () => this.loginDialogRef?.open() }, this.resolvedTerms['login.dialogTitle'] ?? 'Sign in'), h("kritzel-more-menu", { key: 'fe0e1413148b9ac64e5f058790ba96b856faef9e', items: this.resolvedMoreMenuItems, visible: this.isMoreMenuVisible, terms: this.resolvedTerms }), h("kritzel-share-dialog", { key: 'ea06c88b0cba0464144bea1e2247b37cc5c44646', ref: el => {
3187
3237
  if (el) {
3188
3238
  this.shareDialogRef = el;
3189
3239
  }
3190
- }, isPublic: this.currentIsPublic, workspaceId: this.activeWorkspace?.id, terms: this.resolvedTerms, onToggleIsPublic: this.handleToggleIsPublic }), this.loginConfig && (h("kritzel-login-dialog", { key: 'cda0fcdcad75de8730ce10bda425cc0fbe01429a', ref: el => {
3240
+ }, isPublic: this.currentIsPublic, workspaceId: this.activeWorkspace?.id, terms: this.resolvedTerms, onToggleIsPublic: this.handleToggleIsPublic }), this.loginConfig && (h("kritzel-login-dialog", { key: 'e9a523d5989284b7e30ac44f55882a17c82c643b', ref: el => {
3191
3241
  if (el) {
3192
3242
  this.loginDialogRef = el;
3193
3243
  }
@@ -3212,6 +3262,18 @@ const KritzelEditor = class {
3212
3262
  "controls": [{
3213
3263
  "onControlsChange": 0
3214
3264
  }],
3265
+ "moreMenuItems": [{
3266
+ "onMoreMenuItemsChange": 0
3267
+ }],
3268
+ "syncConfig": [{
3269
+ "onSyncConfigChange": 0
3270
+ }],
3271
+ "loginConfig": [{
3272
+ "onLoginConfigChange": 0
3273
+ }],
3274
+ "user": [{
3275
+ "onUserChange": 0
3276
+ }],
3215
3277
  "activeWorkspaceId": [{
3216
3278
  "onActiveWorkspaceIdChange": 0
3217
3279
  }],
@@ -25918,962 +25980,310 @@ class KritzelCore {
25918
25980
  }
25919
25981
  }
25920
25982
 
25921
- /**
25922
- * Helper class for exporting Kritzel objects to SVG format.
25923
- * Generates clean, standards-compliant SVG markup from canvas objects.
25924
- */
25925
- class KritzelSvgExportHelper {
25926
- /**
25927
- * Calculates the combined bounding box that encompasses all objects.
25928
- * Uses rotatedBoundingBox to account for object rotation.
25929
- * @param objects - Array of objects to calculate bounds for
25930
- * @returns Combined bounding box, or null if no objects provided
25931
- */
25932
- static calculateCombinedBounds(objects) {
25933
- if (!objects || objects.length === 0) {
25934
- return null;
25935
- }
25936
- let minX = Infinity;
25937
- let minY = Infinity;
25938
- let maxX = -Infinity;
25939
- let maxY = -Infinity;
25940
- for (const obj of objects) {
25941
- const bbox = obj.rotatedBoundingBox;
25942
- minX = Math.min(minX, bbox.x);
25943
- minY = Math.min(minY, bbox.y);
25944
- maxX = Math.max(maxX, bbox.x + bbox.width);
25945
- maxY = Math.max(maxY, bbox.y + bbox.height);
25946
- }
25947
- return {
25948
- x: minX,
25949
- y: minY,
25950
- z: 1,
25951
- width: maxX - minX,
25952
- height: maxY - minY,
25953
- };
25983
+ function resolveUrl(url, baseUrl) {
25984
+ // url is absolute already
25985
+ if (url.match(/^[a-z]+:\/\//i)) {
25986
+ return url;
25954
25987
  }
25955
- /**
25956
- * Generates a complete SVG document string from the given objects.
25957
- * @param objects - Objects to export
25958
- * @param options - Export options
25959
- * @returns SVG document string
25960
- */
25961
- static generateSvg(objects, options = {}) {
25962
- const { theme, padding = 0, includeXmlDeclaration = true, rasterSafeForCanvas = false, imageDataUrls } = options;
25963
- if (!objects || objects.length === 0) {
25964
- return '';
25965
- }
25966
- // Flatten groups to get all objects including nested children
25967
- const allObjects = this.flattenObjects(objects);
25968
- const bounds = this.calculateCombinedBounds(allObjects);
25969
- if (!bounds) {
25970
- return '';
25988
+ // url is absolute already, without protocol
25989
+ if (url.match(/^\/\//)) {
25990
+ return window.location.protocol + url;
25991
+ }
25992
+ // dataURI, mailto:, tel:, etc.
25993
+ if (url.match(/^[a-z]+:/i)) {
25994
+ return url;
25995
+ }
25996
+ const doc = document.implementation.createHTMLDocument();
25997
+ const base = doc.createElement('base');
25998
+ const a = doc.createElement('a');
25999
+ doc.head.appendChild(base);
26000
+ doc.body.appendChild(a);
26001
+ if (baseUrl) {
26002
+ base.href = baseUrl;
26003
+ }
26004
+ a.href = url;
26005
+ return a.href;
26006
+ }
26007
+ const uuid = (() => {
26008
+ // generate uuid for className of pseudo elements.
26009
+ // We should not use GUIDs, otherwise pseudo elements sometimes cannot be captured.
26010
+ let counter = 0;
26011
+ // ref: http://stackoverflow.com/a/6248722/2519373
26012
+ const random = () =>
26013
+ // eslint-disable-next-line no-bitwise
26014
+ `0000${((Math.random() * 36 ** 4) << 0).toString(36)}`.slice(-4);
26015
+ return () => {
26016
+ counter += 1;
26017
+ return `u${random()}${counter}`;
26018
+ };
26019
+ })();
26020
+ function toArray(arrayLike) {
26021
+ const arr = [];
26022
+ for (let i = 0, l = arrayLike.length; i < l; i++) {
26023
+ arr.push(arrayLike[i]);
26024
+ }
26025
+ return arr;
26026
+ }
26027
+ let styleProps = null;
26028
+ function getStyleProperties(options = {}) {
26029
+ if (styleProps) {
26030
+ return styleProps;
26031
+ }
26032
+ if (options.includeStyleProperties) {
26033
+ styleProps = options.includeStyleProperties;
26034
+ return styleProps;
26035
+ }
26036
+ styleProps = toArray(window.getComputedStyle(document.documentElement));
26037
+ return styleProps;
26038
+ }
26039
+ function px(node, styleProperty) {
26040
+ const win = node.ownerDocument.defaultView || window;
26041
+ const val = win.getComputedStyle(node).getPropertyValue(styleProperty);
26042
+ return val ? parseFloat(val.replace('px', '')) : 0;
26043
+ }
26044
+ function getNodeWidth(node) {
26045
+ const leftBorder = px(node, 'border-left-width');
26046
+ const rightBorder = px(node, 'border-right-width');
26047
+ return node.clientWidth + leftBorder + rightBorder;
26048
+ }
26049
+ function getNodeHeight(node) {
26050
+ const topBorder = px(node, 'border-top-width');
26051
+ const bottomBorder = px(node, 'border-bottom-width');
26052
+ return node.clientHeight + topBorder + bottomBorder;
26053
+ }
26054
+ function getImageSize(targetNode, options = {}) {
26055
+ const width = options.width || getNodeWidth(targetNode);
26056
+ const height = options.height || getNodeHeight(targetNode);
26057
+ return { width, height };
26058
+ }
26059
+ function getPixelRatio() {
26060
+ let ratio;
26061
+ let FINAL_PROCESS;
26062
+ try {
26063
+ FINAL_PROCESS = process;
26064
+ }
26065
+ catch (e) {
26066
+ // pass
26067
+ }
26068
+ const val = FINAL_PROCESS && FINAL_PROCESS.env
26069
+ ? FINAL_PROCESS.env.devicePixelRatio
26070
+ : null;
26071
+ if (val) {
26072
+ ratio = parseInt(val, 10);
26073
+ if (Number.isNaN(ratio)) {
26074
+ ratio = 1;
25971
26075
  }
25972
- // Add padding to bounds
25973
- const viewBoxX = bounds.x - padding;
25974
- const viewBoxY = bounds.y - padding;
25975
- const viewBoxWidth = bounds.width + padding * 2;
25976
- const viewBoxHeight = bounds.height + padding * 2;
25977
- // Collect all defs (markers, patterns, etc.)
25978
- const defs = this.collectDefs(allObjects, theme);
25979
- // Generate SVG elements for each object
25980
- const elements = objects
25981
- .sort((a, b) => a.zIndex - b.zIndex)
25982
- .map(obj => this.objectToSvgElement(obj, theme, imageDataUrls, rasterSafeForCanvas))
25983
- .filter(Boolean)
25984
- .join('\n ');
25985
- const xmlDecl = includeXmlDeclaration ? '<?xml version="1.0" encoding="UTF-8"?>\n' : '';
25986
- const defsContent = defs.length > 0 ? `\n <defs>\n ${defs.join('\n ')}\n </defs>` : '';
25987
- return `${xmlDecl}<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBoxX} ${viewBoxY} ${viewBoxWidth} ${viewBoxHeight}" width="${viewBoxWidth}" height="${viewBoxHeight}">${defsContent}
25988
- ${elements}
25989
- </svg>`;
25990
26076
  }
25991
- /**
25992
- * Flattens a list of objects, expanding groups into their children recursively.
25993
- * @param objects - Objects to flatten
25994
- * @returns Flat array of all objects including group children
25995
- */
25996
- static flattenObjects(objects) {
25997
- const result = [];
25998
- for (const obj of objects) {
25999
- if (KritzelClassHelper.isInstanceOf(obj, 'KritzelGroup')) {
26000
- result.push(...this.flattenObjects(obj.children));
26077
+ return ratio || window.devicePixelRatio || 1;
26078
+ }
26079
+ // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size
26080
+ const canvasDimensionLimit = 16384;
26081
+ function checkCanvasDimensions(canvas) {
26082
+ if (canvas.width > canvasDimensionLimit ||
26083
+ canvas.height > canvasDimensionLimit) {
26084
+ if (canvas.width > canvasDimensionLimit &&
26085
+ canvas.height > canvasDimensionLimit) {
26086
+ if (canvas.width > canvas.height) {
26087
+ canvas.height *= canvasDimensionLimit / canvas.width;
26088
+ canvas.width = canvasDimensionLimit;
26001
26089
  }
26002
26090
  else {
26003
- result.push(obj);
26091
+ canvas.width *= canvasDimensionLimit / canvas.height;
26092
+ canvas.height = canvasDimensionLimit;
26004
26093
  }
26005
26094
  }
26006
- return result;
26007
- }
26008
- /**
26009
- * Resolves every image referenced by the given objects (including those
26010
- * nested inside groups) to an inlined `data:` URL, keyed by object id.
26011
- *
26012
- * This is required for export flows that rasterize the SVG (e.g. PNG
26013
- * export): an SVG loaded into an `<img>` element will NOT load external
26014
- * or `blob:` resources, so a `<image href="blob:...">` renders as a
26015
- * broken-image placeholder. Embedding the bytes as a base64 `data:` URL
26016
- * makes the image self-contained and survive rasterization.
26017
- *
26018
- * Resolution is best-effort: images that fail to resolve are simply
26019
- * omitted from the map, and the SVG falls back to whatever `href` the
26020
- * synchronous path would have produced.
26021
- * @param objects - Objects to inline images for
26022
- * @param assetResolver - Resolver used to fetch asset bytes; optional
26023
- * @returns Map of image object id -> inlined `data:` URL
26024
- */
26025
- static async resolveImageDataUrls(objects, assetResolver) {
26026
- const dataUrls = new Map();
26027
- if (!objects || objects.length === 0) {
26028
- return dataUrls;
26095
+ else if (canvas.width > canvasDimensionLimit) {
26096
+ canvas.height *= canvasDimensionLimit / canvas.width;
26097
+ canvas.width = canvasDimensionLimit;
26029
26098
  }
26030
- const images = this.flattenObjects(objects).filter((obj) => KritzelClassHelper.isInstanceOf(obj, 'KritzelImage'));
26031
- await Promise.all(images.map(async (image) => {
26032
- // Legacy inline data URL is already embeddable as-is.
26033
- if (image.src?.startsWith('data:')) {
26034
- dataUrls.set(image.id, image.src);
26035
- return;
26036
- }
26037
- // A resolvedSrc that is itself a data URL can be embedded directly.
26038
- if (image.resolvedSrc?.startsWith('data:')) {
26039
- dataUrls.set(image.id, image.resolvedSrc);
26040
- return;
26041
- }
26042
- if (!image.assetId || !assetResolver) {
26043
- return;
26044
- }
26099
+ else {
26100
+ canvas.width *= canvasDimensionLimit / canvas.height;
26101
+ canvas.height = canvasDimensionLimit;
26102
+ }
26103
+ }
26104
+ }
26105
+ function createImage(url) {
26106
+ return new Promise((resolve, reject) => {
26107
+ const img = new Image();
26108
+ img.onload = () => {
26109
+ img.decode().then(() => {
26110
+ requestAnimationFrame(() => resolve(img));
26111
+ });
26112
+ };
26113
+ img.onerror = reject;
26114
+ img.crossOrigin = 'anonymous';
26115
+ img.decoding = 'async';
26116
+ img.src = url;
26117
+ });
26118
+ }
26119
+ async function svgToDataURL(svg) {
26120
+ return Promise.resolve()
26121
+ .then(() => new XMLSerializer().serializeToString(svg))
26122
+ .then(encodeURIComponent)
26123
+ .then((html) => `data:image/svg+xml;charset=utf-8,${html}`);
26124
+ }
26125
+ async function nodeToDataURL(node, width, height) {
26126
+ const xmlns = 'http://www.w3.org/2000/svg';
26127
+ const svg = document.createElementNS(xmlns, 'svg');
26128
+ const foreignObject = document.createElementNS(xmlns, 'foreignObject');
26129
+ svg.setAttribute('width', `${width}`);
26130
+ svg.setAttribute('height', `${height}`);
26131
+ svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
26132
+ foreignObject.setAttribute('width', '100%');
26133
+ foreignObject.setAttribute('height', '100%');
26134
+ foreignObject.setAttribute('x', '0');
26135
+ foreignObject.setAttribute('y', '0');
26136
+ foreignObject.setAttribute('externalResourcesRequired', 'true');
26137
+ svg.appendChild(foreignObject);
26138
+ foreignObject.appendChild(node);
26139
+ return svgToDataURL(svg);
26140
+ }
26141
+ const isInstanceOfElement = (node, instance) => {
26142
+ if (node instanceof instance)
26143
+ return true;
26144
+ const nodePrototype = Object.getPrototypeOf(node);
26145
+ if (nodePrototype === null)
26146
+ return false;
26147
+ return (nodePrototype.constructor.name === instance.name ||
26148
+ isInstanceOfElement(nodePrototype, instance));
26149
+ };
26150
+
26151
+ function formatCSSText(style) {
26152
+ const content = style.getPropertyValue('content');
26153
+ return `${style.cssText} content: '${content.replace(/'|"/g, '')}';`;
26154
+ }
26155
+ function formatCSSProperties(style, options) {
26156
+ return getStyleProperties(options)
26157
+ .map((name) => {
26158
+ const value = style.getPropertyValue(name);
26159
+ const priority = style.getPropertyPriority(name);
26160
+ return `${name}: ${value}${priority ? ' !important' : ''};`;
26161
+ })
26162
+ .join(' ');
26163
+ }
26164
+ function getPseudoElementStyle(className, pseudo, style, options) {
26165
+ const selector = `.${className}:${pseudo}`;
26166
+ const cssText = style.cssText
26167
+ ? formatCSSText(style)
26168
+ : formatCSSProperties(style, options);
26169
+ return document.createTextNode(`${selector}{${cssText}}`);
26170
+ }
26171
+ function clonePseudoElement(nativeNode, clonedNode, pseudo, options) {
26172
+ const style = window.getComputedStyle(nativeNode, pseudo);
26173
+ const content = style.getPropertyValue('content');
26174
+ if (content === '' || content === 'none') {
26175
+ return;
26176
+ }
26177
+ const className = uuid();
26178
+ try {
26179
+ clonedNode.className = `${clonedNode.className} ${className}`;
26180
+ }
26181
+ catch (err) {
26182
+ return;
26183
+ }
26184
+ const styleElement = document.createElement('style');
26185
+ styleElement.appendChild(getPseudoElementStyle(className, pseudo, style, options));
26186
+ clonedNode.appendChild(styleElement);
26187
+ }
26188
+ function clonePseudoElements(nativeNode, clonedNode, options) {
26189
+ clonePseudoElement(nativeNode, clonedNode, ':before', options);
26190
+ clonePseudoElement(nativeNode, clonedNode, ':after', options);
26191
+ }
26192
+
26193
+ const WOFF = 'application/font-woff';
26194
+ const JPEG = 'image/jpeg';
26195
+ const mimes = {
26196
+ woff: WOFF,
26197
+ woff2: WOFF,
26198
+ ttf: 'application/font-truetype',
26199
+ eot: 'application/vnd.ms-fontobject',
26200
+ png: 'image/png',
26201
+ jpg: JPEG,
26202
+ jpeg: JPEG,
26203
+ gif: 'image/gif',
26204
+ tiff: 'image/tiff',
26205
+ svg: 'image/svg+xml',
26206
+ webp: 'image/webp',
26207
+ };
26208
+ function getExtension(url) {
26209
+ const match = /\.([^./]*?)$/g.exec(url);
26210
+ return match ? match[1] : '';
26211
+ }
26212
+ function getMimeType(url) {
26213
+ const extension = getExtension(url).toLowerCase();
26214
+ return mimes[extension] || '';
26215
+ }
26216
+
26217
+ function getContentFromDataUrl(dataURL) {
26218
+ return dataURL.split(/,/)[1];
26219
+ }
26220
+ function isDataUrl(url) {
26221
+ return url.search(/^(data:)/) !== -1;
26222
+ }
26223
+ function makeDataUrl(content, mimeType) {
26224
+ return `data:${mimeType};base64,${content}`;
26225
+ }
26226
+ async function fetchAsDataURL(url, init, process) {
26227
+ const res = await fetch(url, init);
26228
+ if (res.status === 404) {
26229
+ throw new Error(`Resource "${res.url}" not found`);
26230
+ }
26231
+ const blob = await res.blob();
26232
+ return new Promise((resolve, reject) => {
26233
+ const reader = new FileReader();
26234
+ reader.onerror = reject;
26235
+ reader.onloadend = () => {
26045
26236
  try {
26046
- const blob = await assetResolver.fetchBlob(image.assetId);
26047
- dataUrls.set(image.id, await this.blobToDataUrl(blob));
26237
+ resolve(process({ res, result: reader.result }));
26048
26238
  }
26049
- catch (err) {
26050
- console.warn(`[KritzelSvgExportHelper] Failed to inline image asset ${image.assetId} for export:`, err);
26239
+ catch (error) {
26240
+ reject(error);
26051
26241
  }
26052
- }));
26053
- return dataUrls;
26242
+ };
26243
+ reader.readAsDataURL(blob);
26244
+ });
26245
+ }
26246
+ const cache = {};
26247
+ function getCacheKey(url, contentType, includeQueryParams) {
26248
+ let key = url.replace(/\?.*/, '');
26249
+ if (includeQueryParams) {
26250
+ key = url;
26054
26251
  }
26055
- /**
26056
- * Reads a Blob into a base64 `data:` URL.
26057
- * @param blob - The blob to encode
26058
- * @returns Promise resolving to the data URL string
26059
- */
26060
- static async blobToDataUrl(blob) {
26061
- // Prefer FileReader in browser-like runtimes for efficiency.
26062
- const hasFileReader = typeof FileReader !== 'undefined';
26063
- if (hasFileReader) {
26064
- try {
26065
- return await new Promise((resolve, reject) => {
26066
- const reader = new FileReader();
26067
- reader.onload = () => {
26068
- if (typeof reader.result === 'string') {
26069
- resolve(reader.result);
26070
- return;
26071
- }
26072
- reject(new Error('FileReader result is not a string'));
26073
- };
26074
- reader.onerror = () => reject(reader.error ?? new Error('Failed to read blob as data URL'));
26075
- reader.readAsDataURL(blob);
26076
- });
26077
- }
26078
- catch {
26079
- // Fall through to the ArrayBuffer-based encoder.
26252
+ // font resource
26253
+ if (/ttf|otf|eot|woff2?/i.test(key)) {
26254
+ key = key.replace(/.*\//, '');
26255
+ }
26256
+ return contentType ? `[${contentType}]${key}` : key;
26257
+ }
26258
+ async function resourceToDataURL(resourceUrl, contentType, options) {
26259
+ const cacheKey = getCacheKey(resourceUrl, contentType, options.includeQueryParams);
26260
+ if (cache[cacheKey] != null) {
26261
+ return cache[cacheKey];
26262
+ }
26263
+ // ref: https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache
26264
+ if (options.cacheBust) {
26265
+ // eslint-disable-next-line no-param-reassign
26266
+ resourceUrl += (/\?/.test(resourceUrl) ? '&' : '?') + new Date().getTime();
26267
+ }
26268
+ let dataURL;
26269
+ try {
26270
+ const content = await fetchAsDataURL(resourceUrl, options.fetchRequestInit, ({ res, result }) => {
26271
+ if (!contentType) {
26272
+ // eslint-disable-next-line no-param-reassign
26273
+ contentType = res.headers.get('Content-Type') || '';
26080
26274
  }
26081
- }
26082
- const bytes = new Uint8Array(await blob.arrayBuffer());
26083
- const mimeType = blob.type || 'application/octet-stream';
26084
- return `data:${mimeType};base64,${this.uint8ToBase64(bytes)}`;
26275
+ return getContentFromDataUrl(result);
26276
+ });
26277
+ dataURL = makeDataUrl(content, contentType);
26085
26278
  }
26086
- /**
26087
- * Encodes a byte array to base64 across browser and Node-like runtimes.
26088
- */
26089
- static uint8ToBase64(bytes) {
26090
- if (typeof Buffer !== 'undefined') {
26091
- return Buffer.from(bytes).toString('base64');
26279
+ catch (error) {
26280
+ dataURL = options.imagePlaceholder || '';
26281
+ let msg = `Failed to fetch resource: ${resourceUrl}`;
26282
+ if (error) {
26283
+ msg = typeof error === 'string' ? error : error.message;
26092
26284
  }
26093
- let binary = '';
26094
- const chunkSize = 0x8000;
26095
- for (let i = 0; i < bytes.length; i += chunkSize) {
26096
- const chunk = bytes.subarray(i, Math.min(i + chunkSize, bytes.length));
26097
- binary += String.fromCharCode(...chunk);
26098
- }
26099
- if (typeof btoa === 'function') {
26100
- return btoa(binary);
26101
- }
26102
- throw new Error('No base64 encoder available in this runtime');
26103
- }
26104
- /**
26105
- * Collects SVG defs (markers, patterns, etc.) needed by the objects.
26106
- * @param objects - Objects to collect defs for
26107
- * @param theme - Theme for color resolution
26108
- * @returns Array of def element strings
26109
- */
26110
- static collectDefs(objects, theme) {
26111
- const defs = [];
26112
- for (const obj of objects) {
26113
- if (KritzelClassHelper.isInstanceOf(obj, 'KritzelLine')) {
26114
- if (obj.hasStartArrow) {
26115
- defs.push(this.generateArrowMarker(obj, 'start', theme));
26116
- }
26117
- if (obj.hasEndArrow) {
26118
- defs.push(this.generateArrowMarker(obj, 'end', theme));
26119
- }
26120
- }
26121
- }
26122
- return defs;
26123
- }
26124
- /**
26125
- * Generates an SVG marker element for line arrows.
26126
- * @param line - The line object
26127
- * @param end - Which end of the line ('start' or 'end')
26128
- * @param theme - Theme for color resolution
26129
- * @returns SVG marker element string
26130
- */
26131
- static generateArrowMarker(line, end, _theme) {
26132
- const markerId = end === 'start' ? line.startMarkerId : line.endMarkerId;
26133
- const size = line.getArrowSize(end);
26134
- const fill = line.getArrowFill(end);
26135
- const style = end === 'start' ? line.arrows?.start?.style : line.arrows?.end?.style;
26136
- const path = line.getArrowPath(style);
26137
- const orient = end === 'start' ? 'auto-start-reverse' : 'auto';
26138
- const refX = 0;
26139
- const refY = size / 2;
26140
- return `<marker id="${markerId}" markerWidth="${size}" markerHeight="${size}" refX="${refX}" refY="${refY}" orient="${orient}" markerUnits="userSpaceOnUse">
26141
- <path d="${path}" fill="${fill}" transform="scale(${size / 10})"/>
26142
- </marker>`;
26143
- }
26144
- /**
26145
- * Converts a Kritzel object to its SVG element representation.
26146
- * @param object - The object to convert
26147
- * @param theme - Theme for color resolution
26148
- * @param imageDataUrls - Optional map of image id -> inlined data URL
26149
- * @returns SVG element string, or empty string if object type is not supported
26150
- */
26151
- static objectToSvgElement(object, theme, imageDataUrls, rasterSafeForCanvas = false) {
26152
- if (KritzelClassHelper.isInstanceOf(object, 'KritzelPath')) {
26153
- return this.pathToSvg(object, theme);
26154
- }
26155
- if (KritzelClassHelper.isInstanceOf(object, 'KritzelLine')) {
26156
- return this.lineToSvg(object, theme);
26157
- }
26158
- if (KritzelClassHelper.isInstanceOf(object, 'KritzelShape')) {
26159
- return this.shapeToSvg(object, theme);
26160
- }
26161
- if (KritzelClassHelper.isInstanceOf(object, 'KritzelText')) {
26162
- return this.textToSvg(object, theme, rasterSafeForCanvas);
26163
- }
26164
- if (KritzelClassHelper.isInstanceOf(object, 'KritzelImage')) {
26165
- return this.imageToSvg(object, imageDataUrls);
26166
- }
26167
- if (KritzelClassHelper.isInstanceOf(object, 'KritzelCustomElement')) {
26168
- return this.customElementToSvg(object, rasterSafeForCanvas);
26169
- }
26170
- if (KritzelClassHelper.isInstanceOf(object, 'KritzelGroup')) {
26171
- return this.groupToSvg(object, theme, imageDataUrls, rasterSafeForCanvas);
26172
- }
26173
- return '';
26174
- }
26175
- /**
26176
- * Converts a KritzelPath to SVG.
26177
- * @param path - The path object
26178
- * @param theme - Theme for color resolution
26179
- * @returns SVG element string
26180
- */
26181
- static pathToSvg(path, theme) {
26182
- const transform = this.buildTransform(path);
26183
- const fill = KritzelColorHelper.resolveThemeColor(path.fill, theme);
26184
- const stroke = KritzelColorHelper.resolveThemeColor(path.stroke, theme);
26185
- const opacity = path.opacity !== 1 ? ` opacity="${path.opacity}"` : '';
26186
- const scale = this.getObjectScale(path);
26187
- // Path needs to be wrapped in a g with translation since path.d uses local coordinates
26188
- return `<g transform="${transform}"${opacity}>
26189
- <svg viewBox="${path.viewBox}" width="${path.totalWidth / scale}" height="${path.totalHeight / scale}" overflow="visible">
26190
- <path d="${path.d}" fill="${fill || 'none'}" stroke="${stroke || 'none'}"${path.strokeWidth ? ` stroke-width="${path.strokeWidth}"` : ''}/>
26191
- </svg>
26192
- </g>`;
26193
- }
26194
- /**
26195
- * Converts a KritzelLine to SVG.
26196
- * @param line - The line object
26197
- * @param theme - Theme for color resolution
26198
- * @returns SVG element string
26199
- */
26200
- static lineToSvg(line, theme) {
26201
- const transform = this.buildTransform(line);
26202
- const stroke = KritzelColorHelper.resolveThemeColor(line.stroke, theme);
26203
- const opacity = line.opacity !== 1 ? ` opacity="${line.opacity}"` : '';
26204
- const scale = this.getObjectScale(line);
26205
- const markerStart = line.hasStartArrow ? ` marker-start="url(#${line.startMarkerId})"` : '';
26206
- const markerEnd = line.hasEndArrow ? ` marker-end="url(#${line.endMarkerId})"` : '';
26207
- return `<g transform="${transform}"${opacity}>
26208
- <svg viewBox="${line.viewBox}" width="${line.totalWidth / scale}" height="${line.totalHeight / scale}" overflow="visible">
26209
- <path d="${line.d}" fill="none" stroke="${stroke}" stroke-width="${line.strokeWidth}" stroke-linecap="round"${markerStart}${markerEnd}/>
26210
- </svg>
26211
- </g>`;
26212
- }
26213
- /**
26214
- * Converts a KritzelShape to SVG.
26215
- * @param shape - The shape object
26216
- * @param theme - Theme for color resolution
26217
- * @returns SVG element string
26218
- */
26219
- static shapeToSvg(shape, theme) {
26220
- const transform = this.buildTransform(shape);
26221
- const fill = KritzelColorHelper.resolveThemeColor(shape.fillColor, theme);
26222
- const stroke = KritzelColorHelper.resolveThemeColor(shape.strokeColor, theme);
26223
- const opacity = shape.opacity !== 1 ? ` opacity="${shape.opacity}"` : '';
26224
- const scale = this.getObjectScale(shape);
26225
- // Get the SVG path for the shape
26226
- const pathD = shape.getSvgPath();
26227
- // Handle text content if present
26228
- let textContent = '';
26229
- if (shape.editor && !shape.editor.state.doc.textContent.trim()) ;
26230
- else if (shape.content) {
26231
- const textHtml = this.prosemirrorToHtml(shape.content, shape, theme);
26232
- if (textHtml) {
26233
- const fontColor = KritzelColorHelper.resolveThemeColor(shape.fontColor, theme);
26234
- textContent = `
26235
- <foreignObject x="${shape.x}" y="${shape.y}" width="${shape.width}" height="${shape.height}">
26236
- <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;">
26237
- ${textHtml}
26238
- </div>
26239
- </foreignObject>`;
26240
- }
26241
- }
26242
- return `<g transform="${transform}"${opacity}>
26243
- <svg viewBox="${shape.viewBox}" width="${shape.totalWidth / scale}" height="${shape.totalHeight / scale}" overflow="visible" preserveAspectRatio="none">
26244
- <path d="${pathD}" fill="${fill || 'transparent'}" stroke="${stroke}" stroke-width="${shape.strokeWidth}"/>${textContent}
26245
- </svg>
26246
- </g>`;
26247
- }
26248
- /**
26249
- * Converts a KritzelText to SVG using foreignObject for rich text support.
26250
- * @param text - The text object
26251
- * @param theme - Theme for color resolution
26252
- * @returns SVG element string
26253
- */
26254
- static textToSvg(text, theme, rasterSafeForCanvas = false) {
26255
- if (rasterSafeForCanvas) {
26256
- return this.textToSvgRasterSafe(text, theme);
26257
- }
26258
- const transform = this.buildTransform(text);
26259
- const opacity = text.opacity !== 1 ? ` opacity="${text.opacity}"` : '';
26260
- const fontColor = KritzelColorHelper.resolveThemeColor(text.fontColor, theme);
26261
- const bgColor = KritzelColorHelper.resolveThemeColor(text.backgroundColor, theme);
26262
- const scale = this.getObjectScale(text);
26263
- // Convert ProseMirror content to HTML
26264
- const htmlContent = this.prosemirrorToHtml(text.content, text, theme);
26265
- // Calculate dimensions in SVG world units (world units = CSS px in a 1:1 SVG).
26266
- const width = text.totalWidth / scale;
26267
- const height = text.totalHeight / scale;
26268
- // Convert the font size from pt to SVG world units (px equivalent).
26269
- // text.fontSize is stored in CSS pt; 1 pt = 4/3 px at 96 dpi.
26270
- // We also bake in scaleFactor (user resize) and divide by the object scale so
26271
- // that the rendered font aligns with the world-unit dimensions above.
26272
- // The CSS transform: scale(scaleFactor) is removed from the div because the
26273
- // adjusted font-size already accounts for it – keeping both would double-scale
26274
- // the content and overflow the foreignObject.
26275
- const svgFontSizePx = (text.fontSize * (4 / 3) * (text.scaleFactor || 1) / scale).toFixed(4);
26276
- return `<g transform="${transform}"${opacity}>
26277
- <foreignObject x="0" y="0" width="${width}" height="${height}" overflow="visible">
26278
- <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>
26279
- </foreignObject>
26280
- </g>`;
26281
- }
26282
- /**
26283
- * Converts a KritzelText to SVG using pure SVG text primitives.
26284
- * This path avoids `<foreignObject>`, which taints canvases when the
26285
- * generated SVG is rasterized through `<img>` and `drawImage`.
26286
- */
26287
- static textToSvgRasterSafe(text, theme) {
26288
- const transform = this.buildTransform(text);
26289
- const opacity = text.opacity !== 1 ? ` opacity="${text.opacity}"` : '';
26290
- const fontColor = KritzelColorHelper.resolveThemeColor(text.fontColor, theme);
26291
- const bgColor = KritzelColorHelper.resolveThemeColor(text.backgroundColor, theme);
26292
- const scale = this.getObjectScale(text);
26293
- const width = text.totalWidth / scale;
26294
- const height = text.totalHeight / scale;
26295
- const lines = this.prosemirrorToPlainTextLines(text.content);
26296
- const resolvedLines = lines.length > 0 ? lines : [''];
26297
- // Convert the font size from pt to SVG world units (px equivalent).
26298
- // text.fontSize is stored in CSS pt; 1 pt = 4/3 px at 96 dpi.
26299
- // We also bake in scaleFactor (user resize) and divide by the object scale so
26300
- // that the rendered lines align with the world-unit bounding box dimensions above.
26301
- const resolvedFontSizePx = Math.max(1, text.fontSize * (4 / 3) * (text.scaleFactor || 1) / scale);
26302
- const lineHeight = resolvedFontSizePx * 1.2;
26303
- const startY = resolvedFontSizePx;
26304
- const bgRect = bgColor && bgColor !== 'transparent'
26305
- ? `<rect x="0" y="0" width="${width}" height="${height}" fill="${bgColor}"/>`
26306
- : '';
26307
- const tspans = resolvedLines
26308
- .map((line, index) => {
26309
- const y = startY + index * lineHeight;
26310
- return `<tspan x="0" y="${y}" xml:space="preserve">${this.escapeHtml(line)}</tspan>`;
26311
- })
26312
- .join('');
26313
- return `<g transform="${transform}"${opacity}>
26314
- ${bgRect}
26315
- <text font-family="${this.escapeHtml(text.fontFamily)}" font-size="${resolvedFontSizePx}" fill="${fontColor}">${tspans}</text>
26316
- </g>`;
26317
- }
26318
- /**
26319
- * Converts a KritzelImage to SVG.
26320
- *
26321
- * Prefers an inlined `data:` URL from `imageDataUrls` (keyed by object
26322
- * id) when provided, so the embedded bytes survive rasterization to
26323
- * PNG. Falls back to `resolvedSrc` (populated by the renderer via the
26324
- * asset resolver) and finally the legacy inline `src` field for
26325
- * documents persisted before the asset layer existed.
26326
- *
26327
- * Note: a `blob:` `resolvedSrc` renders correctly on-canvas but becomes
26328
- * a broken-image placeholder once the SVG is loaded into an `<img>` and
26329
- * drawn to a canvas. Callers that rasterize the SVG must pass
26330
- * `imageDataUrls` (see {@link KritzelSvgExportHelper.resolveImageDataUrls}).
26331
- * @param image - The image object
26332
- * @param imageDataUrls - Optional map of image id -> inlined data URL
26333
- * @returns SVG element string
26334
- */
26335
- static imageToSvg(image, imageDataUrls) {
26336
- const transform = this.buildTransform(image);
26337
- const opacity = image.opacity !== 1 ? ` opacity="${image.opacity}"` : '';
26338
- const scale = this.getObjectScale(image);
26339
- // Calculate dimensions
26340
- const width = image.totalWidth / scale;
26341
- const height = image.totalHeight / scale;
26342
- const href = imageDataUrls?.get(image.id) || image.resolvedSrc || image.src || '';
26343
- return `<g transform="${transform}"${opacity}>
26344
- <image href="${href}" x="0" y="0" width="${width}" height="${height}" preserveAspectRatio="xMidYMid meet"/>
26345
- </g>`;
26346
- }
26347
- /**
26348
- * Converts a KritzelCustomElement to SVG.
26349
- * SVG mode: embeds HTML in foreignObject (best-effort).
26350
- * PNG/raster mode: renders placeholder rect with warning.
26351
- * @param obj - The custom element object
26352
- * @param rasterSafeForCanvas - If true, render placeholder (for PNG export)
26353
- * @returns SVG element string
26354
- */
26355
- static customElementToSvg(obj, rasterSafeForCanvas = false) {
26356
- const transform = this.buildTransform(obj);
26357
- const opacity = obj.opacity !== 1 ? ` opacity="${obj.opacity}"` : '';
26358
- const scale = this.getObjectScale(obj);
26359
- const width = obj.totalWidth / scale;
26360
- const height = obj.totalHeight / scale;
26361
- if (rasterSafeForCanvas) {
26362
- // PNG export: render placeholder rect (foreignObject doesn't rasterize to PNG well)
26363
- console.warn('[KritzelCustomElement SVG Export] Custom elements do not serialize well to PNG. Use SVG export or screenshot instead.');
26364
- return `<g transform="${transform}"${opacity}>
26365
- <rect x="0" y="0" width="${width}" height="${height}" fill="#f0f0f0" stroke="#999" stroke-width="1" />
26366
- <text x="${width / 2}" y="${height / 2}" text-anchor="middle" dominant-baseline="middle" font-size="12" fill="#999">[Custom Element]</text>
26367
- </g>`;
26368
- }
26369
- // SVG export: use foreignObject to embed HTML
26370
- const html = obj.element?.outerHTML || '<div style="border:1px dashed #ccc">Empty</div>';
26371
- return `<g transform="${transform}"${opacity}>
26372
- <foreignObject x="0" y="0" width="${width}" height="${height}">
26373
- <div xmlns="http://www.w3.org/1999/xhtml" style="width:100%;height:100%;overflow:hidden;">
26374
- ${html}
26375
- </div>
26376
- </foreignObject>
26377
- </g>`;
26378
- }
26379
- /**
26380
- * Converts a KritzelGroup to SVG by recursively converting children.
26381
- * @param group - The group object
26382
- * @param theme - Theme for color resolution
26383
- * @param imageDataUrls - Optional map of image id -> inlined data URL
26384
- * @returns SVG element string
26385
- */
26386
- static groupToSvg(group, theme, imageDataUrls, rasterSafeForCanvas = false) {
26387
- const children = group.children
26388
- .sort((a, b) => a.zIndex - b.zIndex)
26389
- .map(child => this.objectToSvgElement(child, theme, imageDataUrls, rasterSafeForCanvas))
26390
- .filter(Boolean)
26391
- .join('\n ');
26392
- if (!children) {
26393
- return '';
26394
- }
26395
- const opacity = group.opacity !== 1 ? ` opacity="${group.opacity}"` : '';
26396
- return `<g${opacity}>
26397
- ${children}
26398
- </g>`;
26399
- }
26400
- /**
26401
- * Builds a transform attribute string for an object.
26402
- * Combines translation and rotation.
26403
- * @param object - The object to build transform for
26404
- * @returns Transform attribute value string
26405
- */
26406
- static buildTransform(object) {
26407
- const transforms = [];
26408
- const scale = this.getObjectScale(object);
26409
- // Translation
26410
- transforms.push(`translate(${object.translateX}, ${object.translateY})`);
26411
- // Rotation around center
26412
- if (object.rotation !== 0) {
26413
- const centerX = object.totalWidth / 2 / scale;
26414
- const centerY = object.totalHeight / 2 / scale;
26415
- const degrees = object.rotation * (180 / Math.PI);
26416
- transforms.push(`rotate(${degrees}, ${centerX}, ${centerY})`);
26417
- }
26418
- return transforms.join(' ');
26419
- }
26420
- /**
26421
- * Returns a safe non-zero scale for export geometry calculations.
26422
- */
26423
- static getObjectScale(object) {
26424
- const scale = object.scale ?? 1;
26425
- return scale === 0 ? 1 : scale;
26426
- }
26427
- /**
26428
- * Converts ProseMirror JSON content to HTML string.
26429
- * @param content - ProseMirror document JSON
26430
- * @param context - The text or shape object for styling context
26431
- * @param theme - Theme for color resolution
26432
- * @returns HTML string
26433
- */
26434
- static prosemirrorToHtml(content, _context, theme) {
26435
- if (!content || !content.content) {
26436
- return '';
26437
- }
26438
- return content.content
26439
- .map((node) => this.nodeToHtml(node, theme))
26440
- .join('');
26441
- }
26442
- /**
26443
- * Converts a ProseMirror node to HTML.
26444
- * @param node - ProseMirror node JSON
26445
- * @param theme - Theme for color resolution
26446
- * @returns HTML string
26447
- */
26448
- static nodeToHtml(node, theme) {
26449
- if (!node)
26450
- return '';
26451
- switch (node.type) {
26452
- case 'paragraph':
26453
- const pContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
26454
- return `<p style="margin: 0;">${pContent || '<br/>'}</p>`;
26455
- case 'text':
26456
- let text = this.escapeHtml(node.text || '');
26457
- if (node.marks) {
26458
- for (const mark of node.marks) {
26459
- text = this.applyMark(text, mark, theme);
26460
- }
26461
- }
26462
- return text;
26463
- case 'hard_break':
26464
- return '<br/>';
26465
- case 'bullet_list':
26466
- const blContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
26467
- return `<ul style="margin: 0; padding-left: 1.5em;">${blContent}</ul>`;
26468
- case 'ordered_list':
26469
- const olContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
26470
- return `<ol style="margin: 0; padding-left: 1.5em;">${olContent}</ol>`;
26471
- case 'list_item':
26472
- const liContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
26473
- return `<li>${liContent}</li>`;
26474
- default:
26475
- // For unknown node types, try to render content
26476
- if (node.content) {
26477
- return node.content.map((n) => this.nodeToHtml(n, theme)).join('');
26478
- }
26479
- return '';
26480
- }
26481
- }
26482
- /**
26483
- * Converts ProseMirror JSON content to plain-text lines.
26484
- * Used by raster-safe SVG export, which cannot depend on HTML/foreignObject.
26485
- */
26486
- static prosemirrorToPlainTextLines(content) {
26487
- if (!content || !Array.isArray(content.content)) {
26488
- return [];
26489
- }
26490
- const plainText = content.content
26491
- .map((node) => this.nodeToPlainText(node))
26492
- .join('')
26493
- .replace(/\r\n/g, '\n')
26494
- .replace(/\n+$/, '');
26495
- if (!plainText) {
26496
- return [];
26497
- }
26498
- return plainText.split('\n');
26499
- }
26500
- /**
26501
- * Converts a ProseMirror node tree into plain text.
26502
- */
26503
- static nodeToPlainText(node) {
26504
- if (!node) {
26505
- return '';
26506
- }
26507
- switch (node.type) {
26508
- case 'text':
26509
- return node.text || '';
26510
- case 'hard_break':
26511
- return '\n';
26512
- case 'paragraph': {
26513
- const paragraph = Array.isArray(node.content)
26514
- ? node.content.map((child) => this.nodeToPlainText(child)).join('')
26515
- : '';
26516
- return `${paragraph}\n`;
26517
- }
26518
- case 'list_item': {
26519
- const item = Array.isArray(node.content)
26520
- ? node.content.map((child) => this.nodeToPlainText(child)).join('')
26521
- : '';
26522
- return `- ${item.trim()}\n`;
26523
- }
26524
- default:
26525
- return Array.isArray(node.content)
26526
- ? node.content.map((child) => this.nodeToPlainText(child)).join('')
26527
- : '';
26528
- }
26529
- }
26530
- /**
26531
- * Applies a ProseMirror mark to text.
26532
- * @param text - The text to wrap
26533
- * @param mark - The mark to apply
26534
- * @param theme - Theme for color resolution
26535
- * @returns Wrapped HTML string
26536
- */
26537
- static applyMark(text, mark, _theme) {
26538
- switch (mark.type) {
26539
- case 'bold':
26540
- case 'strong':
26541
- return `<strong>${text}</strong>`;
26542
- case 'italic':
26543
- case 'em':
26544
- return `<em>${text}</em>`;
26545
- case 'underline':
26546
- return `<u>${text}</u>`;
26547
- case 'strike':
26548
- return `<s>${text}</s>`;
26549
- case 'code':
26550
- return `<code>${text}</code>`;
26551
- case 'link':
26552
- const href = mark.attrs?.href || '#';
26553
- return `<a href="${this.escapeHtml(href)}">${text}</a>`;
26554
- default:
26555
- return text;
26556
- }
26557
- }
26558
- /**
26559
- * Escapes HTML special characters.
26560
- * @param str - String to escape
26561
- * @returns Escaped string
26562
- */
26563
- static escapeHtml(str) {
26564
- return str
26565
- .replace(/&/g, '&amp;')
26566
- .replace(/</g, '&lt;')
26567
- .replace(/>/g, '&gt;')
26568
- .replace(/"/g, '&quot;')
26569
- .replace(/'/g, '&#039;');
26570
- }
26571
- }
26572
-
26573
- function resolveUrl(url, baseUrl) {
26574
- // url is absolute already
26575
- if (url.match(/^[a-z]+:\/\//i)) {
26576
- return url;
26577
- }
26578
- // url is absolute already, without protocol
26579
- if (url.match(/^\/\//)) {
26580
- return window.location.protocol + url;
26581
- }
26582
- // dataURI, mailto:, tel:, etc.
26583
- if (url.match(/^[a-z]+:/i)) {
26584
- return url;
26585
- }
26586
- const doc = document.implementation.createHTMLDocument();
26587
- const base = doc.createElement('base');
26588
- const a = doc.createElement('a');
26589
- doc.head.appendChild(base);
26590
- doc.body.appendChild(a);
26591
- if (baseUrl) {
26592
- base.href = baseUrl;
26593
- }
26594
- a.href = url;
26595
- return a.href;
26596
- }
26597
- const uuid = (() => {
26598
- // generate uuid for className of pseudo elements.
26599
- // We should not use GUIDs, otherwise pseudo elements sometimes cannot be captured.
26600
- let counter = 0;
26601
- // ref: http://stackoverflow.com/a/6248722/2519373
26602
- const random = () =>
26603
- // eslint-disable-next-line no-bitwise
26604
- `0000${((Math.random() * 36 ** 4) << 0).toString(36)}`.slice(-4);
26605
- return () => {
26606
- counter += 1;
26607
- return `u${random()}${counter}`;
26608
- };
26609
- })();
26610
- function toArray(arrayLike) {
26611
- const arr = [];
26612
- for (let i = 0, l = arrayLike.length; i < l; i++) {
26613
- arr.push(arrayLike[i]);
26614
- }
26615
- return arr;
26616
- }
26617
- let styleProps = null;
26618
- function getStyleProperties(options = {}) {
26619
- if (styleProps) {
26620
- return styleProps;
26621
- }
26622
- if (options.includeStyleProperties) {
26623
- styleProps = options.includeStyleProperties;
26624
- return styleProps;
26625
- }
26626
- styleProps = toArray(window.getComputedStyle(document.documentElement));
26627
- return styleProps;
26628
- }
26629
- function px(node, styleProperty) {
26630
- const win = node.ownerDocument.defaultView || window;
26631
- const val = win.getComputedStyle(node).getPropertyValue(styleProperty);
26632
- return val ? parseFloat(val.replace('px', '')) : 0;
26633
- }
26634
- function getNodeWidth(node) {
26635
- const leftBorder = px(node, 'border-left-width');
26636
- const rightBorder = px(node, 'border-right-width');
26637
- return node.clientWidth + leftBorder + rightBorder;
26638
- }
26639
- function getNodeHeight(node) {
26640
- const topBorder = px(node, 'border-top-width');
26641
- const bottomBorder = px(node, 'border-bottom-width');
26642
- return node.clientHeight + topBorder + bottomBorder;
26643
- }
26644
- function getImageSize(targetNode, options = {}) {
26645
- const width = options.width || getNodeWidth(targetNode);
26646
- const height = options.height || getNodeHeight(targetNode);
26647
- return { width, height };
26648
- }
26649
- function getPixelRatio() {
26650
- let ratio;
26651
- let FINAL_PROCESS;
26652
- try {
26653
- FINAL_PROCESS = process;
26654
- }
26655
- catch (e) {
26656
- // pass
26657
- }
26658
- const val = FINAL_PROCESS && FINAL_PROCESS.env
26659
- ? FINAL_PROCESS.env.devicePixelRatio
26660
- : null;
26661
- if (val) {
26662
- ratio = parseInt(val, 10);
26663
- if (Number.isNaN(ratio)) {
26664
- ratio = 1;
26665
- }
26666
- }
26667
- return ratio || window.devicePixelRatio || 1;
26668
- }
26669
- // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size
26670
- const canvasDimensionLimit = 16384;
26671
- function checkCanvasDimensions(canvas) {
26672
- if (canvas.width > canvasDimensionLimit ||
26673
- canvas.height > canvasDimensionLimit) {
26674
- if (canvas.width > canvasDimensionLimit &&
26675
- canvas.height > canvasDimensionLimit) {
26676
- if (canvas.width > canvas.height) {
26677
- canvas.height *= canvasDimensionLimit / canvas.width;
26678
- canvas.width = canvasDimensionLimit;
26679
- }
26680
- else {
26681
- canvas.width *= canvasDimensionLimit / canvas.height;
26682
- canvas.height = canvasDimensionLimit;
26683
- }
26684
- }
26685
- else if (canvas.width > canvasDimensionLimit) {
26686
- canvas.height *= canvasDimensionLimit / canvas.width;
26687
- canvas.width = canvasDimensionLimit;
26688
- }
26689
- else {
26690
- canvas.width *= canvasDimensionLimit / canvas.height;
26691
- canvas.height = canvasDimensionLimit;
26692
- }
26693
- }
26694
- }
26695
- function createImage(url) {
26696
- return new Promise((resolve, reject) => {
26697
- const img = new Image();
26698
- img.onload = () => {
26699
- img.decode().then(() => {
26700
- requestAnimationFrame(() => resolve(img));
26701
- });
26702
- };
26703
- img.onerror = reject;
26704
- img.crossOrigin = 'anonymous';
26705
- img.decoding = 'async';
26706
- img.src = url;
26707
- });
26708
- }
26709
- async function svgToDataURL(svg) {
26710
- return Promise.resolve()
26711
- .then(() => new XMLSerializer().serializeToString(svg))
26712
- .then(encodeURIComponent)
26713
- .then((html) => `data:image/svg+xml;charset=utf-8,${html}`);
26714
- }
26715
- async function nodeToDataURL(node, width, height) {
26716
- const xmlns = 'http://www.w3.org/2000/svg';
26717
- const svg = document.createElementNS(xmlns, 'svg');
26718
- const foreignObject = document.createElementNS(xmlns, 'foreignObject');
26719
- svg.setAttribute('width', `${width}`);
26720
- svg.setAttribute('height', `${height}`);
26721
- svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
26722
- foreignObject.setAttribute('width', '100%');
26723
- foreignObject.setAttribute('height', '100%');
26724
- foreignObject.setAttribute('x', '0');
26725
- foreignObject.setAttribute('y', '0');
26726
- foreignObject.setAttribute('externalResourcesRequired', 'true');
26727
- svg.appendChild(foreignObject);
26728
- foreignObject.appendChild(node);
26729
- return svgToDataURL(svg);
26730
- }
26731
- const isInstanceOfElement = (node, instance) => {
26732
- if (node instanceof instance)
26733
- return true;
26734
- const nodePrototype = Object.getPrototypeOf(node);
26735
- if (nodePrototype === null)
26736
- return false;
26737
- return (nodePrototype.constructor.name === instance.name ||
26738
- isInstanceOfElement(nodePrototype, instance));
26739
- };
26740
-
26741
- function formatCSSText(style) {
26742
- const content = style.getPropertyValue('content');
26743
- return `${style.cssText} content: '${content.replace(/'|"/g, '')}';`;
26744
- }
26745
- function formatCSSProperties(style, options) {
26746
- return getStyleProperties(options)
26747
- .map((name) => {
26748
- const value = style.getPropertyValue(name);
26749
- const priority = style.getPropertyPriority(name);
26750
- return `${name}: ${value}${priority ? ' !important' : ''};`;
26751
- })
26752
- .join(' ');
26753
- }
26754
- function getPseudoElementStyle(className, pseudo, style, options) {
26755
- const selector = `.${className}:${pseudo}`;
26756
- const cssText = style.cssText
26757
- ? formatCSSText(style)
26758
- : formatCSSProperties(style, options);
26759
- return document.createTextNode(`${selector}{${cssText}}`);
26760
- }
26761
- function clonePseudoElement(nativeNode, clonedNode, pseudo, options) {
26762
- const style = window.getComputedStyle(nativeNode, pseudo);
26763
- const content = style.getPropertyValue('content');
26764
- if (content === '' || content === 'none') {
26765
- return;
26766
- }
26767
- const className = uuid();
26768
- try {
26769
- clonedNode.className = `${clonedNode.className} ${className}`;
26770
- }
26771
- catch (err) {
26772
- return;
26773
- }
26774
- const styleElement = document.createElement('style');
26775
- styleElement.appendChild(getPseudoElementStyle(className, pseudo, style, options));
26776
- clonedNode.appendChild(styleElement);
26777
- }
26778
- function clonePseudoElements(nativeNode, clonedNode, options) {
26779
- clonePseudoElement(nativeNode, clonedNode, ':before', options);
26780
- clonePseudoElement(nativeNode, clonedNode, ':after', options);
26781
- }
26782
-
26783
- const WOFF = 'application/font-woff';
26784
- const JPEG = 'image/jpeg';
26785
- const mimes = {
26786
- woff: WOFF,
26787
- woff2: WOFF,
26788
- ttf: 'application/font-truetype',
26789
- eot: 'application/vnd.ms-fontobject',
26790
- png: 'image/png',
26791
- jpg: JPEG,
26792
- jpeg: JPEG,
26793
- gif: 'image/gif',
26794
- tiff: 'image/tiff',
26795
- svg: 'image/svg+xml',
26796
- webp: 'image/webp',
26797
- };
26798
- function getExtension(url) {
26799
- const match = /\.([^./]*?)$/g.exec(url);
26800
- return match ? match[1] : '';
26801
- }
26802
- function getMimeType(url) {
26803
- const extension = getExtension(url).toLowerCase();
26804
- return mimes[extension] || '';
26805
- }
26806
-
26807
- function getContentFromDataUrl(dataURL) {
26808
- return dataURL.split(/,/)[1];
26809
- }
26810
- function isDataUrl(url) {
26811
- return url.search(/^(data:)/) !== -1;
26812
- }
26813
- function makeDataUrl(content, mimeType) {
26814
- return `data:${mimeType};base64,${content}`;
26815
- }
26816
- async function fetchAsDataURL(url, init, process) {
26817
- const res = await fetch(url, init);
26818
- if (res.status === 404) {
26819
- throw new Error(`Resource "${res.url}" not found`);
26820
- }
26821
- const blob = await res.blob();
26822
- return new Promise((resolve, reject) => {
26823
- const reader = new FileReader();
26824
- reader.onerror = reject;
26825
- reader.onloadend = () => {
26826
- try {
26827
- resolve(process({ res, result: reader.result }));
26828
- }
26829
- catch (error) {
26830
- reject(error);
26831
- }
26832
- };
26833
- reader.readAsDataURL(blob);
26834
- });
26835
- }
26836
- const cache = {};
26837
- function getCacheKey(url, contentType, includeQueryParams) {
26838
- let key = url.replace(/\?.*/, '');
26839
- if (includeQueryParams) {
26840
- key = url;
26841
- }
26842
- // font resource
26843
- if (/ttf|otf|eot|woff2?/i.test(key)) {
26844
- key = key.replace(/.*\//, '');
26845
- }
26846
- return contentType ? `[${contentType}]${key}` : key;
26847
- }
26848
- async function resourceToDataURL(resourceUrl, contentType, options) {
26849
- const cacheKey = getCacheKey(resourceUrl, contentType, options.includeQueryParams);
26850
- if (cache[cacheKey] != null) {
26851
- return cache[cacheKey];
26852
- }
26853
- // ref: https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache
26854
- if (options.cacheBust) {
26855
- // eslint-disable-next-line no-param-reassign
26856
- resourceUrl += (/\?/.test(resourceUrl) ? '&' : '?') + new Date().getTime();
26857
- }
26858
- let dataURL;
26859
- try {
26860
- const content = await fetchAsDataURL(resourceUrl, options.fetchRequestInit, ({ res, result }) => {
26861
- if (!contentType) {
26862
- // eslint-disable-next-line no-param-reassign
26863
- contentType = res.headers.get('Content-Type') || '';
26864
- }
26865
- return getContentFromDataUrl(result);
26866
- });
26867
- dataURL = makeDataUrl(content, contentType);
26868
- }
26869
- catch (error) {
26870
- dataURL = options.imagePlaceholder || '';
26871
- let msg = `Failed to fetch resource: ${resourceUrl}`;
26872
- if (error) {
26873
- msg = typeof error === 'string' ? error : error.message;
26874
- }
26875
- if (msg) {
26876
- console.warn(msg);
26285
+ if (msg) {
26286
+ console.warn(msg);
26877
26287
  }
26878
26288
  }
26879
26289
  cache[cacheKey] = dataURL;
@@ -27407,156 +26817,808 @@ async function embedWebFonts(clonedNode, options) {
27407
26817
  clonedNode.appendChild(styleNode);
27408
26818
  }
27409
26819
  }
27410
- }
27411
-
27412
- async function toSvg(node, options = {}) {
27413
- const { width, height } = getImageSize(node, options);
27414
- const clonedNode = (await cloneNode(node, options, true));
27415
- await embedWebFonts(clonedNode, options);
27416
- await embedImages(clonedNode, options);
27417
- applyStyle(clonedNode, options);
27418
- const datauri = await nodeToDataURL(clonedNode, width, height);
27419
- return datauri;
27420
- }
27421
- async function toCanvas(node, options = {}) {
27422
- const { width, height } = getImageSize(node, options);
27423
- const svg = await toSvg(node, options);
27424
- const img = await createImage(svg);
27425
- const canvas = document.createElement('canvas');
27426
- const context = canvas.getContext('2d');
27427
- const ratio = options.pixelRatio || getPixelRatio();
27428
- const canvasWidth = options.canvasWidth || width;
27429
- const canvasHeight = options.canvasHeight || height;
27430
- canvas.width = canvasWidth * ratio;
27431
- canvas.height = canvasHeight * ratio;
27432
- if (!options.skipAutoScale) {
27433
- checkCanvasDimensions(canvas);
26820
+ }
26821
+
26822
+ async function toSvg(node, options = {}) {
26823
+ const { width, height } = getImageSize(node, options);
26824
+ const clonedNode = (await cloneNode(node, options, true));
26825
+ await embedWebFonts(clonedNode, options);
26826
+ await embedImages(clonedNode, options);
26827
+ applyStyle(clonedNode, options);
26828
+ const datauri = await nodeToDataURL(clonedNode, width, height);
26829
+ return datauri;
26830
+ }
26831
+ async function toCanvas(node, options = {}) {
26832
+ const { width, height } = getImageSize(node, options);
26833
+ const svg = await toSvg(node, options);
26834
+ const img = await createImage(svg);
26835
+ const canvas = document.createElement('canvas');
26836
+ const context = canvas.getContext('2d');
26837
+ const ratio = options.pixelRatio || getPixelRatio();
26838
+ const canvasWidth = options.canvasWidth || width;
26839
+ const canvasHeight = options.canvasHeight || height;
26840
+ canvas.width = canvasWidth * ratio;
26841
+ canvas.height = canvasHeight * ratio;
26842
+ if (!options.skipAutoScale) {
26843
+ checkCanvasDimensions(canvas);
26844
+ }
26845
+ canvas.style.width = `${canvasWidth}`;
26846
+ canvas.style.height = `${canvasHeight}`;
26847
+ if (options.backgroundColor) {
26848
+ context.fillStyle = options.backgroundColor;
26849
+ context.fillRect(0, 0, canvas.width, canvas.height);
26850
+ }
26851
+ context.drawImage(img, 0, 0, canvas.width, canvas.height);
26852
+ return canvas;
26853
+ }
26854
+ async function toPng(node, options = {}) {
26855
+ const canvas = await toCanvas(node, options);
26856
+ return canvas.toDataURL();
26857
+ }
26858
+
26859
+ class KritzelWheelHelper {
26860
+ static shouldKeepWheelInCustomElement(ev, customElement) {
26861
+ // Keep Ctrl+wheel available for viewport zoom gestures.
26862
+ if (ev.ctrlKey || customElement.isSelected !== true) {
26863
+ return false;
26864
+ }
26865
+ const path = typeof ev.composedPath === 'function' ? ev.composedPath() : [];
26866
+ const scrollContainer = this.getScrollableContainerFromEventPath(path, customElement);
26867
+ // No scroll container found inside the custom element: let the wheel
26868
+ // event pass through so the viewport can continue handling it.
26869
+ if (!scrollContainer) {
26870
+ return false;
26871
+ }
26872
+ return this.canElementConsumeWheel(scrollContainer, ev.deltaX, ev.deltaY);
26873
+ }
26874
+ static getScrollableContainerFromEventPath(path, customElement) {
26875
+ const potentialRoots = [];
26876
+ if (customElement.element instanceof HTMLElement) {
26877
+ potentialRoots.push(customElement.element);
26878
+ }
26879
+ if (customElement.elementRef instanceof HTMLElement) {
26880
+ potentialRoots.push(customElement.elementRef);
26881
+ }
26882
+ const legacyElementRef = customElement.elmeentRef;
26883
+ if (legacyElementRef instanceof HTMLElement) {
26884
+ potentialRoots.push(legacyElementRef);
26885
+ }
26886
+ for (const root of potentialRoots) {
26887
+ const rootIndex = path.findIndex(entry => entry === root);
26888
+ if (rootIndex === -1) {
26889
+ continue;
26890
+ }
26891
+ for (const entry of path.slice(0, rootIndex + 1)) {
26892
+ if (!(entry instanceof HTMLElement)) {
26893
+ continue;
26894
+ }
26895
+ if (this.isScrollableElement(entry)) {
26896
+ return entry;
26897
+ }
26898
+ }
26899
+ }
26900
+ // Fallback for synthetic/custom wheel events where composedPath is absent
26901
+ // or does not include the custom-element subtree.
26902
+ for (const root of potentialRoots) {
26903
+ if (this.isScrollableElement(root)) {
26904
+ return root;
26905
+ }
26906
+ const firstChild = root.firstElementChild;
26907
+ if (firstChild instanceof HTMLElement && this.isScrollableElement(firstChild)) {
26908
+ return firstChild;
26909
+ }
26910
+ }
26911
+ return null;
26912
+ }
26913
+ static isScrollableElement(element) {
26914
+ const canScrollVertically = element.scrollHeight - element.clientHeight > 0;
26915
+ const canScrollHorizontally = element.scrollWidth - element.clientWidth > 0;
26916
+ if (!canScrollVertically && !canScrollHorizontally) {
26917
+ return false;
26918
+ }
26919
+ const computedStyle = window.getComputedStyle(element);
26920
+ const overflowX = computedStyle.overflowX;
26921
+ const overflowY = computedStyle.overflowY;
26922
+ const allowsVerticalScroll = overflowY !== 'visible' && overflowY !== 'clip';
26923
+ const allowsHorizontalScroll = overflowX !== 'visible' && overflowX !== 'clip';
26924
+ return (canScrollVertically && allowsVerticalScroll) || (canScrollHorizontally && allowsHorizontalScroll);
26925
+ }
26926
+ static canElementConsumeWheel(element, deltaX, deltaY) {
26927
+ const canConsumeVertical = this.canElementConsumeVerticalWheel(element, deltaY);
26928
+ const canConsumeHorizontal = this.canElementConsumeHorizontalWheel(element, deltaX);
26929
+ // Keep wheel events inside the custom element whenever at least one
26930
+ // requested axis can still be consumed by its scroll container.
26931
+ return canConsumeVertical || canConsumeHorizontal;
26932
+ }
26933
+ static canElementConsumeVerticalWheel(element, deltaY) {
26934
+ if (deltaY === 0) {
26935
+ return false;
26936
+ }
26937
+ const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
26938
+ if (maxScrollTop === 0) {
26939
+ return false;
26940
+ }
26941
+ const epsilon = 1;
26942
+ const scrollingDown = deltaY > 0;
26943
+ const scrollingUp = deltaY < 0;
26944
+ if (scrollingDown) {
26945
+ return element.scrollTop < maxScrollTop - epsilon;
26946
+ }
26947
+ if (scrollingUp) {
26948
+ return element.scrollTop > epsilon;
26949
+ }
26950
+ return false;
26951
+ }
26952
+ static canElementConsumeHorizontalWheel(element, deltaX) {
26953
+ if (deltaX === 0) {
26954
+ return false;
26955
+ }
26956
+ const maxScrollLeft = Math.max(0, element.scrollWidth - element.clientWidth);
26957
+ if (maxScrollLeft === 0) {
26958
+ return false;
26959
+ }
26960
+ const epsilon = 1;
26961
+ const scrollingRight = deltaX > 0;
26962
+ const scrollingLeft = deltaX < 0;
26963
+ if (scrollingRight) {
26964
+ return element.scrollLeft < maxScrollLeft - epsilon;
26965
+ }
26966
+ if (scrollingLeft) {
26967
+ return element.scrollLeft > epsilon;
26968
+ }
26969
+ return false;
26970
+ }
26971
+ }
26972
+
26973
+ /**
26974
+ * Helper class for exporting Kritzel objects to SVG format.
26975
+ * Generates clean, standards-compliant SVG markup from canvas objects.
26976
+ */
26977
+ class KritzelSvgExportHelper {
26978
+ /**
26979
+ * Calculates the combined bounding box that encompasses all objects.
26980
+ * Uses rotatedBoundingBox to account for object rotation.
26981
+ * @param objects - Array of objects to calculate bounds for
26982
+ * @returns Combined bounding box, or null if no objects provided
26983
+ */
26984
+ static calculateCombinedBounds(objects) {
26985
+ if (!objects || objects.length === 0) {
26986
+ return null;
26987
+ }
26988
+ let minX = Infinity;
26989
+ let minY = Infinity;
26990
+ let maxX = -Infinity;
26991
+ let maxY = -Infinity;
26992
+ for (const obj of objects) {
26993
+ const bbox = obj.rotatedBoundingBox;
26994
+ minX = Math.min(minX, bbox.x);
26995
+ minY = Math.min(minY, bbox.y);
26996
+ maxX = Math.max(maxX, bbox.x + bbox.width);
26997
+ maxY = Math.max(maxY, bbox.y + bbox.height);
26998
+ }
26999
+ return {
27000
+ x: minX,
27001
+ y: minY,
27002
+ z: 1,
27003
+ width: maxX - minX,
27004
+ height: maxY - minY,
27005
+ };
27006
+ }
27007
+ /**
27008
+ * Generates a complete SVG document string from the given objects.
27009
+ * @param objects - Objects to export
27010
+ * @param options - Export options
27011
+ * @returns SVG document string
27012
+ */
27013
+ static generateSvg(objects, options = {}) {
27014
+ const { theme, padding = 0, includeXmlDeclaration = true, rasterSafeForCanvas = false, imageDataUrls } = options;
27015
+ if (!objects || objects.length === 0) {
27016
+ return '';
27017
+ }
27018
+ // Flatten groups to get all objects including nested children
27019
+ const allObjects = this.flattenObjects(objects);
27020
+ const bounds = this.calculateCombinedBounds(allObjects);
27021
+ if (!bounds) {
27022
+ return '';
27023
+ }
27024
+ // Add padding to bounds
27025
+ const viewBoxX = bounds.x - padding;
27026
+ const viewBoxY = bounds.y - padding;
27027
+ const viewBoxWidth = bounds.width + padding * 2;
27028
+ const viewBoxHeight = bounds.height + padding * 2;
27029
+ // Collect all defs (markers, patterns, etc.)
27030
+ const defs = this.collectDefs(allObjects, theme);
27031
+ // Generate SVG elements for each object
27032
+ const elements = objects
27033
+ .sort((a, b) => a.zIndex - b.zIndex)
27034
+ .map(obj => this.objectToSvgElement(obj, theme, imageDataUrls, rasterSafeForCanvas))
27035
+ .filter(Boolean)
27036
+ .join('\n ');
27037
+ const xmlDecl = includeXmlDeclaration ? '<?xml version="1.0" encoding="UTF-8"?>\n' : '';
27038
+ const defsContent = defs.length > 0 ? `\n <defs>\n ${defs.join('\n ')}\n </defs>` : '';
27039
+ return `${xmlDecl}<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBoxX} ${viewBoxY} ${viewBoxWidth} ${viewBoxHeight}" width="${viewBoxWidth}" height="${viewBoxHeight}">${defsContent}
27040
+ ${elements}
27041
+ </svg>`;
27042
+ }
27043
+ /**
27044
+ * Flattens a list of objects, expanding groups into their children recursively.
27045
+ * @param objects - Objects to flatten
27046
+ * @returns Flat array of all objects including group children
27047
+ */
27048
+ static flattenObjects(objects) {
27049
+ const result = [];
27050
+ for (const obj of objects) {
27051
+ if (KritzelClassHelper.isInstanceOf(obj, 'KritzelGroup')) {
27052
+ result.push(...this.flattenObjects(obj.children));
27053
+ }
27054
+ else {
27055
+ result.push(obj);
27056
+ }
27057
+ }
27058
+ return result;
27059
+ }
27060
+ /**
27061
+ * Resolves every image referenced by the given objects (including those
27062
+ * nested inside groups) to an inlined `data:` URL, keyed by object id.
27063
+ *
27064
+ * This is required for export flows that rasterize the SVG (e.g. PNG
27065
+ * export): an SVG loaded into an `<img>` element will NOT load external
27066
+ * or `blob:` resources, so a `<image href="blob:...">` renders as a
27067
+ * broken-image placeholder. Embedding the bytes as a base64 `data:` URL
27068
+ * makes the image self-contained and survive rasterization.
27069
+ *
27070
+ * Resolution is best-effort: images that fail to resolve are simply
27071
+ * omitted from the map, and the SVG falls back to whatever `href` the
27072
+ * synchronous path would have produced.
27073
+ * @param objects - Objects to inline images for
27074
+ * @param assetResolver - Resolver used to fetch asset bytes; optional
27075
+ * @returns Map of image object id -> inlined `data:` URL
27076
+ */
27077
+ static async resolveImageDataUrls(objects, assetResolver) {
27078
+ const dataUrls = new Map();
27079
+ if (!objects || objects.length === 0) {
27080
+ return dataUrls;
27081
+ }
27082
+ const images = this.flattenObjects(objects).filter((obj) => KritzelClassHelper.isInstanceOf(obj, 'KritzelImage'));
27083
+ await Promise.all(images.map(async (image) => {
27084
+ // Legacy inline data URL is already embeddable as-is.
27085
+ if (image.src?.startsWith('data:')) {
27086
+ dataUrls.set(image.id, image.src);
27087
+ return;
27088
+ }
27089
+ // A resolvedSrc that is itself a data URL can be embedded directly.
27090
+ if (image.resolvedSrc?.startsWith('data:')) {
27091
+ dataUrls.set(image.id, image.resolvedSrc);
27092
+ return;
27093
+ }
27094
+ if (!image.assetId || !assetResolver) {
27095
+ return;
27096
+ }
27097
+ try {
27098
+ const blob = await assetResolver.fetchBlob(image.assetId);
27099
+ dataUrls.set(image.id, await this.blobToDataUrl(blob));
27100
+ }
27101
+ catch (err) {
27102
+ console.warn(`[KritzelSvgExportHelper] Failed to inline image asset ${image.assetId} for export:`, err);
27103
+ }
27104
+ }));
27105
+ return dataUrls;
27106
+ }
27107
+ /**
27108
+ * Reads a Blob into a base64 `data:` URL.
27109
+ * @param blob - The blob to encode
27110
+ * @returns Promise resolving to the data URL string
27111
+ */
27112
+ static async blobToDataUrl(blob) {
27113
+ // Prefer FileReader in browser-like runtimes for efficiency.
27114
+ const hasFileReader = typeof FileReader !== 'undefined';
27115
+ if (hasFileReader) {
27116
+ try {
27117
+ return await new Promise((resolve, reject) => {
27118
+ const reader = new FileReader();
27119
+ reader.onload = () => {
27120
+ if (typeof reader.result === 'string') {
27121
+ resolve(reader.result);
27122
+ return;
27123
+ }
27124
+ reject(new Error('FileReader result is not a string'));
27125
+ };
27126
+ reader.onerror = () => reject(reader.error ?? new Error('Failed to read blob as data URL'));
27127
+ reader.readAsDataURL(blob);
27128
+ });
27129
+ }
27130
+ catch {
27131
+ // Fall through to the ArrayBuffer-based encoder.
27132
+ }
27133
+ }
27134
+ const bytes = new Uint8Array(await blob.arrayBuffer());
27135
+ const mimeType = blob.type || 'application/octet-stream';
27136
+ return `data:${mimeType};base64,${this.uint8ToBase64(bytes)}`;
27137
+ }
27138
+ /**
27139
+ * Encodes a byte array to base64 across browser and Node-like runtimes.
27140
+ */
27141
+ static uint8ToBase64(bytes) {
27142
+ if (typeof Buffer !== 'undefined') {
27143
+ return Buffer.from(bytes).toString('base64');
27144
+ }
27145
+ let binary = '';
27146
+ const chunkSize = 0x8000;
27147
+ for (let i = 0; i < bytes.length; i += chunkSize) {
27148
+ const chunk = bytes.subarray(i, Math.min(i + chunkSize, bytes.length));
27149
+ binary += String.fromCharCode(...chunk);
27150
+ }
27151
+ if (typeof btoa === 'function') {
27152
+ return btoa(binary);
27153
+ }
27154
+ throw new Error('No base64 encoder available in this runtime');
27155
+ }
27156
+ /**
27157
+ * Collects SVG defs (markers, patterns, etc.) needed by the objects.
27158
+ * @param objects - Objects to collect defs for
27159
+ * @param theme - Theme for color resolution
27160
+ * @returns Array of def element strings
27161
+ */
27162
+ static collectDefs(objects, theme) {
27163
+ const defs = [];
27164
+ for (const obj of objects) {
27165
+ if (KritzelClassHelper.isInstanceOf(obj, 'KritzelLine')) {
27166
+ if (obj.hasStartArrow) {
27167
+ defs.push(this.generateArrowMarker(obj, 'start', theme));
27168
+ }
27169
+ if (obj.hasEndArrow) {
27170
+ defs.push(this.generateArrowMarker(obj, 'end', theme));
27171
+ }
27172
+ }
27173
+ }
27174
+ return defs;
27175
+ }
27176
+ /**
27177
+ * Generates an SVG marker element for line arrows.
27178
+ * @param line - The line object
27179
+ * @param end - Which end of the line ('start' or 'end')
27180
+ * @param theme - Theme for color resolution
27181
+ * @returns SVG marker element string
27182
+ */
27183
+ static generateArrowMarker(line, end, _theme) {
27184
+ const markerId = end === 'start' ? line.startMarkerId : line.endMarkerId;
27185
+ const size = line.getArrowSize(end);
27186
+ const fill = line.getArrowFill(end);
27187
+ const style = end === 'start' ? line.arrows?.start?.style : line.arrows?.end?.style;
27188
+ const path = line.getArrowPath(style);
27189
+ const orient = end === 'start' ? 'auto-start-reverse' : 'auto';
27190
+ const refX = 0;
27191
+ const refY = size / 2;
27192
+ return `<marker id="${markerId}" markerWidth="${size}" markerHeight="${size}" refX="${refX}" refY="${refY}" orient="${orient}" markerUnits="userSpaceOnUse">
27193
+ <path d="${path}" fill="${fill}" transform="scale(${size / 10})"/>
27194
+ </marker>`;
27195
+ }
27196
+ /**
27197
+ * Converts a Kritzel object to its SVG element representation.
27198
+ * @param object - The object to convert
27199
+ * @param theme - Theme for color resolution
27200
+ * @param imageDataUrls - Optional map of image id -> inlined data URL
27201
+ * @returns SVG element string, or empty string if object type is not supported
27202
+ */
27203
+ static objectToSvgElement(object, theme, imageDataUrls, rasterSafeForCanvas = false) {
27204
+ if (KritzelClassHelper.isInstanceOf(object, 'KritzelPath')) {
27205
+ return this.pathToSvg(object, theme);
27206
+ }
27207
+ if (KritzelClassHelper.isInstanceOf(object, 'KritzelLine')) {
27208
+ return this.lineToSvg(object, theme);
27209
+ }
27210
+ if (KritzelClassHelper.isInstanceOf(object, 'KritzelShape')) {
27211
+ return this.shapeToSvg(object, theme);
27212
+ }
27213
+ if (KritzelClassHelper.isInstanceOf(object, 'KritzelText')) {
27214
+ return this.textToSvg(object, theme, rasterSafeForCanvas);
27215
+ }
27216
+ if (KritzelClassHelper.isInstanceOf(object, 'KritzelImage')) {
27217
+ return this.imageToSvg(object, imageDataUrls);
27218
+ }
27219
+ if (KritzelClassHelper.isInstanceOf(object, 'KritzelCustomElement')) {
27220
+ return this.customElementToSvg(object, rasterSafeForCanvas);
27221
+ }
27222
+ if (KritzelClassHelper.isInstanceOf(object, 'KritzelGroup')) {
27223
+ return this.groupToSvg(object, theme, imageDataUrls, rasterSafeForCanvas);
27224
+ }
27225
+ return '';
27226
+ }
27227
+ /**
27228
+ * Converts a KritzelPath to SVG.
27229
+ * @param path - The path object
27230
+ * @param theme - Theme for color resolution
27231
+ * @returns SVG element string
27232
+ */
27233
+ static pathToSvg(path, theme) {
27234
+ const transform = this.buildTransform(path);
27235
+ const fill = KritzelColorHelper.resolveThemeColor(path.fill, theme);
27236
+ const stroke = KritzelColorHelper.resolveThemeColor(path.stroke, theme);
27237
+ const opacity = path.opacity !== 1 ? ` opacity="${path.opacity}"` : '';
27238
+ const scale = this.getObjectScale(path);
27239
+ // Path needs to be wrapped in a g with translation since path.d uses local coordinates
27240
+ return `<g transform="${transform}"${opacity}>
27241
+ <svg viewBox="${path.viewBox}" width="${path.totalWidth / scale}" height="${path.totalHeight / scale}" overflow="visible">
27242
+ <path d="${path.d}" fill="${fill || 'none'}" stroke="${stroke || 'none'}"${path.strokeWidth ? ` stroke-width="${path.strokeWidth}"` : ''}/>
27243
+ </svg>
27244
+ </g>`;
27245
+ }
27246
+ /**
27247
+ * Converts a KritzelLine to SVG.
27248
+ * @param line - The line object
27249
+ * @param theme - Theme for color resolution
27250
+ * @returns SVG element string
27251
+ */
27252
+ static lineToSvg(line, theme) {
27253
+ const transform = this.buildTransform(line);
27254
+ const stroke = KritzelColorHelper.resolveThemeColor(line.stroke, theme);
27255
+ const opacity = line.opacity !== 1 ? ` opacity="${line.opacity}"` : '';
27256
+ const scale = this.getObjectScale(line);
27257
+ const markerStart = line.hasStartArrow ? ` marker-start="url(#${line.startMarkerId})"` : '';
27258
+ const markerEnd = line.hasEndArrow ? ` marker-end="url(#${line.endMarkerId})"` : '';
27259
+ return `<g transform="${transform}"${opacity}>
27260
+ <svg viewBox="${line.viewBox}" width="${line.totalWidth / scale}" height="${line.totalHeight / scale}" overflow="visible">
27261
+ <path d="${line.d}" fill="none" stroke="${stroke}" stroke-width="${line.strokeWidth}" stroke-linecap="round"${markerStart}${markerEnd}/>
27262
+ </svg>
27263
+ </g>`;
27264
+ }
27265
+ /**
27266
+ * Converts a KritzelShape to SVG.
27267
+ * @param shape - The shape object
27268
+ * @param theme - Theme for color resolution
27269
+ * @returns SVG element string
27270
+ */
27271
+ static shapeToSvg(shape, theme) {
27272
+ const transform = this.buildTransform(shape);
27273
+ const fill = KritzelColorHelper.resolveThemeColor(shape.fillColor, theme);
27274
+ const stroke = KritzelColorHelper.resolveThemeColor(shape.strokeColor, theme);
27275
+ const opacity = shape.opacity !== 1 ? ` opacity="${shape.opacity}"` : '';
27276
+ const scale = this.getObjectScale(shape);
27277
+ // Get the SVG path for the shape
27278
+ const pathD = shape.getSvgPath();
27279
+ // Handle text content if present
27280
+ let textContent = '';
27281
+ if (shape.editor && !shape.editor.state.doc.textContent.trim()) ;
27282
+ else if (shape.content) {
27283
+ const textHtml = this.prosemirrorToHtml(shape.content, shape, theme);
27284
+ if (textHtml) {
27285
+ const fontColor = KritzelColorHelper.resolveThemeColor(shape.fontColor, theme);
27286
+ textContent = `
27287
+ <foreignObject x="${shape.x}" y="${shape.y}" width="${shape.width}" height="${shape.height}">
27288
+ <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;">
27289
+ ${textHtml}
27290
+ </div>
27291
+ </foreignObject>`;
27292
+ }
27293
+ }
27294
+ return `<g transform="${transform}"${opacity}>
27295
+ <svg viewBox="${shape.viewBox}" width="${shape.totalWidth / scale}" height="${shape.totalHeight / scale}" overflow="visible" preserveAspectRatio="none">
27296
+ <path d="${pathD}" fill="${fill || 'transparent'}" stroke="${stroke}" stroke-width="${shape.strokeWidth}"/>${textContent}
27297
+ </svg>
27298
+ </g>`;
27299
+ }
27300
+ /**
27301
+ * Converts a KritzelText to SVG using foreignObject for rich text support.
27302
+ * @param text - The text object
27303
+ * @param theme - Theme for color resolution
27304
+ * @returns SVG element string
27305
+ */
27306
+ static textToSvg(text, theme, rasterSafeForCanvas = false) {
27307
+ if (rasterSafeForCanvas) {
27308
+ return this.textToSvgRasterSafe(text, theme);
27309
+ }
27310
+ const transform = this.buildTransform(text);
27311
+ const opacity = text.opacity !== 1 ? ` opacity="${text.opacity}"` : '';
27312
+ const fontColor = KritzelColorHelper.resolveThemeColor(text.fontColor, theme);
27313
+ const bgColor = KritzelColorHelper.resolveThemeColor(text.backgroundColor, theme);
27314
+ const scale = this.getObjectScale(text);
27315
+ // Convert ProseMirror content to HTML
27316
+ const htmlContent = this.prosemirrorToHtml(text.content, text, theme);
27317
+ // Calculate dimensions in SVG world units (world units = CSS px in a 1:1 SVG).
27318
+ const width = text.totalWidth / scale;
27319
+ const height = text.totalHeight / scale;
27320
+ // Convert the font size from pt to SVG world units (px equivalent).
27321
+ // text.fontSize is stored in CSS pt; 1 pt = 4/3 px at 96 dpi.
27322
+ // We also bake in scaleFactor (user resize) and divide by the object scale so
27323
+ // that the rendered font aligns with the world-unit dimensions above.
27324
+ // The CSS transform: scale(scaleFactor) is removed from the div because the
27325
+ // adjusted font-size already accounts for it – keeping both would double-scale
27326
+ // the content and overflow the foreignObject.
27327
+ const svgFontSizePx = (text.fontSize * (4 / 3) * (text.scaleFactor || 1) / scale).toFixed(4);
27328
+ return `<g transform="${transform}"${opacity}>
27329
+ <foreignObject x="0" y="0" width="${width}" height="${height}" overflow="visible">
27330
+ <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>
27331
+ </foreignObject>
27332
+ </g>`;
27333
+ }
27334
+ /**
27335
+ * Converts a KritzelText to SVG using pure SVG text primitives.
27336
+ * This path avoids `<foreignObject>`, which taints canvases when the
27337
+ * generated SVG is rasterized through `<img>` and `drawImage`.
27338
+ */
27339
+ static textToSvgRasterSafe(text, theme) {
27340
+ const transform = this.buildTransform(text);
27341
+ const opacity = text.opacity !== 1 ? ` opacity="${text.opacity}"` : '';
27342
+ const fontColor = KritzelColorHelper.resolveThemeColor(text.fontColor, theme);
27343
+ const bgColor = KritzelColorHelper.resolveThemeColor(text.backgroundColor, theme);
27344
+ const scale = this.getObjectScale(text);
27345
+ const width = text.totalWidth / scale;
27346
+ const height = text.totalHeight / scale;
27347
+ const lines = this.prosemirrorToPlainTextLines(text.content);
27348
+ const resolvedLines = lines.length > 0 ? lines : [''];
27349
+ // Convert the font size from pt to SVG world units (px equivalent).
27350
+ // text.fontSize is stored in CSS pt; 1 pt = 4/3 px at 96 dpi.
27351
+ // We also bake in scaleFactor (user resize) and divide by the object scale so
27352
+ // that the rendered lines align with the world-unit bounding box dimensions above.
27353
+ const resolvedFontSizePx = Math.max(1, text.fontSize * (4 / 3) * (text.scaleFactor || 1) / scale);
27354
+ const lineHeight = resolvedFontSizePx * 1.2;
27355
+ const startY = resolvedFontSizePx;
27356
+ const bgRect = bgColor && bgColor !== 'transparent'
27357
+ ? `<rect x="0" y="0" width="${width}" height="${height}" fill="${bgColor}"/>`
27358
+ : '';
27359
+ const tspans = resolvedLines
27360
+ .map((line, index) => {
27361
+ const y = startY + index * lineHeight;
27362
+ return `<tspan x="0" y="${y}" xml:space="preserve">${this.escapeHtml(line)}</tspan>`;
27363
+ })
27364
+ .join('');
27365
+ return `<g transform="${transform}"${opacity}>
27366
+ ${bgRect}
27367
+ <text font-family="${this.escapeHtml(text.fontFamily)}" font-size="${resolvedFontSizePx}" fill="${fontColor}">${tspans}</text>
27368
+ </g>`;
27434
27369
  }
27435
- canvas.style.width = `${canvasWidth}`;
27436
- canvas.style.height = `${canvasHeight}`;
27437
- if (options.backgroundColor) {
27438
- context.fillStyle = options.backgroundColor;
27439
- context.fillRect(0, 0, canvas.width, canvas.height);
27370
+ /**
27371
+ * Converts a KritzelImage to SVG.
27372
+ *
27373
+ * Prefers an inlined `data:` URL from `imageDataUrls` (keyed by object
27374
+ * id) when provided, so the embedded bytes survive rasterization to
27375
+ * PNG. Falls back to `resolvedSrc` (populated by the renderer via the
27376
+ * asset resolver) and finally the legacy inline `src` field for
27377
+ * documents persisted before the asset layer existed.
27378
+ *
27379
+ * Note: a `blob:` `resolvedSrc` renders correctly on-canvas but becomes
27380
+ * a broken-image placeholder once the SVG is loaded into an `<img>` and
27381
+ * drawn to a canvas. Callers that rasterize the SVG must pass
27382
+ * `imageDataUrls` (see {@link KritzelSvgExportHelper.resolveImageDataUrls}).
27383
+ * @param image - The image object
27384
+ * @param imageDataUrls - Optional map of image id -> inlined data URL
27385
+ * @returns SVG element string
27386
+ */
27387
+ static imageToSvg(image, imageDataUrls) {
27388
+ const transform = this.buildTransform(image);
27389
+ const opacity = image.opacity !== 1 ? ` opacity="${image.opacity}"` : '';
27390
+ const scale = this.getObjectScale(image);
27391
+ // Calculate dimensions
27392
+ const width = image.totalWidth / scale;
27393
+ const height = image.totalHeight / scale;
27394
+ const href = imageDataUrls?.get(image.id) || image.resolvedSrc || image.src || '';
27395
+ return `<g transform="${transform}"${opacity}>
27396
+ <image href="${href}" x="0" y="0" width="${width}" height="${height}" preserveAspectRatio="xMidYMid meet"/>
27397
+ </g>`;
27440
27398
  }
27441
- context.drawImage(img, 0, 0, canvas.width, canvas.height);
27442
- return canvas;
27443
- }
27444
- async function toPng(node, options = {}) {
27445
- const canvas = await toCanvas(node, options);
27446
- return canvas.toDataURL();
27447
- }
27448
-
27449
- class KritzelWheelHelper {
27450
- static shouldKeepWheelInCustomElement(ev, customElement) {
27451
- // Keep Ctrl+wheel available for viewport zoom gestures.
27452
- if (ev.ctrlKey || customElement.isSelected !== true) {
27453
- return false;
27454
- }
27455
- const path = typeof ev.composedPath === 'function' ? ev.composedPath() : [];
27456
- const scrollContainer = this.getScrollableContainerFromEventPath(path, customElement);
27457
- // No scroll container found inside the custom element: let the wheel
27458
- // event pass through so the viewport can continue handling it.
27459
- if (!scrollContainer) {
27460
- return false;
27399
+ /**
27400
+ * Converts a KritzelCustomElement to SVG.
27401
+ * SVG mode: embeds HTML in foreignObject (best-effort).
27402
+ * PNG/raster mode: renders placeholder rect with warning.
27403
+ * @param obj - The custom element object
27404
+ * @param rasterSafeForCanvas - If true, render placeholder (for PNG export)
27405
+ * @returns SVG element string
27406
+ */
27407
+ static customElementToSvg(obj, rasterSafeForCanvas = false) {
27408
+ const transform = this.buildTransform(obj);
27409
+ const opacity = obj.opacity !== 1 ? ` opacity="${obj.opacity}"` : '';
27410
+ const scale = this.getObjectScale(obj);
27411
+ const width = obj.totalWidth / scale;
27412
+ const height = obj.totalHeight / scale;
27413
+ if (rasterSafeForCanvas) {
27414
+ // PNG export: render placeholder rect (foreignObject doesn't rasterize to PNG well)
27415
+ console.warn('[KritzelCustomElement SVG Export] Custom elements do not serialize well to PNG. Use SVG export or screenshot instead.');
27416
+ return `<g transform="${transform}"${opacity}>
27417
+ <rect x="0" y="0" width="${width}" height="${height}" fill="#f0f0f0" stroke="#999" stroke-width="1" />
27418
+ <text x="${width / 2}" y="${height / 2}" text-anchor="middle" dominant-baseline="middle" font-size="12" fill="#999">[Custom Element]</text>
27419
+ </g>`;
27461
27420
  }
27462
- return this.canElementConsumeWheel(scrollContainer, ev.deltaX, ev.deltaY);
27421
+ // SVG export: use foreignObject to embed HTML
27422
+ const html = obj.element?.outerHTML || '<div style="border:1px dashed #ccc">Empty</div>';
27423
+ return `<g transform="${transform}"${opacity}>
27424
+ <foreignObject x="0" y="0" width="${width}" height="${height}">
27425
+ <div xmlns="http://www.w3.org/1999/xhtml" style="width:100%;height:100%;overflow:hidden;">
27426
+ ${html}
27427
+ </div>
27428
+ </foreignObject>
27429
+ </g>`;
27463
27430
  }
27464
- static getScrollableContainerFromEventPath(path, customElement) {
27465
- const potentialRoots = [];
27466
- if (customElement.element instanceof HTMLElement) {
27467
- potentialRoots.push(customElement.element);
27468
- }
27469
- if (customElement.elementRef instanceof HTMLElement) {
27470
- potentialRoots.push(customElement.elementRef);
27471
- }
27472
- const legacyElementRef = customElement.elmeentRef;
27473
- if (legacyElementRef instanceof HTMLElement) {
27474
- potentialRoots.push(legacyElementRef);
27475
- }
27476
- for (const root of potentialRoots) {
27477
- const rootIndex = path.findIndex(entry => entry === root);
27478
- if (rootIndex === -1) {
27479
- continue;
27480
- }
27481
- for (const entry of path.slice(0, rootIndex + 1)) {
27482
- if (!(entry instanceof HTMLElement)) {
27483
- continue;
27484
- }
27485
- if (this.isScrollableElement(entry)) {
27486
- return entry;
27487
- }
27488
- }
27489
- }
27490
- // Fallback for synthetic/custom wheel events where composedPath is absent
27491
- // or does not include the custom-element subtree.
27492
- for (const root of potentialRoots) {
27493
- if (this.isScrollableElement(root)) {
27494
- return root;
27495
- }
27496
- const firstChild = root.firstElementChild;
27497
- if (firstChild instanceof HTMLElement && this.isScrollableElement(firstChild)) {
27498
- return firstChild;
27499
- }
27431
+ /**
27432
+ * Converts a KritzelGroup to SVG by recursively converting children.
27433
+ * @param group - The group object
27434
+ * @param theme - Theme for color resolution
27435
+ * @param imageDataUrls - Optional map of image id -> inlined data URL
27436
+ * @returns SVG element string
27437
+ */
27438
+ static groupToSvg(group, theme, imageDataUrls, rasterSafeForCanvas = false) {
27439
+ const children = group.children
27440
+ .sort((a, b) => a.zIndex - b.zIndex)
27441
+ .map(child => this.objectToSvgElement(child, theme, imageDataUrls, rasterSafeForCanvas))
27442
+ .filter(Boolean)
27443
+ .join('\n ');
27444
+ if (!children) {
27445
+ return '';
27500
27446
  }
27501
- return null;
27447
+ const opacity = group.opacity !== 1 ? ` opacity="${group.opacity}"` : '';
27448
+ return `<g${opacity}>
27449
+ ${children}
27450
+ </g>`;
27502
27451
  }
27503
- static isScrollableElement(element) {
27504
- const canScrollVertically = element.scrollHeight - element.clientHeight > 0;
27505
- const canScrollHorizontally = element.scrollWidth - element.clientWidth > 0;
27506
- if (!canScrollVertically && !canScrollHorizontally) {
27507
- return false;
27452
+ /**
27453
+ * Builds a transform attribute string for an object.
27454
+ * Combines translation and rotation.
27455
+ * @param object - The object to build transform for
27456
+ * @returns Transform attribute value string
27457
+ */
27458
+ static buildTransform(object) {
27459
+ const transforms = [];
27460
+ const scale = this.getObjectScale(object);
27461
+ // Translation
27462
+ transforms.push(`translate(${object.translateX}, ${object.translateY})`);
27463
+ // Rotation around center
27464
+ if (object.rotation !== 0) {
27465
+ const centerX = object.totalWidth / 2 / scale;
27466
+ const centerY = object.totalHeight / 2 / scale;
27467
+ const degrees = object.rotation * (180 / Math.PI);
27468
+ transforms.push(`rotate(${degrees}, ${centerX}, ${centerY})`);
27508
27469
  }
27509
- const computedStyle = window.getComputedStyle(element);
27510
- const overflowX = computedStyle.overflowX;
27511
- const overflowY = computedStyle.overflowY;
27512
- const allowsVerticalScroll = overflowY !== 'visible' && overflowY !== 'clip';
27513
- const allowsHorizontalScroll = overflowX !== 'visible' && overflowX !== 'clip';
27514
- return (canScrollVertically && allowsVerticalScroll) || (canScrollHorizontally && allowsHorizontalScroll);
27470
+ return transforms.join(' ');
27515
27471
  }
27516
- static canElementConsumeWheel(element, deltaX, deltaY) {
27517
- const canConsumeVertical = this.canElementConsumeVerticalWheel(element, deltaY);
27518
- const canConsumeHorizontal = this.canElementConsumeHorizontalWheel(element, deltaX);
27519
- // Keep wheel events inside the custom element whenever at least one
27520
- // requested axis can still be consumed by its scroll container.
27521
- return canConsumeVertical || canConsumeHorizontal;
27472
+ /**
27473
+ * Returns a safe non-zero scale for export geometry calculations.
27474
+ */
27475
+ static getObjectScale(object) {
27476
+ const scale = object.scale ?? 1;
27477
+ return scale === 0 ? 1 : scale;
27522
27478
  }
27523
- static canElementConsumeVerticalWheel(element, deltaY) {
27524
- if (deltaY === 0) {
27525
- return false;
27479
+ /**
27480
+ * Converts ProseMirror JSON content to HTML string.
27481
+ * @param content - ProseMirror document JSON
27482
+ * @param context - The text or shape object for styling context
27483
+ * @param theme - Theme for color resolution
27484
+ * @returns HTML string
27485
+ */
27486
+ static prosemirrorToHtml(content, _context, theme) {
27487
+ if (!content || !content.content) {
27488
+ return '';
27526
27489
  }
27527
- const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
27528
- if (maxScrollTop === 0) {
27529
- return false;
27490
+ return content.content
27491
+ .map((node) => this.nodeToHtml(node, theme))
27492
+ .join('');
27493
+ }
27494
+ /**
27495
+ * Converts a ProseMirror node to HTML.
27496
+ * @param node - ProseMirror node JSON
27497
+ * @param theme - Theme for color resolution
27498
+ * @returns HTML string
27499
+ */
27500
+ static nodeToHtml(node, theme) {
27501
+ if (!node)
27502
+ return '';
27503
+ switch (node.type) {
27504
+ case 'paragraph':
27505
+ const pContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
27506
+ return `<p style="margin: 0;">${pContent || '<br/>'}</p>`;
27507
+ case 'text':
27508
+ let text = this.escapeHtml(node.text || '');
27509
+ if (node.marks) {
27510
+ for (const mark of node.marks) {
27511
+ text = this.applyMark(text, mark, theme);
27512
+ }
27513
+ }
27514
+ return text;
27515
+ case 'hard_break':
27516
+ return '<br/>';
27517
+ case 'bullet_list':
27518
+ const blContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
27519
+ return `<ul style="margin: 0; padding-left: 1.5em;">${blContent}</ul>`;
27520
+ case 'ordered_list':
27521
+ const olContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
27522
+ return `<ol style="margin: 0; padding-left: 1.5em;">${olContent}</ol>`;
27523
+ case 'list_item':
27524
+ const liContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
27525
+ return `<li>${liContent}</li>`;
27526
+ default:
27527
+ // For unknown node types, try to render content
27528
+ if (node.content) {
27529
+ return node.content.map((n) => this.nodeToHtml(n, theme)).join('');
27530
+ }
27531
+ return '';
27530
27532
  }
27531
- const epsilon = 1;
27532
- const scrollingDown = deltaY > 0;
27533
- const scrollingUp = deltaY < 0;
27534
- if (scrollingDown) {
27535
- return element.scrollTop < maxScrollTop - epsilon;
27533
+ }
27534
+ /**
27535
+ * Converts ProseMirror JSON content to plain-text lines.
27536
+ * Used by raster-safe SVG export, which cannot depend on HTML/foreignObject.
27537
+ */
27538
+ static prosemirrorToPlainTextLines(content) {
27539
+ if (!content || !Array.isArray(content.content)) {
27540
+ return [];
27536
27541
  }
27537
- if (scrollingUp) {
27538
- return element.scrollTop > epsilon;
27542
+ const plainText = content.content
27543
+ .map((node) => this.nodeToPlainText(node))
27544
+ .join('')
27545
+ .replace(/\r\n/g, '\n')
27546
+ .replace(/\n+$/, '');
27547
+ if (!plainText) {
27548
+ return [];
27539
27549
  }
27540
- return false;
27550
+ return plainText.split('\n');
27541
27551
  }
27542
- static canElementConsumeHorizontalWheel(element, deltaX) {
27543
- if (deltaX === 0) {
27544
- return false;
27545
- }
27546
- const maxScrollLeft = Math.max(0, element.scrollWidth - element.clientWidth);
27547
- if (maxScrollLeft === 0) {
27548
- return false;
27552
+ /**
27553
+ * Converts a ProseMirror node tree into plain text.
27554
+ */
27555
+ static nodeToPlainText(node) {
27556
+ if (!node) {
27557
+ return '';
27549
27558
  }
27550
- const epsilon = 1;
27551
- const scrollingRight = deltaX > 0;
27552
- const scrollingLeft = deltaX < 0;
27553
- if (scrollingRight) {
27554
- return element.scrollLeft < maxScrollLeft - epsilon;
27559
+ switch (node.type) {
27560
+ case 'text':
27561
+ return node.text || '';
27562
+ case 'hard_break':
27563
+ return '\n';
27564
+ case 'paragraph': {
27565
+ const paragraph = Array.isArray(node.content)
27566
+ ? node.content.map((child) => this.nodeToPlainText(child)).join('')
27567
+ : '';
27568
+ return `${paragraph}\n`;
27569
+ }
27570
+ case 'list_item': {
27571
+ const item = Array.isArray(node.content)
27572
+ ? node.content.map((child) => this.nodeToPlainText(child)).join('')
27573
+ : '';
27574
+ return `- ${item.trim()}\n`;
27575
+ }
27576
+ default:
27577
+ return Array.isArray(node.content)
27578
+ ? node.content.map((child) => this.nodeToPlainText(child)).join('')
27579
+ : '';
27555
27580
  }
27556
- if (scrollingLeft) {
27557
- return element.scrollLeft > epsilon;
27581
+ }
27582
+ /**
27583
+ * Applies a ProseMirror mark to text.
27584
+ * @param text - The text to wrap
27585
+ * @param mark - The mark to apply
27586
+ * @param theme - Theme for color resolution
27587
+ * @returns Wrapped HTML string
27588
+ */
27589
+ static applyMark(text, mark, _theme) {
27590
+ switch (mark.type) {
27591
+ case 'bold':
27592
+ case 'strong':
27593
+ return `<strong>${text}</strong>`;
27594
+ case 'italic':
27595
+ case 'em':
27596
+ return `<em>${text}</em>`;
27597
+ case 'underline':
27598
+ return `<u>${text}</u>`;
27599
+ case 'strike':
27600
+ return `<s>${text}</s>`;
27601
+ case 'code':
27602
+ return `<code>${text}</code>`;
27603
+ case 'link':
27604
+ const href = mark.attrs?.href || '#';
27605
+ return `<a href="${this.escapeHtml(href)}">${text}</a>`;
27606
+ default:
27607
+ return text;
27558
27608
  }
27559
- return false;
27609
+ }
27610
+ /**
27611
+ * Escapes HTML special characters.
27612
+ * @param str - String to escape
27613
+ * @returns Escaped string
27614
+ */
27615
+ static escapeHtml(str) {
27616
+ return str
27617
+ .replace(/&/g, '&amp;')
27618
+ .replace(/</g, '&lt;')
27619
+ .replace(/>/g, '&gt;')
27620
+ .replace(/"/g, '&quot;')
27621
+ .replace(/'/g, '&#039;');
27560
27622
  }
27561
27623
  }
27562
27624
 
@@ -28753,10 +28815,7 @@ const KritzelEngine = class {
28753
28815
  console.error('Failed to export viewport as PNG: screenshot could not be generated');
28754
28816
  return;
28755
28817
  }
28756
- const link = document.createElement('a');
28757
- link.download = filename;
28758
- link.href = dataUrl;
28759
- link.click();
28818
+ this.triggerDataUrlDownload(dataUrl, filename);
28760
28819
  }
28761
28820
  catch (error) {
28762
28821
  console.error('Failed to export viewport as PNG:', error);
@@ -28778,129 +28837,57 @@ const KritzelEngine = class {
28778
28837
  console.error('Failed to export viewport as SVG: screenshot could not be generated');
28779
28838
  return;
28780
28839
  }
28781
- const link = document.createElement('a');
28782
- link.download = filename;
28783
- link.href = dataUrl;
28784
- link.click();
28840
+ this.triggerDataUrlDownload(dataUrl, filename);
28785
28841
  }
28786
28842
  catch (error) {
28787
28843
  console.error('Failed to export viewport as SVG:', error);
28788
28844
  }
28789
28845
  }
28790
28846
  /**
28791
- * Generates an SVG string from the currently selected objects.
28792
- * Creates clean, standards-compliant SVG markup by directly serializing object properties.
28793
- * @param options - Optional export settings (theme, padding, raster-safe mode for canvas export).
28794
- * @returns SVG markup string, or null if no objects are selected.
28847
+ * Returns export formats supported by the currently selected object.
28848
+ * Only returns formats when exactly one object is selected.
28795
28849
  */
28796
- async getSelectedObjectsAsSvgString(options) {
28797
- const selectedObjects = await this.getSelectedObjects();
28798
- if (!selectedObjects || selectedObjects.length === 0) {
28799
- return null;
28850
+ async getSelectedObjectSupportedExportFormats() {
28851
+ const selected = this.getSingleSelectedExportableObject();
28852
+ if (!selected) {
28853
+ return [];
28800
28854
  }
28801
- const theme = options?.theme ?? this.core.themeManager.getStoredTheme();
28802
- // Inline image bytes as data URLs so they survive both standalone SVG
28803
- // files and rasterization to PNG (a `blob:` href is not loaded when an
28804
- // SVG is drawn into a canvas, which otherwise yields a broken image).
28805
- const imageDataUrls = await KritzelSvgExportHelper.resolveImageDataUrls(selectedObjects, this.core.assetResolver);
28806
- return KritzelSvgExportHelper.generateSvg(selectedObjects, {
28807
- theme,
28808
- padding: options?.padding ?? 0,
28809
- includeXmlDeclaration: true,
28810
- rasterSafeForCanvas: options?.rasterSafeForCanvas ?? false,
28811
- imageDataUrls,
28812
- });
28855
+ return selected.getSupportedExportFormats();
28813
28856
  }
28814
28857
  /**
28815
- * Exports the currently selected objects as an SVG file and triggers a browser download.
28816
- * Creates clean, standards-compliant SVG markup suitable for import into vector editors.
28817
- * @param options - Optional export settings (theme, padding).
28858
+ * Checks whether the currently selected object supports a specific export format.
28859
+ * Returns false when none or multiple objects are selected.
28818
28860
  */
28819
- async exportSelectedObjectsAsSvg(options) {
28820
- try {
28821
- const svgString = await this.getSelectedObjectsAsSvgString(options);
28822
- if (!svgString) {
28823
- console.warn('No objects selected for SVG export');
28824
- return;
28825
- }
28826
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
28827
- const filename = `selection-${timestamp}.svg`;
28828
- const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
28829
- const url = URL.createObjectURL(blob);
28830
- const link = document.createElement('a');
28831
- link.download = filename;
28832
- link.href = url;
28833
- link.click();
28834
- URL.revokeObjectURL(url);
28835
- }
28836
- catch (error) {
28837
- console.error('Failed to export selected objects as SVG:', error);
28861
+ async canExportSelectedObjectAs(format) {
28862
+ const selected = this.getSingleSelectedExportableObject();
28863
+ if (!selected) {
28864
+ return false;
28838
28865
  }
28866
+ return selected.getSupportedExportFormats().includes(format);
28839
28867
  }
28840
28868
  /**
28841
- * Generates a PNG data URL from the currently selected objects.
28842
- * Converts the SVG representation to a rasterized PNG image.
28843
- * @param options - Optional export settings (theme, padding, scale for resolution).
28844
- * @returns PNG data URL string, or null if no objects are selected.
28869
+ * Exports the currently selected object in the requested format and triggers a file download.
28870
+ * Only supports single selection.
28845
28871
  */
28846
- async getSelectedObjectsAsPngDataUrl(options) {
28847
- const svgString = await this.getSelectedObjectsAsSvgString({
28848
- theme: options?.theme,
28849
- padding: options?.padding,
28850
- // Avoid foreignObject-based text serialization, which taints the
28851
- // canvas in SVG->PNG conversion paths.
28852
- rasterSafeForCanvas: true,
28853
- });
28854
- if (!svgString) {
28855
- return null;
28872
+ async exportSelectedObject(format) {
28873
+ const selected = this.getSingleSelectedExportableObject();
28874
+ if (!selected) {
28875
+ return;
28856
28876
  }
28857
- const scale = options?.scale ?? 2; // Default 2x for better quality
28858
- return new Promise((resolve, reject) => {
28859
- const img = new Image();
28860
- const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
28861
- const url = URL.createObjectURL(svgBlob);
28862
- img.onload = () => {
28863
- const canvas = document.createElement('canvas');
28864
- canvas.width = img.width * scale;
28865
- canvas.height = img.height * scale;
28866
- const ctx = canvas.getContext('2d');
28867
- if (!ctx) {
28868
- URL.revokeObjectURL(url);
28869
- reject(new Error('Failed to get canvas context'));
28870
- return;
28871
- }
28872
- ctx.scale(scale, scale);
28873
- ctx.drawImage(img, 0, 0);
28874
- URL.revokeObjectURL(url);
28875
- resolve(canvas.toDataURL('image/png'));
28876
- };
28877
- img.onerror = () => {
28878
- URL.revokeObjectURL(url);
28879
- reject(new Error('Failed to load SVG for PNG conversion'));
28880
- };
28881
- img.src = url;
28882
- });
28883
- }
28884
- /**
28885
- * Exports the currently selected objects as a PNG file and triggers a browser download.
28886
- * @param options - Optional export settings (theme, padding, scale for resolution).
28887
- */
28888
- async exportSelectedObjectsAsPng(options) {
28889
28877
  try {
28890
- const dataUrl = await this.getSelectedObjectsAsPngDataUrl(options);
28878
+ const dataUrl = await selected.exportAs(format, this.getObjectExportContext());
28891
28879
  if (!dataUrl) {
28892
- console.warn('No objects selected for PNG export');
28880
+ console.error(`Failed to export selected object as ${format}: export returned no data URL`);
28893
28881
  return;
28894
28882
  }
28883
+ const activeWorkspaceName = this.core.store.state?.activeWorkspace?.name || 'workspace';
28884
+ const objectName = selected.__class__.replace(/^Kritzel/, '').toLowerCase() || 'object';
28895
28885
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
28896
- const filename = `selection-${timestamp}.png`;
28897
- const link = document.createElement('a');
28898
- link.download = filename;
28899
- link.href = dataUrl;
28900
- link.click();
28886
+ const filename = `${activeWorkspaceName}-${objectName}-${timestamp}.${format}`;
28887
+ this.triggerDataUrlDownload(dataUrl, filename);
28901
28888
  }
28902
28889
  catch (error) {
28903
- console.error('Failed to export selected objects as PNG:', error);
28890
+ console.error(`Failed to export selected object as ${format}:`, error);
28904
28891
  }
28905
28892
  }
28906
28893
  /**
@@ -29095,6 +29082,73 @@ const KritzelEngine = class {
29095
29082
  const response = await fetch(dataUrl);
29096
29083
  return response.blob();
29097
29084
  }
29085
+ triggerDataUrlDownload(dataUrl, filename) {
29086
+ const link = document.createElement('a');
29087
+ link.download = filename;
29088
+ link.href = dataUrl;
29089
+ link.click();
29090
+ }
29091
+ getSingleSelectedExportableObject() {
29092
+ const selectedObjects = this.core.store.selectionGroup?.objects ?? [];
29093
+ if (selectedObjects.length !== 1) {
29094
+ return null;
29095
+ }
29096
+ const [selected] = selectedObjects;
29097
+ if (!ObjectHelper.isKritzelExportable(selected)) {
29098
+ return null;
29099
+ }
29100
+ return selected;
29101
+ }
29102
+ getObjectExportContext() {
29103
+ return {
29104
+ getObjectAsSvgDataUrl: async (object, options) => {
29105
+ const svg = await this.getObjectAsSvgMarkup(object, options?.rasterSafeForCanvas ?? false);
29106
+ return svg ? this.svgMarkupToDataUrl(svg) : null;
29107
+ },
29108
+ getObjectAsPngDataUrl: async (object) => {
29109
+ const svg = await this.getObjectAsSvgMarkup(object, true);
29110
+ return svg ? this.svgMarkupToPngDataUrl(svg) : null;
29111
+ },
29112
+ };
29113
+ }
29114
+ async getObjectAsSvgMarkup(object, rasterSafeForCanvas) {
29115
+ const imageDataUrls = await KritzelSvgExportHelper.resolveImageDataUrls([object], this.core.assetResolver);
29116
+ const svg = KritzelSvgExportHelper.generateSvg([object], {
29117
+ theme: this.theme,
29118
+ rasterSafeForCanvas,
29119
+ imageDataUrls,
29120
+ });
29121
+ return svg || null;
29122
+ }
29123
+ svgMarkupToDataUrl(svgMarkup) {
29124
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgMarkup)}`;
29125
+ }
29126
+ async svgMarkupToPngDataUrl(svgMarkup) {
29127
+ const svgBlob = new Blob([svgMarkup], { type: 'image/svg+xml;charset=utf-8' });
29128
+ const svgBlobUrl = URL.createObjectURL(svgBlob);
29129
+ try {
29130
+ const img = await new Promise((resolve, reject) => {
29131
+ const element = new Image();
29132
+ element.onload = () => resolve(element);
29133
+ element.onerror = err => reject(err);
29134
+ element.src = svgBlobUrl;
29135
+ });
29136
+ const width = Math.max(1, Math.ceil(img.naturalWidth || img.width));
29137
+ const height = Math.max(1, Math.ceil(img.naturalHeight || img.height));
29138
+ const canvas = document.createElement('canvas');
29139
+ canvas.width = width;
29140
+ canvas.height = height;
29141
+ const context = canvas.getContext('2d');
29142
+ if (!context) {
29143
+ throw new Error('Could not create 2D canvas context for PNG export');
29144
+ }
29145
+ context.drawImage(img, 0, 0, width, height);
29146
+ return canvas.toDataURL('image/png');
29147
+ }
29148
+ finally {
29149
+ URL.revokeObjectURL(svgBlobUrl);
29150
+ }
29151
+ }
29098
29152
  getImportedWorkspaceName(rawName) {
29099
29153
  if (typeof rawName === 'string' && rawName.trim().length > 0) {
29100
29154
  return rawName;
@@ -29971,7 +30025,7 @@ const KritzelEngine = class {
29971
30025
  this.emitObjectsInViewportChange();
29972
30026
  }
29973
30027
  }
29974
- return (h(Host, null, h("kritzel-loading-overlay", { visible: this.core.store.state.isLoading, text: this.core.localizationManager.translate('engine.loading') }), h("kritzel-sync-indicator", { visible: this.isSyncing, text: "Syncing..." }), this.core.store.state.debugInfo.showViewportInfo && (h("div", { class: "debug-panel", style: {
30028
+ return (h(Host, null, this.core.store.state.debugInfo.showViewportInfo && (h("div", { class: "debug-panel", style: {
29975
30029
  color: 'var(--kritzel-global-text-primary)',
29976
30030
  } }, h("div", null, "ActiveWorkspaceId: ", this.core.store.state?.activeWorkspace?.id), h("div", null, "ActiveWorkspaceName: ", this.core.store.state?.activeWorkspace?.name), h("div", null, "TranslateX: ", this.core.store.state?.translateX), h("div", null, "TranslateY: ", this.core.store.state?.translateY), h("div", null, "ViewportWidth: ", this.core.store.state?.viewportWidth), h("div", null, "ViewportHeight: ", this.core.store.state?.viewportHeight), h("div", null, "PointerCount: ", this.core.store.state.pointers.size), h("div", null, "Scale: ", this.core.store.state?.scale), h("div", null, "ActiveTool: ", this.core.store.state?.activeTool?.name), h("div", null, "HasViewportChanged: ", this.core.store.state?.hasViewportChanged ? 'true' : 'false'), h("div", null, "IsEnabled: ", this.core.store.state?.isEnabled ? 'true' : 'false'), h("div", null, "IsScaling: ", this.core.store.state?.isScaling ? 'true' : 'false'), h("div", null, "IsPanning: ", this.core.store.state?.isPanning ? 'true' : 'false'), h("div", null, "IsSelecting: ", this.isSelecting ? 'true' : 'false'), h("div", null, "IsSelectionActive: ", this.isSelectionActive ? 'true' : 'false'), h("div", null, "IsResizeHandleSelected: ", this.core.store.state.isResizeHandleSelected ? 'true' : 'false'), h("div", null, "IsRotationHandleSelected: ", this.core.store.state.isRotationHandleSelected ? 'true' : 'false'), h("div", null, "IsRotationHandleHovered: ", this.core.store.state.isRotationHandleHovered ? 'true' : 'false'), h("div", null, "IsDrawing: ", this.core.store.state.isDrawing ? 'true' : 'false'), h("div", null, "IsWriting: ", this.core.store.state.isWriting ? 'true' : 'false'), h("div", null, "IsPointerDown: ", this.core.store.isPointerDown ? 'true' : 'false'), h("div", null, "PointerX: ", this.core.store.state?.pointerX), h("div", null, "PointerY: ", this.core.store.state?.pointerY), h("div", null, "TotalObjects: ", this.core.store.totalObjectCount), h("div", null, "ObjectsInViewport: ", this.core.store.objectsInViewport.length), h("div", null, "SelectedObjects: ", this.core.store.selectionGroup?.objects.length || 0), h("div", null, "ViewportCenter: (", viewportCenterX.toFixed(2), ", ", viewportCenterY.toFixed(2), ")"))), h("div", { id: "origin", class: "origin", style: {
29977
30031
  transform: `matrix(${this.core.store.state?.scale}, 0, 0, ${this.core.store.state?.scale}, ${this.core.store.state?.translateX}, ${this.core.store.state?.translateY})`,
@@ -30566,7 +30620,7 @@ const KritzelExport = class {
30566
30620
  return (h("div", { class: "export-tab-content" }, 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 })));
30567
30621
  }
30568
30622
  render() {
30569
- return (h(Host, { key: '0bc45e80716be4a7b19ae12a6464a641b5bd4550' }, h("kritzel-dialog", { key: 'f336f8b1e4ae10954d48ca67da88504faf06b295', isOpen: this.isDialogOpen, dialogTitle: this.terms['export.dialogTitle'] ?? 'Export', closable: true, contained: true, onDialogClose: this.closeDialog }, h("div", { key: 'e2d1209652bb54fb7f623e50bc4bbbb703083b76', class: "export-content" }, 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(), h("button", { key: 'e06564889d991afc7a4ac43bb1e5504e98c94c52', class: "export-primary-button", onClick: this.handleExport }, this.terms['export.exportButton'] ?? 'Export')))));
30623
+ return (h(Host, { key: '64a9957ef87799215f9274213ead958a895dd8ef' }, h("kritzel-dialog", { key: '59ad70cc7d2834f5ecac3a659189390015cd26cb', isOpen: this.isDialogOpen, dialogTitle: this.terms['export.dialogTitle'] ?? 'Export', closable: true, contained: true, onDialogClose: this.closeDialog }, h("div", { key: 'fc9247f5b3848def87e6f6a6044d3fb1e63594ac', class: "export-content" }, 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()), h("div", { key: 'b8e748034496936bf000a8eb111f52d58bf28d20', slot: "footer" }, h("button", { key: 'b7a59f3882146b45a0eb8395ea1c45cb41af0f34', class: "export-primary-button", onClick: this.handleExport }, this.terms['export.exportButton'] ?? 'Export')))));
30570
30624
  }
30571
30625
  };
30572
30626
  KritzelExport.style = kritzelExportCss();
@@ -30581,7 +30635,7 @@ const KritzelFont = class {
30581
30635
  size = 24;
30582
30636
  color = '#000000';
30583
30637
  render() {
30584
- return (h(Host, { key: '054371cac30a2433611139a670406b0a1e3b99ff' }, h("div", { key: 'b4f2142b3cb31d067f39952a38b6f665f28f63bf', class: "font-preview", style: {
30638
+ return (h(Host, { key: '42f058426e2afcc49c788f2385e23a5ff153e77c' }, h("div", { key: '962dfc48ba84c5f5d2b98eb2464c69c04764ab0f', class: "font-preview", style: {
30585
30639
  fontFamily: this.fontFamily,
30586
30640
  fontSize: `${this.size}px`,
30587
30641
  color: this.color
@@ -30665,7 +30719,7 @@ const KritzelFontSize = class {
30665
30719
  render() {
30666
30720
  const color = 'var(--kritzel-global-text-primary)';
30667
30721
  const sizes = this.sizes ?? [];
30668
- return (h(Host, { key: '56eb19367abe312bac3b626cfcae3b1ff6e90d21' }, sizes.map(size => (h("div", { tabIndex: 0, class: {
30722
+ return (h(Host, { key: '9d6592f049c25e9dfa2f6f3a270feb2440aa7f35' }, sizes.map(size => (h("div", { tabIndex: 0, class: {
30669
30723
  'size-container': true,
30670
30724
  'selected': this.selectedSize === size,
30671
30725
  }, onClick: () => this.handleSizeClick(size), onKeyDown: event => this.handleKeyDown(event, size) }, h("kritzel-font", { fontFamily: this.fontFamily, size: size, color: color }))))));
@@ -30732,7 +30786,7 @@ const KritzelInput = class {
30732
30786
  this.valueChange.emit(input.value);
30733
30787
  };
30734
30788
  render() {
30735
- return (h(Host, { key: '46a0a48902a06ad85edcbaad3679ce3fe9ba850c' }, h("div", { key: 'a6beaf920614aa959d2fbd68b7268afba8637358', class: "input-container" }, this.label && h("label", { key: 'ca6d3fb3c22f1540795dbb0695ddd9b87cc05eee', class: "input-label" }, this.label), h("div", { key: '78d116e82531a9cca996d594792245449bcaf9c8', class: { 'input-wrapper': true, 'has-suffix': !!this.suffix } }, h("input", { key: '0d10d6356dea3a11338172bbe8c3c878eefcc708', type: this.type, class: "text-input", value: this.inputValue, placeholder: this.placeholder, disabled: this.disabled, onInput: this.handleInput }), this.suffix && h("span", { key: '0e6166ba8728efc1035302d7fb048306efbe5a68', class: "input-suffix" }, this.suffix)))));
30789
+ return (h(Host, { key: 'a4d86101322da32c87a62ac8bb05edb7015ede2a' }, h("div", { key: 'abc5b007af0f6f9353e0b680636c92813ee1ef49', class: "input-container" }, this.label && h("label", { key: 'f79e02a925aa6bd1b6cd48fc4225e441695ff893', class: "input-label" }, this.label), h("div", { key: 'e2937d2ad1def996e1937a9dd711578a3f9b6494', class: { 'input-wrapper': true, 'has-suffix': !!this.suffix } }, h("input", { key: '8bc45cc6f2603bec30f2299286d03beacd8c07fb', type: this.type, class: "text-input", value: this.inputValue, placeholder: this.placeholder, disabled: this.disabled, onInput: this.handleInput }), this.suffix && h("span", { key: '917fb6fa6c5fe62ab0f1e5c83f09aedd22623353', class: "input-suffix" }, this.suffix)))));
30736
30790
  }
30737
30791
  static get watchers() { return {
30738
30792
  "value": [{
@@ -30825,7 +30879,7 @@ const KritzelLineEndings = class {
30825
30879
  };
30826
30880
  KritzelLineEndings.style = kritzelLineEndingsCss();
30827
30881
 
30828
- 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}}`;
30882
+ 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}}`;
30829
30883
 
30830
30884
  const KritzelLoadingOverlay = class {
30831
30885
  constructor(hostRef) {
@@ -30840,7 +30894,7 @@ const KritzelLoadingOverlay = class {
30840
30894
  */
30841
30895
  text = 'Loading...';
30842
30896
  render() {
30843
- return (h(Host, { key: '331e4c9eb30e6f7b98023c23218b3caf5093c34b' }, h("div", { key: 'f994fec0db325a2bd687e5acc68263752e7a6acd', class: { 'loading-overlay': true, visible: this.visible }, "aria-hidden": this.visible ? 'false' : 'true' }, h("span", { key: 'b6087dc1e59845ab95946e654c7bb8e568b42dda', class: "loading-spinner" }), this.text)));
30897
+ return (h(Host, { key: '6fc1886217516966fa3599473989748151d93efd' }, h("div", { key: 'c6f2aa1c7f81bbab65411292934d185b4bf69e5a', class: { 'loading-overlay': true, visible: this.visible }, "aria-hidden": this.visible ? 'false' : 'true' }, h("span", { key: '21bae0b6f9b9a08f814aec7e6635a904018ab839', class: "loading-spinner" }), this.text)));
30844
30898
  }
30845
30899
  };
30846
30900
  KritzelLoadingOverlay.style = kritzelLoadingOverlayCss();
@@ -30892,7 +30946,7 @@ const KritzelLoginDialog = class {
30892
30946
  this.dialogClosed.emit();
30893
30947
  };
30894
30948
  render() {
30895
- return (h(Host, { key: 'bcd7f30bbfeb07bbca732c5f1034bcabbd5e4f80' }, h("kritzel-dialog", { key: '61b5c5054d43433894747f37c571e68ecf08fc4c', dialogTitle: this.dialogTitle, isOpen: this.isDialogOpen, onDialogClose: this.closeDialog, size: "small", contained: true }, h("div", { key: '3ddcb858b6148e8be4bd121b2bd8ca38c66e0505', class: "login-content" }, this.subtitle && (h("p", { key: '6f10f890af9f6025a3aa2d69f8f4d916821fd653', class: "login-subtitle" }, this.subtitle)), h("div", { key: '0d043ac1ac751dc3d0a561bc16e52a2cfaffc578', class: "login-providers" }, this.providers.map(provider => (h("button", { key: provider.name, class: {
30949
+ return (h(Host, { key: '97532a2eeb125a2692117228ec3c77907c90d021' }, h("kritzel-dialog", { key: '60597c3d75f532a3c73aab983bed683bea243eac', dialogTitle: this.dialogTitle, isOpen: this.isDialogOpen, onDialogClose: this.closeDialog, size: "small", contained: true }, h("div", { key: 'cb086e90c5981f8c3d5fc65e7b94675884a255dc', class: "login-content" }, this.subtitle && (h("p", { key: '0ac0e31a5bf8d8a3e24b7df1d388f1cdc6951bd7', class: "login-subtitle" }, this.subtitle)), h("div", { key: '6057615670a1758581d0c518bb2809b88af0672e', class: "login-providers" }, this.providers.map(provider => (h("button", { key: provider.name, class: {
30896
30950
  'provider-button': true,
30897
30951
  'is-loading': this.loadingProvider === provider.name,
30898
30952
  'is-disabled': this.loadingProvider !== null && this.loadingProvider !== provider.name,
@@ -30998,15 +31052,15 @@ const KritzelMasterDetail = class {
30998
31052
  const selectedItem = this.items.find(item => item.id === this.selectedItemId);
30999
31053
  const panelId = 'master-detail-panel';
31000
31054
  const selectedTabId = selectedItem ? `tab-${selectedItem.id}` : undefined;
31001
- return (h(Host, { key: '0edf50b0b853633b88bf481f97f22aceba9e163e' }, h("div", { key: '21a0a3f81e5a60889ccd152c905cb41f52c3e037', class: {
31055
+ return (h(Host, { key: '0166376f16ebb098049cd2ef5539a86086a5570e' }, h("div", { key: 'f5091fe672bc4751c8f66d43baa5c1fc86fca034', class: {
31002
31056
  'master-detail-container': true,
31003
31057
  'is-mobile-detail-visible': this.showMobileDetail,
31004
- } }, h("nav", { key: '41f88248cc372c8cae4cb50a19edf462a01e43f2', class: "master-menu", role: "tablist", "aria-orientation": "vertical", "aria-label": "Settings categories" }, this.items.map((item, index) => (h("button", { key: item.id, id: `tab-${item.id}`, ref: el => this.setTabRef(el, index), class: {
31058
+ } }, h("nav", { key: '66f5c61ea0308ea9ae1339526e86dcf1ee51fc0a', class: "master-menu", role: "tablist", "aria-orientation": "vertical", "aria-label": "Settings categories" }, this.items.map((item, index) => (h("button", { key: item.id, id: `tab-${item.id}`, ref: el => this.setTabRef(el, index), class: {
31005
31059
  'menu-item': true,
31006
31060
  'is-selected': item.id === this.selectedItemId,
31007
31061
  'is-disabled': !!item.disabled,
31008
31062
  'is-focused': index === this.focusedIndex,
31009
- }, role: "tab", "aria-selected": item.id === this.selectedItemId ? 'true' : 'false', "aria-controls": panelId, "aria-disabled": item.disabled ? 'true' : undefined, tabIndex: this.getTabIndex(item, index), disabled: item.disabled, onClick: () => this.handleItemClick(item), onKeyDown: e => this.handleKeyDown(e, item, index), onFocus: () => this.handleFocus(index), onBlur: this.handleBlur }, item.icon && (h("kritzel-icon", { name: item.icon, size: 20, class: "menu-item-icon" })), h("span", { class: "menu-item-label" }, item.label), h("span", { class: "menu-item-chevron", "aria-hidden": "true" }, h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("path", { d: "m9 18 6-6-6-6" }))))))), h("div", { key: 'f8447facf3dfdbd7772823654847e1b90b03f73e', id: panelId, class: "detail-panel", role: "tabpanel", "aria-labelledby": selectedTabId }, h("button", { key: '3edc9784267613e6b3a706739bbb537161e4c747', class: "mobile-back-button", onClick: this.handleBackClick, "aria-label": "Back to menu" }, h("kritzel-icon", { key: 'ec0b4691735578525e5863842271366e04b13fb7', name: "chevron-left", size: 20, class: "mobile-back-icon" }), "Back"), h("slot", { key: 'c21fbaff78f98859b9756427125b60504d4bb630' })))));
31063
+ }, role: "tab", "aria-selected": item.id === this.selectedItemId ? 'true' : 'false', "aria-controls": panelId, "aria-disabled": item.disabled ? 'true' : undefined, tabIndex: this.getTabIndex(item, index), disabled: item.disabled, onClick: () => this.handleItemClick(item), onKeyDown: e => this.handleKeyDown(e, item, index), onFocus: () => this.handleFocus(index), onBlur: this.handleBlur }, item.icon && (h("kritzel-icon", { name: item.icon, size: 20, class: "menu-item-icon" })), h("span", { class: "menu-item-label" }, item.label), h("span", { class: "menu-item-chevron", "aria-hidden": "true" }, h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("path", { d: "m9 18 6-6-6-6" }))))))), h("div", { key: 'e8f762ad87fdcf401ec717538bf1e979b22a9f15', id: panelId, class: "detail-panel", role: "tabpanel", "aria-labelledby": selectedTabId }, h("button", { key: '958360d57a1fad0953bc3865503e86c1d8aa24e3', class: "mobile-back-button", onClick: this.handleBackClick, "aria-label": "Back to menu" }, h("kritzel-icon", { key: 'c65eb947f80aafec99b367dae7ea72b1a77acdac', name: "chevron-left", size: 20, class: "mobile-back-icon" }), "Back"), h("slot", { key: '44557fddf513fffd86c7a1b28b71a41fef7701f1' })))));
31010
31064
  }
31011
31065
  static get watchers() { return {
31012
31066
  "selectedItemId": [{
@@ -31079,7 +31133,7 @@ const KritzelMenu = class {
31079
31133
  this.itemCloseChildMenu.emit(event.detail);
31080
31134
  };
31081
31135
  render() {
31082
- return (h(Host, { key: '27a90a3a8549517a2d6e341a5da13496bf714970', tabIndex: 0, onClick: e => e.stopPropagation() }, this.openChildMenuItem && h("div", { key: 'c0c2351fac389f6676b38481eaacee553ca6867a', class: "has-open-child-overlay", onClick: this.onOverlayClick }), this.items.map(item => (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 })))));
31136
+ return (h(Host, { key: '804ed34eb5340ecc1611ada01f608f7a0cf2f619', tabIndex: 0, onClick: e => e.stopPropagation() }, this.openChildMenuItem && h("div", { key: '6de5954a05b7f28961fa33cf8d34df3a81aae5f8', class: "has-open-child-overlay", onClick: this.onOverlayClick }), this.items.map(item => (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 })))));
31083
31137
  }
31084
31138
  };
31085
31139
  KritzelMenu.style = kritzelMenuCss();
@@ -31184,12 +31238,12 @@ const KritzelMenuItem = class {
31184
31238
  ];
31185
31239
  }
31186
31240
  render() {
31187
- return (h(Host, { key: '9d6653a630da5a3da38291d89d6d7d59d6994e02', tabIndex: this.item.isDisabled ? -1 : 0, class: {
31241
+ return (h(Host, { key: '9b35e5631cfa14aca391636f06598ef6f2f26550', tabIndex: this.item.isDisabled ? -1 : 0, class: {
31188
31242
  'selected': this.item.isSelected,
31189
31243
  'editing': this.item.isEditing,
31190
31244
  'disabled': this.item.isDisabled,
31191
31245
  'child-open': this.item.isChildMenuOpen,
31192
- }, onClick: this.handleItemSelect }, h("div", { key: '48a1678296e672c6c03fb3e1473c42d3a246d481', class: "menu-item-overlay" }), this.item.isEditing ? this.renderEditMode() : this.renderViewMode()));
31246
+ }, onClick: this.handleItemSelect }, h("div", { key: '175258699f76fdad5dcf3c7481e7882c6765bf33', class: "menu-item-overlay" }), this.item.isEditing ? this.renderEditMode() : this.renderViewMode()));
31193
31247
  }
31194
31248
  static get watchers() { return {
31195
31249
  "item": [{
@@ -31237,6 +31291,14 @@ const KritzelMoreMenu = class {
31237
31291
  itemSelect;
31238
31292
  menuAnchor = null;
31239
31293
  isTouchDevice = KritzelDevicesHelper.isTouchDevice();
31294
+ visibleItems = [];
31295
+ visibilityResolveVersion = 0;
31296
+ onItemsChange() {
31297
+ void this.resolveVisibleItems();
31298
+ }
31299
+ componentWillLoad() {
31300
+ void this.resolveVisibleItems();
31301
+ }
31240
31302
  toggleMenu = (event) => {
31241
31303
  event.stopPropagation();
31242
31304
  if (this.menuAnchor) {
@@ -31245,13 +31307,84 @@ const KritzelMoreMenu = class {
31245
31307
  else {
31246
31308
  window.dispatchEvent(new CustomEvent('kritzel-dismiss-menus'));
31247
31309
  this.menuAnchor = this.host;
31310
+ this.visibleItems = this.filterVisibleItemsSync(this.items ?? []);
31311
+ void this.resolveVisibleItems();
31248
31312
  }
31249
31313
  };
31250
31314
  closeMenu = () => {
31251
31315
  this.menuAnchor = null;
31252
31316
  };
31253
- get visibleItems() {
31254
- return this.items.filter(item => item.isVisible !== false);
31317
+ async evaluateVisibility(visibility) {
31318
+ if (typeof visibility === 'function') {
31319
+ try {
31320
+ return (await visibility()) !== false;
31321
+ }
31322
+ catch {
31323
+ return false;
31324
+ }
31325
+ }
31326
+ return visibility !== false;
31327
+ }
31328
+ evaluateVisibilitySync(visibility) {
31329
+ if (typeof visibility === 'function') {
31330
+ try {
31331
+ const result = visibility();
31332
+ return result instanceof Promise ? null : result !== false;
31333
+ }
31334
+ catch {
31335
+ return false;
31336
+ }
31337
+ }
31338
+ return visibility !== false;
31339
+ }
31340
+ filterVisibleItemsSync(items) {
31341
+ return items.reduce((visibleItems, item) => {
31342
+ const isVisible = this.evaluateVisibilitySync(item.isVisible);
31343
+ if (isVisible === null || !isVisible) {
31344
+ return visibleItems;
31345
+ }
31346
+ if (!item.children?.length) {
31347
+ visibleItems.push(item);
31348
+ return visibleItems;
31349
+ }
31350
+ visibleItems.push({
31351
+ ...item,
31352
+ children: this.filterVisibleItemsSync(item.children),
31353
+ });
31354
+ return visibleItems;
31355
+ }, []);
31356
+ }
31357
+ async filterVisibleItems(items) {
31358
+ const resolved = await Promise.all(items.map(async (item) => {
31359
+ const isVisible = await this.evaluateVisibility(item.isVisible);
31360
+ if (!isVisible) {
31361
+ return null;
31362
+ }
31363
+ if (!item.children?.length) {
31364
+ return item;
31365
+ }
31366
+ return {
31367
+ ...item,
31368
+ children: await this.filterVisibleItems(item.children),
31369
+ };
31370
+ }));
31371
+ return resolved.filter((item) => !!item);
31372
+ }
31373
+ async resolveVisibleItems() {
31374
+ const version = ++this.visibilityResolveVersion;
31375
+ const items = this.items ?? [];
31376
+ const resolvedItems = await this.filterVisibleItems(items);
31377
+ if (version !== this.visibilityResolveVersion) {
31378
+ return;
31379
+ }
31380
+ this.visibleItems = resolvedItems;
31381
+ }
31382
+ resolveMenuLabels(items) {
31383
+ return items.map(item => ({
31384
+ ...item,
31385
+ label: this.terms[item.label] ?? item.label,
31386
+ children: item.children ? this.resolveMenuLabels(item.children) : undefined,
31387
+ }));
31255
31388
  }
31256
31389
  handleMenuItemSelect = (event) => {
31257
31390
  const { item, parent } = event.detail;
@@ -31262,8 +31395,13 @@ const KritzelMoreMenu = class {
31262
31395
  this.closeMenu();
31263
31396
  };
31264
31397
  render() {
31265
- return (h(Host, { key: '96451062021ac2ebcb44b65384a7e9be67f1563b', class: { mobile: this.isTouchDevice }, style: { display: this.visible ? '' : 'none' } }, h("div", { key: 'e776398742284ea6984925d4a5a99e732674d640', class: { 'more-menu-wrapper': true, visible: this.visible } }, h("button", { key: '4371b2e5cbf7d35fac09a06ee2e54cd49072929b', class: "more-menu-button", "data-testid": "more-menu-button", onClick: this.toggleMenu, "aria-label": this.terms['moreMenu.ariaLabel'] ?? 'More options' }, h("kritzel-icon", { key: '076513ce393222b6f604036ec646a8309bc3a3ac', name: this.icon, size: this.iconSize })), h("kritzel-portal", { key: 'fde8071b3199735cc7827a2d6f2eaf79d0e1b997', anchor: this.menuAnchor, offsetY: this.offsetY, onClose: this.closeMenu }, h("kritzel-menu", { key: '2d49ad8a228c4b55a34c388b794bfdf500fec3c8', items: this.visibleItems, onItemSelect: this.handleMenuItemSelect })))));
31398
+ return (h(Host, { key: '3eaf5dac0beb1f08100c9f57f63a2bdbef6e592d', class: { mobile: this.isTouchDevice }, style: { display: this.visible ? '' : 'none' } }, h("div", { key: '30e4426b7d3a06ec45f0159f6403862bf4106723', class: { 'more-menu-wrapper': true, visible: this.visible } }, h("button", { key: '037b715fadad3b674f07bd9225acc4075b2d41b3', class: "more-menu-button", "data-testid": "more-menu-button", onClick: this.toggleMenu, "aria-label": this.terms['moreMenu.ariaLabel'] ?? 'More options' }, h("kritzel-icon", { key: 'b68b306b5b913ff36d220604a70626aa8468215c', name: this.icon, size: this.iconSize })), h("kritzel-portal", { key: 'dc86c2679956f2dfc66d000b183d99865291b6d3', anchor: this.menuAnchor, offsetY: this.offsetY, onClose: this.closeMenu }, h("kritzel-menu", { key: '9bd661fceea95df4d307e497d64365fcb854e8ce', items: this.resolveMenuLabels(this.visibleItems), onItemSelect: this.handleMenuItemSelect })))));
31266
31399
  }
31400
+ static get watchers() { return {
31401
+ "items": [{
31402
+ "onItemsChange": 0
31403
+ }]
31404
+ }; }
31267
31405
  };
31268
31406
  KritzelMoreMenu.style = kritzelMoreMenuCss();
31269
31407
 
@@ -31429,7 +31567,7 @@ const KritzelNumericInput = class {
31429
31567
  this.valueChange.emit(newValue);
31430
31568
  };
31431
31569
  render() {
31432
- return (h(Host, { key: '8901e0b56ccbc344420483a4a56663f7cd74d9e8' }, h("div", { key: '5f3d6b5c56007e11383aa69f60f22031b0e8f332', class: "input-container" }, this.label && h("label", { key: '2cb102a5b6c16d068f4f304fbcc6241f79fc0ce8', class: "input-label" }, this.label), h("div", { key: '96441095acc3d9a8e77b6d2bac2ceda683c9c976', class: "input-wrapper" }, 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 }), h("div", { key: '2071e98125a8d014513e93052bd8c1fee17932a5', class: "spinner-buttons" }, h("button", { key: '12422e5285dfb532dbbb6eb1a36dbcb3de50f2bc', type: "button", class: "spinner-button spinner-up", onClick: this.handleIncrement, tabIndex: -1, "aria-label": "Increase value" }, h("svg", { key: '49ad5625c80fae6706ba4ab91f47bd550481ef74', viewBox: "0 0 10 6", class: "spinner-icon" }, h("path", { key: 'ff7d19b61baeb5ba58bb60f2cc629bcec6eb4411', d: "M1 5L5 1L9 5", stroke: "currentColor", "stroke-width": "1.5", fill: "none", "stroke-linecap": "round", "stroke-linejoin": "round" }))), h("button", { key: 'e4fec28e4fc997e007127f2b30b00cc752433221', type: "button", class: "spinner-button spinner-down", onClick: this.handleDecrement, tabIndex: -1, "aria-label": "Decrease value" }, h("svg", { key: '27bf06cde8de11f4a97090911afd6b8993463112', viewBox: "0 0 10 6", class: "spinner-icon" }, h("path", { key: 'ed7071dd79bfe509ebb319ce43ecb3e982dd4157', d: "M1 1L5 5L9 1", stroke: "currentColor", "stroke-width": "1.5", fill: "none", "stroke-linecap": "round", "stroke-linejoin": "round" }))))))));
31570
+ return (h(Host, { key: '68e6fecd79c3c970d1742b1a0bb8667ca2d31729' }, h("div", { key: 'efcf623186d221690bbb956088ec51343de07f3a', class: "input-container" }, this.label && h("label", { key: '300de793f02008ad037da021003b46fc34fc5239', class: "input-label" }, this.label), h("div", { key: '9ff424901fddfcbdac4835e2b2089df2e0966d0f', class: "input-wrapper" }, 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 }), h("div", { key: 'c286606fa0617cc3acbbe40a27e40b154002e01d', class: "spinner-buttons" }, h("button", { key: 'e3a65a327975918fe604cd196ffb087fc4d3ca47', type: "button", class: "spinner-button spinner-up", onClick: this.handleIncrement, tabIndex: -1, "aria-label": "Increase value" }, h("svg", { key: '62b2ff4ed67ca8b6fc220ff9c63ceca90f75baea', viewBox: "0 0 10 6", class: "spinner-icon" }, h("path", { key: '803d5c30826d40d95f282071f3f88356366a9faf', d: "M1 5L5 1L9 5", stroke: "currentColor", "stroke-width": "1.5", fill: "none", "stroke-linecap": "round", "stroke-linejoin": "round" }))), h("button", { key: '3c54a48e7082f5aaa9ed923357d0cc4a722018b5', type: "button", class: "spinner-button spinner-down", onClick: this.handleDecrement, tabIndex: -1, "aria-label": "Decrease value" }, h("svg", { key: '9f6cb5f34bbb0e70b5773aecec519b7d8b742712', viewBox: "0 0 10 6", class: "spinner-icon" }, h("path", { key: '6539b0d4f4bd26e463fa9152cf58757497355cf6', d: "M1 1L5 5L9 1", stroke: "currentColor", "stroke-width": "1.5", fill: "none", "stroke-linecap": "round", "stroke-linejoin": "round" }))))))));
31433
31571
  }
31434
31572
  static get watchers() { return {
31435
31573
  "value": [{
@@ -31468,7 +31606,7 @@ const KritzelOpacitySlider = class {
31468
31606
  }
31469
31607
  render() {
31470
31608
  const percentage = this.getPercentage();
31471
- return (h(Host, { key: '2dadd8a7c4d1af748e91a6005433288ab231f2e8' }, h("div", { key: '65ae90d95bffff244d97329754cf0cb8f73331fa', class: "opacity-container" }, h("div", { key: '90e95347f7015882112f0653a25347e715233227', class: "slider-wrapper" }, 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: {
31609
+ return (h(Host, { key: '895ae10ad71e846a1bc62b794751332816650727' }, h("div", { key: '22ea167ce0d6eb17c733d789cde10477c48a2a0e', class: "opacity-container" }, h("div", { key: '2be4fbf8c1f8ddb1fde8f9da727ed0947d21646c', class: "slider-wrapper" }, 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: {
31472
31610
  '--slider-progress': `${percentage}%`,
31473
31611
  '--kritzel-opacity-slider-thumb-border-color': this.previewColor,
31474
31612
  } })))));
@@ -31821,7 +31959,7 @@ const KritzelPortal = class {
31821
31959
  this.portal.style.visibility = 'visible';
31822
31960
  }
31823
31961
  render() {
31824
- return (h(Host, { key: '72273b4c7811473072ee27668836a5ddddcfd618', style: { display: this.anchor ? 'block' : 'none' } }, h("slot", { key: 'b4f37ca27c47ab950a4639162fa31a3c2362d6f7' })));
31962
+ return (h(Host, { key: '489a752e48d96c2607defa40a39477675685b60a', style: { display: this.anchor ? 'block' : 'none' } }, h("slot", { key: '13c6b7567039b8c8c78de27e3d9aeda30a798de1' })));
31825
31963
  }
31826
31964
  static get watchers() { return {
31827
31965
  "anchor": [{
@@ -31835,7 +31973,7 @@ const KritzelPortal = class {
31835
31973
  * This file is auto-generated by the version bump scripts.
31836
31974
  * Do not modify manually.
31837
31975
  */
31838
- const KRITZEL_VERSION = '0.4.8';
31976
+ const KRITZEL_VERSION = '0.4.10';
31839
31977
 
31840
31978
  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)}`;
31841
31979
 
@@ -32047,7 +32185,7 @@ const KritzelSettings = class {
32047
32185
  return (h("div", { class: "settings-categories-wrapper" }, h("div", { class: { 'settings-tab-panel': true, 'is-active': this.selectedCategoryId === 'general' } }, h("div", { class: "settings-content" }, h("h3", null, this.t('settings.general.title', 'General Settings')), h("div", { class: "settings-group" }, h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.general.theme.label', 'Theme')), h("p", { class: "settings-description" }, this.t('settings.general.theme.description', 'Select a registered color theme for the editor interface.')), h("kritzel-dropdown", { options: this.availableThemes.map(t => ({ value: t, label: t })), value: this.theme, onValueChanged: this.handleThemeChange })), this.availableLocales.length > 0 && (h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.general.language.label', 'Language')), h("p", { class: "settings-description" }, this.t('settings.general.language.description', 'Select the display language for the editor interface.')), h("kritzel-dropdown", { options: this.availableLocales.map(l => ({ value: l.code, label: l.label })), value: this.locale, onValueChanged: this.handleLocaleChange }))), h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.general.lockDrawingScale.label', 'Lock Drawing Scale')), 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.')), h("kritzel-slide-toggle", { checked: this.lockDrawingScale, label: this.t('settings.general.lockDrawingScale.label', 'Lock Drawing Scale'), onCheckedChange: this.handleLockDrawingScaleChange }))))), h("div", { class: { 'settings-tab-panel': true, 'is-active': this.selectedCategoryId === 'viewport' } }, h("div", { class: "settings-content" }, h("h3", null, this.t('settings.viewport.title', 'Viewport Settings')), h("div", { class: "settings-group" }, h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.viewport.minZoom.label', 'Minimum Zoom Level')), 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.')), h("kritzel-numeric-input", { value: this.scaleMin, min: 0.0001, max: 1, step: 0.0001, onValueChange: this.handleScaleMinChange })), h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.viewport.maxZoom.label', 'Maximum Zoom Level')), 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.')), h("kritzel-numeric-input", { value: this.scaleMax, min: 1, max: 1000, step: 1, onValueChange: this.handleScaleMaxChange })), h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.viewport.boundaryLeft.label', 'Viewport Boundary Left')), 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.')), h("kritzel-numeric-input", { value: this.viewportBoundaryLeft, step: 100, placeholder: this.t('settings.viewport.boundaryPlaceholder', 'Infinite'), onValueChange: this.handleViewportBoundaryLeftChange })), h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.viewport.boundaryRight.label', 'Viewport Boundary Right')), 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.')), h("kritzel-numeric-input", { value: this.viewportBoundaryRight, step: 100, placeholder: this.t('settings.viewport.boundaryPlaceholder', 'Infinite'), onValueChange: this.handleViewportBoundaryRightChange })), h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.viewport.boundaryTop.label', 'Viewport Boundary Top')), 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.')), h("kritzel-numeric-input", { value: this.viewportBoundaryTop, step: 100, placeholder: this.t('settings.viewport.boundaryPlaceholder', 'Infinite'), onValueChange: this.handleViewportBoundaryTopChange })), h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.viewport.boundaryBottom.label', 'Viewport Boundary Bottom')), 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.')), h("kritzel-numeric-input", { value: this.viewportBoundaryBottom, step: 100, placeholder: this.t('settings.viewport.boundaryPlaceholder', 'Infinite'), onValueChange: this.handleViewportBoundaryBottomChange }))))), h("div", { class: { 'settings-tab-panel': true, 'is-active': this.selectedCategoryId === 'shortcuts' } }, h("div", { class: "settings-content" }, h("h3", null, this.t('settings.shortcuts.title', 'Keyboard Shortcuts')), h("div", { class: "shortcuts-list" }, Array.from(this.groupShortcutsByCategory()).map(([category, shortcuts]) => (h("div", { class: "shortcuts-category", key: category }, h("h4", { class: "shortcuts-category-title" }, category), h("div", { class: "shortcuts-group" }, shortcuts.map(shortcut => (h("div", { class: "shortcut-item", key: shortcut.key + shortcut.label }, h("span", { class: "shortcut-label" }, shortcut.label), h("kbd", { class: "shortcut-key" }, this.formatKeyCombo(shortcut)))))))))))), h("div", { class: { 'settings-tab-panel': true, 'is-active': this.selectedCategoryId === 'developer' } }, h("div", { class: "settings-content" }, h("h3", null, this.t('settings.developer.title', 'Developer Options')), h("div", { class: "settings-group" }, h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.developer.showViewportInfo.label', 'Show Viewport Info')), h("p", { class: "settings-description" }, this.t('settings.developer.showViewportInfo.description', 'Display viewport debug information such as position, zoom level, and boundaries.')), h("kritzel-slide-toggle", { checked: this.debugInfo.showViewportInfo, label: this.t('settings.developer.showViewportInfo.label', 'Show Viewport Info'), onCheckedChange: this.handleDebugInfoChange('showViewportInfo') })), h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.developer.showObjectInfo.label', 'Show Object Info')), h("p", { class: "settings-description" }, this.t('settings.developer.showObjectInfo.description', 'Display debug information about objects on the canvas.')), h("kritzel-slide-toggle", { checked: this.debugInfo.showObjectInfo, label: this.t('settings.developer.showObjectInfo.label', 'Show Object Info'), onCheckedChange: this.handleDebugInfoChange('showObjectInfo') })), h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.developer.showSyncProviderInfo.label', 'Show Sync Provider Info')), h("p", { class: "settings-description" }, this.t('settings.developer.showSyncProviderInfo.description', 'Display debug information about the sync provider connection status.')), h("kritzel-slide-toggle", { checked: this.debugInfo.showSyncProviderInfo, label: this.t('settings.developer.showSyncProviderInfo.label', 'Show Sync Provider Info'), onCheckedChange: this.handleDebugInfoChange('showSyncProviderInfo') })), h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.developer.showMigrationInfo.label', 'Show Migration Info')), h("p", { class: "settings-description" }, this.t('settings.developer.showMigrationInfo.description', 'Display debug information about data migrations.')), h("kritzel-slide-toggle", { checked: this.debugInfo.showMigrationInfo, label: this.t('settings.developer.showMigrationInfo.label', 'Show Migration Info'), onCheckedChange: this.handleDebugInfoChange('showMigrationInfo') }))))), h("div", { class: { 'settings-tab-panel': true, 'is-active': this.selectedCategoryId === 'about' } }, h("div", { class: "settings-content" }, h("h3", null, this.t('settings.about.title', 'About')), h("p", null, this.t('settings.about.description', 'Kritzel - A drawing application')), h("p", { class: "version-info" }, "Version ", KRITZEL_VERSION), h("p", { class: "version-info" }, "App-State Schema v", CURRENT_APP_STATE_SCHEMA_VERSION), h("p", { class: "version-info" }, "Workspace Schema v", CURRENT_WORKSPACE_SCHEMA_VERSION)))));
32048
32186
  }
32049
32187
  render() {
32050
- return (h(Host, { key: '36bca8be04b34a37630d6aa5394595a8b394d180' }, h("kritzel-dialog", { key: '1e257904178fd9afa2bfbe4a758663fb366c8847', isOpen: this.isDialogOpen, dialogTitle: this.t('settings.dialogTitle', 'Settings'), size: "large", contained: true, onDialogClose: this.closeDialog }, h("kritzel-master-detail", { key: 'b7d9ad97d9b126ccf6e800e6c9032749635eb988', items: this.categories, selectedItemId: this.selectedCategoryId, onItemSelect: this.handleCategorySelect }, this.renderCategoryContent()))));
32188
+ return (h(Host, { key: '71f8a12325a781212dd6f2c8b794563fcb4eee64' }, h("kritzel-dialog", { key: '1abe7c1dedcf79433a95d60dadce342f46e62c3a', isOpen: this.isDialogOpen, dialogTitle: this.t('settings.dialogTitle', 'Settings'), size: "large", contained: true, onDialogClose: this.closeDialog }, h("kritzel-master-detail", { key: '9e8b3a11d97c1a10c6693434eb2e8bfc1d3fda7d', items: this.categories, selectedItemId: this.selectedCategoryId, onItemSelect: this.handleCategorySelect }, this.renderCategoryContent()))));
32051
32189
  }
32052
32190
  static get watchers() { return {
32053
32191
  "settings": [{
@@ -35287,9 +35425,9 @@ const KritzelShareDialog = class {
35287
35425
  this.dialogClosed.emit();
35288
35426
  };
35289
35427
  render() {
35290
- return (h(Host, { key: 'd30a468b9443f76731b207cb5caec1911f378c15' }, h("kritzel-dialog", { key: '4a4ec0a2becba58a49a87fc411a82e26557d8232', dialogTitle: this.terms['share.dialogTitle'] ?? 'Share Workspace', size: "small", isOpen: this.isDialogOpen, onDialogClose: this.closeDialog, contained: true }, h("div", { key: '0b2fec3db6c039752429768fb4bd6a2150a4345e', class: "share-content" }, h("div", { key: 'bd0b85f8418d0c46d14b718024ed62b9f72e4f52', class: "share-section" }, h("div", { key: 'bf62503a593454ffacd30d532fcd79dcbf938659', class: "share-row" }, h("div", { key: 'dfd9e83166b886d00fb5b097d0ccd77169c011bd', class: "share-label-group" }, h("label", { key: '67dba950ded47a08eb006c1628d14038b18171b5', class: "share-label" }, this.terms['share.linkSharing.label'] ?? 'Link sharing'), h("p", { key: '926f8670b35dab1e3e2a6496f44b5d6e0ee46417', class: "share-description" }, this.internalIsPublic
35428
+ return (h(Host, { key: 'f6028ad618f04e3be65c816f4b53c4ccaa93fc71' }, h("kritzel-dialog", { key: '7dabab65955f287650de4ba484e37f4ccdd7c57e', dialogTitle: this.terms['share.dialogTitle'] ?? 'Share Workspace', size: "small", isOpen: this.isDialogOpen, onDialogClose: this.closeDialog, contained: true }, h("div", { key: '321e2b17048dea20528c0f34f39a462548c64689', class: "share-content" }, h("div", { key: 'b31bbe2f686f5fe2fdc03189ffb98082683fff63', class: "share-section" }, h("div", { key: '2c864e00aa4c2da0a7f8ee6c83f149e6e93610a5', class: "share-row" }, h("div", { key: 'cbee03ac7ec9ee82f22c77d3681d4a540a0780a3', class: "share-label-group" }, h("label", { key: '3be7c8dc2ac2f7dbfe6458a902b14a887895fdc3', class: "share-label" }, this.terms['share.linkSharing.label'] ?? 'Link sharing'), h("p", { key: 'c7401bc76fdccbafc6c7c4e37380fc75f35b3a99', class: "share-description" }, this.internalIsPublic
35291
35429
  ? (this.terms['share.linkSharing.enabledDescription'] ?? 'Anyone with the link can access this workspace.')
35292
- : (this.terms['share.linkSharing.disabledDescription'] ?? 'Link sharing is disabled. Only you can access this workspace.'))), h("kritzel-slide-toggle", { key: 'e42fabecb37fd21cc4907b966e2f50113b945eda', checked: this.internalIsPublic, onCheckedChange: this.handleToggleChange, label: this.terms['share.linkSharing.toggleLabel'] ?? 'Enable link sharing' }))), this.internalIsPublic && (h("div", { key: '40afacc81e0bbcf4c404ae32491e5142a32bca66', class: "share-section" }, h("div", { key: '57b6952670b1cbc789770362b573623035b50cf3', class: "share-url-container" }, h("input", { key: '0ee836655ca26c1e073d5d86014322939527c74b', type: "text", class: "share-url-input", value: this.getShareUrl(), readOnly: true, onClick: (e) => e.target.select() }), 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') }, h("kritzel-icon", { key: 'b43bde94601c352b6065c89c026807d7efb3bcc3', name: this.copySuccess ? 'check' : 'copy', size: 18 }))), this.qrCodeDataUrl && (h("div", { key: 'f4d657aa918abbb3cf3ed039332d8c67247b83e8', class: "share-qr-container" }, h("img", { key: 'b165a446df3fba87918a19928734d39af1e6bfcf', class: "share-qr-image", src: this.qrCodeDataUrl, alt: "QR code for share link" })))))))));
35430
+ : (this.terms['share.linkSharing.disabledDescription'] ?? 'Link sharing is disabled. Only you can access this workspace.'))), h("kritzel-slide-toggle", { key: '52ae14da0d79797e9ad73f1ffbac003fd1395f47', checked: this.internalIsPublic, onCheckedChange: this.handleToggleChange, label: this.terms['share.linkSharing.toggleLabel'] ?? 'Enable link sharing' }))), this.internalIsPublic && (h("div", { key: 'bd44618680db14d7d95f0c1bbc4441a103e9f15a', class: "share-section" }, h("div", { key: '659cd6b2e2e719d76f692a8dc5a8c0855e5d246d', class: "share-url-container" }, h("input", { key: '76be432ecaad753cb7043aad912d8aa1770afb61', type: "text", class: "share-url-input", value: this.getShareUrl(), readOnly: true, onClick: (e) => e.target.select() }), 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') }, h("kritzel-icon", { key: '06bbdc32d4348ca6d75dbbe11ba74d94e1c86882', name: this.copySuccess ? 'check' : 'copy', size: 18 }))), this.qrCodeDataUrl && (h("div", { key: '9246e86b26a9794738a29e6a6bfe6c4812339449', class: "share-qr-container" }, h("img", { key: '90e18eee230c23e0bfd773c9b8a6967803ae10dc', class: "share-qr-image", src: this.qrCodeDataUrl, alt: "QR code for share link" })))))))));
35293
35431
  }
35294
35432
  static get watchers() { return {
35295
35433
  "isPublic": [{
@@ -35330,7 +35468,7 @@ const KritzelSlideToggle = class {
35330
35468
  }
35331
35469
  };
35332
35470
  render() {
35333
- return (h(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 }, h("div", { key: '385008948ffbdc0f26249d2d833aae8f04eeb6a0', class: "toggle-track" }, h("div", { key: '43929e94335589feb24ee45f1c34a270653dd412', class: "toggle-thumb" }))));
35471
+ return (h(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 }, h("div", { key: 'fef37a1b373bf438cbacbf6537640269c2ab6b59', class: "toggle-track" }, h("div", { key: '4e140f83a8802e04c0b42e76f5e5719efd5a1cbc', class: "toggle-thumb" }))));
35334
35472
  }
35335
35473
  };
35336
35474
  KritzelSlideToggle.style = kritzelSlideToggleCss();
@@ -35430,7 +35568,7 @@ const KritzelSplitButton = class {
35430
35568
  this.menuScrollTop = event.target.scrollTop;
35431
35569
  };
35432
35570
  render() {
35433
- return (h(Host, { key: 'fa22dc1185ef07d8127d9ee9f6dbe482d126f934', class: { mobile: this.isTouchDevice } }, h("button", { key: 'a48aba702b70e9b24ad5486dde61fa9f9c8f7cdc', class: "split-main-button", tabIndex: 0, onClick: this.handleButtonClick, disabled: this.mainButtonDisabled, "aria-label": "Main action" }, this.buttonIcon && h("kritzel-icon", { key: '0d1c9a3e9f744541dd6ebe491105f6ee65233543', name: this.buttonIcon })), h("div", { key: 'c9db29f0b9c9cf1974d78f4f60be9e1b0e26c5d2', class: "split-divider" }), 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" }, h("kritzel-icon", { key: 'd860cc503043425860b1234b5d34693bfdd1d5ab', name: this.dropdownIcon })), h("kritzel-portal", { key: 'cd6dda8869aea4205d922fc639073019bbd2b102', anchor: this.anchorElement, offsetY: 4, onClose: this.closeMenu }, 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 }))));
35571
+ return (h(Host, { key: 'f7f5019b32f4514a4ccf5bd07742658e3d05020a', class: { mobile: this.isTouchDevice } }, h("button", { key: '361007b4281edfe2bf18f28f7c4f19ba1f251b49', class: "split-main-button", tabIndex: 0, onClick: this.handleButtonClick, disabled: this.mainButtonDisabled, "aria-label": "Main action" }, this.buttonIcon && h("kritzel-icon", { key: '17d4fad6cfa070225daa67dfc648d4ac64091e90', name: this.buttonIcon })), h("div", { key: 'c18107b7d557525092c28ff5d7336ebb3648b672', class: "split-divider" }), 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" }, h("kritzel-icon", { key: '5b40284c8f04fba80ca606b0af201040efe53880', name: this.dropdownIcon })), h("kritzel-portal", { key: '62bd7f26c020497f2131759d0d1ef9d18e817b37', anchor: this.anchorElement, offsetY: 4, onClose: this.closeMenu }, 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 }))));
35434
35572
  }
35435
35573
  };
35436
35574
  KritzelSplitButton.style = kritzelSplitButtonCss();
@@ -35451,7 +35589,7 @@ const KritzelStrokeSize = class {
35451
35589
  }
35452
35590
  render() {
35453
35591
  const sizes = this.sizes ?? [];
35454
- return (h(Host, { key: 'ba181d798d2bf7b60308746635d2077ce1353b83' }, h("div", { key: '048fc8934190359b9337747567844ca88925a6ce', class: "size-grid" }, sizes.map(size => (h("div", { tabIndex: 0, class: {
35592
+ return (h(Host, { key: 'c0307e527e6f9d5d663ae3f8f605162cdde2a339' }, h("div", { key: '9533e0486f8078d25b7e8fc3323126d77700ac72', class: "size-grid" }, sizes.map(size => (h("div", { tabIndex: 0, class: {
35455
35593
  'size-container': true,
35456
35594
  'selected': this.selectedSize === size,
35457
35595
  }, onClick: () => this.handleSizeClick(size) }, h("kritzel-color", { value: 'var(--kritzel-global-text-primary)', size: size })))))));
@@ -35459,7 +35597,7 @@ const KritzelStrokeSize = class {
35459
35597
  };
35460
35598
  KritzelStrokeSize.style = kritzelStrokeSizeCss();
35461
35599
 
35462
- 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)}}`;
35600
+ 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)}}`;
35463
35601
 
35464
35602
  const KritzelSyncIndicator = class {
35465
35603
  constructor(hostRef) {
@@ -35470,7 +35608,7 @@ const KritzelSyncIndicator = class {
35470
35608
  /** The text displayed next to the spinner. */
35471
35609
  text = 'Syncing...';
35472
35610
  render() {
35473
- return (h(Host, { key: '585d5fd13116c093e5221164e36e6e986a63da58' }, h("div", { key: 'd4e0d9e51db002b8ea201a0564c22916809c346d', class: { indicator: true, visible: this.visible }, role: "status", "aria-live": "polite", "aria-label": "Synchronizing workspace" }, h("span", { key: 'b82f4c363ee31ae697e435fc8eb6ce0e2f3df1ab', class: "spinner", "aria-hidden": "true" }), h("span", { key: 'f39e04332aaf622dad69d0e5d6b6e90ee5df3ed1' }, this.text))));
35611
+ return (h(Host, { key: '05e825ee1c08fe02008b8e1adfe9e09a7996f221' }, h("div", { key: 'ea1635896f643c16713d35d6efd3abf486b1fbe2', class: { indicator: true, visible: this.visible }, role: "status", "aria-live": "polite", "aria-label": "Synchronizing workspace" }, h("span", { key: '7b57ea67d6f2c40dc74e966c16ebe9d95c75394a', class: "spinner", "aria-hidden": "true" }), h("span", { key: 'fc16405b02d96213d0025239b6e863883192ada5' }, this.text))));
35474
35612
  }
35475
35613
  };
35476
35614
  KritzelSyncIndicator.style = kritzelSyncIndicatorCss();
@@ -35883,14 +36021,14 @@ const KritzelTooltip = class {
35883
36021
  }
35884
36022
  }
35885
36023
  render() {
35886
- return (h(Host, { key: '227146703158aa65ecffcce348e568d559cee829', style: {
36024
+ return (h(Host, { key: '93486d37c5dcaaf9a3d0d1c53d6308ddd5712560', style: {
35887
36025
  position: 'fixed',
35888
36026
  zIndex: '9999',
35889
36027
  transition: 'opacity 0.3s ease-in-out, transform 0.3s ease-in-out',
35890
36028
  visibility: this.isVisible ? 'visible' : 'hidden',
35891
36029
  left: `${this.positionX}px`,
35892
36030
  bottom: `${this.positionY}px`,
35893
- } }, h("div", { key: '573e97ed81920066e8fbf50f790224c23b37ec21', class: "tooltip-content", onClick: event => event.stopPropagation(), onPointerDown: event => event.stopPropagation(), onMouseDown: event => event.stopPropagation() }, h("slot", { key: 'c489b45928cf6720c6827df73b9eedf08bc9d5e2' }))));
36031
+ } }, h("div", { key: '4b5cf451f75d09459145b874eb3c7faffafa87e0', class: "tooltip-content", onClick: event => event.stopPropagation(), onPointerDown: event => event.stopPropagation(), onMouseDown: event => event.stopPropagation() }, h("slot", { key: '6c7fda100473a7d4fc533edc52d67bf6909bdc0e' }))));
35894
36032
  }
35895
36033
  static get watchers() { return {
35896
36034
  "triggerElement": [{
@@ -35931,7 +36069,7 @@ const KritzelUtilityPanel = class {
35931
36069
  this.redo.emit();
35932
36070
  }
35933
36071
  render() {
35934
- return (h(Host, { key: '3e86e2c4c480ab56f4646a4f288ad37a63e29642' }, 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' }, h("kritzel-icon", { key: '5a2b903cc3e5f9c554928f11a1c3098d569cbc4d', name: "undo" })), 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' }, h("kritzel-icon", { key: '19209a95787f4475eda9dc0712c2bf36a573c953', name: "redo" })), h("div", { key: '7a463d5ac1f78017fb728abcd2c33061d8545978', class: "utility-separator" }), h("button", { key: '155c2bb50d70223e4f4d5377bacced56ffda68ad', class: "utility-button", "data-testid": "utility-delete", onClick: () => this.delete.emit(), "aria-label": this.terms['utility.delete'] ?? 'Delete selected items' }, h("kritzel-icon", { key: '47bd8469b9efe395e9185b3e7375c7375811c0aa', name: "delete" }))));
36072
+ return (h(Host, { key: '57c1a6746e4d5b319bb368c9d0bfc52a31d8c8de' }, 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' }, h("kritzel-icon", { key: '5ed2c36405ac7113ac697de9102aaf7067127e2a', name: "undo" })), 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' }, h("kritzel-icon", { key: '70264118cbe0f54335a02941fdc868f63476cd1c', name: "redo" })), h("div", { key: '6eed6a603b8bc62e55b1f6fe82e7d035d1f5833c', class: "utility-separator" }), h("button", { key: 'd11195da7a86496afcca26105a6a0f066b1b2340', class: "utility-button", "data-testid": "utility-delete", onClick: () => this.delete.emit(), "aria-label": this.terms['utility.delete'] ?? 'Delete selected items' }, h("kritzel-icon", { key: '5e4c731f90863b4db917c67ac7e87c2945c7bcaa', name: "delete" }))));
35935
36073
  }
35936
36074
  };
35937
36075
  KritzelUtilityPanel.style = kritzelUtilityPanelCss();
@@ -35952,7 +36090,7 @@ const KritzelWatermark = class {
35952
36090
  label;
35953
36091
  render() {
35954
36092
  const label = this.label ?? this.core.localizationManager.translate('watermark.poweredBy');
35955
- return (h(Host, { key: '0eccc75e51c7ac199e23d8f3a871593f1fb65a17' }, 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)));
36093
+ return (h(Host, { key: 'ae00d98f16912afcb7a90d47a9fc4d55fc7468f0' }, 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)));
35956
36094
  }
35957
36095
  };
35958
36096
  KritzelWatermark.style = kritzelWatermarkCss();
@@ -36140,7 +36278,7 @@ const KritzelZoomPanel = class {
36140
36278
  return Number.isFinite(this.zoomPercent) ? Math.max(1, Math.round(this.zoomPercent)) : 100;
36141
36279
  }
36142
36280
  render() {
36143
- return (h(Host, { key: '054e24893ed609c29b02cf574a68cf9311a3f025' }, h("div", { key: '42ad21dca9486ae350d5814b949e0c577a6ed996', class: { panel: true, visible: this.visible } }, h("button", { key: 'e360884e5a2edaada46d232cae8121f2c5f45bb6', class: "zoom-button", type: "button", "aria-label": this.terms['zoom.zoomOut'] ?? 'Zoom out', disabled: this.disabled, onClick: this.handleZoomOut }, h("kritzel-icon", { key: '8c8bb23c4ec4b78fd04aee5c0edd3371c4d14125', name: "minus" })), h("span", { key: '42ab93551c2fbb6d8d4ef076d66405277ba941bf', class: "zoom-level", "aria-live": "polite" }, this.normalizedZoomPercent, "%"), h("button", { key: '232f0f74b748999c6892549978bbe085636ca189', class: "zoom-button", type: "button", "aria-label": this.terms['zoom.zoomIn'] ?? 'Zoom in', disabled: this.disabled, onClick: this.handleZoomIn }, h("kritzel-icon", { key: '6f9b8ddd4c666573d9716d7bc455c2a3b1035ede', name: "plus" })))));
36281
+ return (h(Host, { key: 'ac5b3f37fbc298b1522f0ff80c26734164b5358a' }, h("div", { key: '8ac3f3600c45c064d9c4c4c9efa78397790edf4b', class: { panel: true, visible: this.visible } }, h("button", { key: '63b2a3fce804a310146d95cc9c2a874707f9e265', class: "zoom-button", type: "button", "aria-label": this.terms['zoom.zoomOut'] ?? 'Zoom out', disabled: this.disabled, onClick: this.handleZoomOut }, h("kritzel-icon", { key: '86ebb85dfc2b746cedd0ed63cd2ae90378f6bbb8', name: "minus" })), h("span", { key: '5191cc1aa9966234c1634882af145403acd32211', class: "zoom-level", "aria-live": "polite" }, this.normalizedZoomPercent, "%"), h("button", { key: '47a7ee5b6c1cf1c99cd1826877e0a1c4ce224442', class: "zoom-button", type: "button", "aria-label": this.terms['zoom.zoomIn'] ?? 'Zoom in', disabled: this.disabled, onClick: this.handleZoomIn }, h("kritzel-icon", { key: '09782e094534fbc5bbcc612f24eea359568d6132', name: "plus" })))));
36144
36282
  }
36145
36283
  };
36146
36284
  KritzelZoomPanel.style = kritzelZoomPanelCss();