kritzel-stencil 0.3.16 → 0.3.18

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 (227) hide show
  1. package/LICENSE.md +50 -0
  2. package/dist/cjs/index-Xav9JFHg.js +2 -2
  3. package/dist/cjs/index.cjs.js +7 -1
  4. package/dist/cjs/{kritzel-active-users_42.cjs.entry.js → kritzel-active-users_44.cjs.entry.js} +710 -145
  5. package/dist/cjs/loader.cjs.js +1 -1
  6. package/dist/cjs/{schema.constants-DJQTjcy7.js → schema.constants-DrHO_CYF.js} +1169 -171
  7. package/dist/cjs/stencil.cjs.js +1 -1
  8. package/dist/collection/classes/core/core.class.js +24 -0
  9. package/dist/collection/classes/handlers/context-menu.handler.js +24 -2
  10. package/dist/collection/classes/managers/license.manager.js +285 -0
  11. package/dist/collection/classes/managers/localization.manager.js +189 -0
  12. package/dist/collection/classes/objects/custom-element.class.js +2 -0
  13. package/dist/collection/classes/objects/group.class.js +7 -2
  14. package/dist/collection/classes/objects/image.class.js +10 -7
  15. package/dist/collection/classes/objects/line.class.js +3 -0
  16. package/dist/collection/classes/objects/path.class.js +13 -12
  17. package/dist/collection/classes/objects/selection-group.class.js +7 -2
  18. package/dist/collection/classes/objects/shape.class.js +3 -0
  19. package/dist/collection/classes/objects/text.class.js +4 -1
  20. package/dist/collection/classes/registries/icon-registry.class.js +1 -0
  21. package/dist/collection/classes/tools/brush-tool.class.js +1 -1
  22. package/dist/collection/collection-manifest.json +3 -1
  23. package/dist/collection/components/core/kritzel-editor/kritzel-editor.css +16 -0
  24. package/dist/collection/components/core/kritzel-editor/kritzel-editor.js +462 -60
  25. package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +446 -16
  26. package/dist/collection/components/core/kritzel-watermark/kritzel-watermark.css +29 -0
  27. package/dist/collection/components/core/kritzel-watermark/kritzel-watermark.js +83 -0
  28. package/dist/collection/components/shared/kritzel-avatar/kritzel-avatar.js +3 -3
  29. package/dist/collection/components/shared/kritzel-button/kritzel-button.js +2 -2
  30. package/dist/collection/components/shared/kritzel-color/kritzel-color.js +2 -2
  31. package/dist/collection/components/shared/kritzel-color-palette/kritzel-color-palette.js +1 -1
  32. package/dist/collection/components/shared/kritzel-font/kritzel-font.js +1 -1
  33. package/dist/collection/components/shared/kritzel-font-size/kritzel-font-size.js +2 -1
  34. package/dist/collection/components/shared/kritzel-input/kritzel-input.js +1 -1
  35. package/dist/collection/components/shared/kritzel-master-detail/kritzel-master-detail.js +3 -3
  36. package/dist/collection/components/shared/kritzel-menu/kritzel-menu.js +1 -1
  37. package/dist/collection/components/shared/kritzel-menu-item/kritzel-menu-item.js +2 -2
  38. package/dist/collection/components/shared/kritzel-numeric-input/kritzel-numeric-input.js +1 -1
  39. package/dist/collection/components/shared/kritzel-opacity-slider/kritzel-opacity-slider.js +1 -1
  40. package/dist/collection/components/shared/kritzel-portal/kritzel-portal.js +1 -1
  41. package/dist/collection/components/shared/kritzel-slide-toggle/kritzel-slide-toggle.js +1 -1
  42. package/dist/collection/components/shared/kritzel-split-button/kritzel-split-button.js +1 -1
  43. package/dist/collection/components/shared/kritzel-stroke-size/kritzel-stroke-size.js +2 -1
  44. package/dist/collection/components/shared/kritzel-tooltip/kritzel-tooltip.js +2 -2
  45. package/dist/collection/components/ui/kritzel-back-to-content/kritzel-back-to-content.js +1 -1
  46. package/dist/collection/components/ui/kritzel-controls/kritzel-controls.js +41 -6
  47. package/dist/collection/components/ui/kritzel-current-user/kritzel-current-user.js +36 -1
  48. package/dist/collection/components/ui/kritzel-current-user-dialog/kritzel-current-user-dialog.js +36 -1
  49. package/dist/collection/components/ui/kritzel-export/kritzel-export.js +44 -7
  50. package/dist/collection/components/ui/kritzel-login-dialog/kritzel-login-dialog.js +1 -1
  51. package/dist/collection/components/ui/kritzel-more-menu/kritzel-more-menu.js +36 -1
  52. package/dist/collection/components/ui/kritzel-settings/kritzel-settings.js +108 -14
  53. package/dist/collection/components/ui/kritzel-share-dialog/kritzel-share-dialog.js +38 -3
  54. package/dist/collection/components/ui/kritzel-tool-config/kritzel-tool-config.js +38 -3
  55. package/dist/collection/components/ui/kritzel-utility-panel/kritzel-utility-panel.js +36 -1
  56. package/dist/collection/components/ui/kritzel-workspace-manager/kritzel-workspace-manager.js +38 -3
  57. package/dist/collection/components/ui/kritzel-zoom-panel/kritzel-zoom-panel.css +72 -0
  58. package/dist/collection/components/ui/kritzel-zoom-panel/kritzel-zoom-panel.js +173 -0
  59. package/dist/collection/constants/engine.constants.js +2 -0
  60. package/dist/collection/constants/license.constants.js +25 -0
  61. package/dist/collection/constants/version.js +1 -1
  62. package/dist/collection/helpers/localization.helper.js +25 -0
  63. package/dist/collection/helpers/math.helper.js +3 -0
  64. package/dist/collection/helpers/svg-export.helper.js +223 -26
  65. package/dist/collection/index.js +13 -0
  66. package/dist/collection/interfaces/localization.interface.js +1 -0
  67. package/dist/collection/locales/de-locale.js +119 -0
  68. package/dist/collection/locales/en-locale.js +120 -0
  69. package/dist/collection/locales/fr-locale.js +119 -0
  70. package/dist/collection/themes/dark-theme.js +18 -0
  71. package/dist/collection/themes/light-theme.js +18 -0
  72. package/dist/components/index.d.ts +4 -0
  73. package/dist/components/index.js +1 -1
  74. package/dist/components/kritzel-active-users.js +1 -1
  75. package/dist/components/kritzel-avatar.js +1 -1
  76. package/dist/components/kritzel-awareness-cursors.js +1 -1
  77. package/dist/components/kritzel-back-to-content.js +1 -1
  78. package/dist/components/kritzel-brush-style.js +1 -1
  79. package/dist/components/kritzel-button.js +1 -1
  80. package/dist/components/kritzel-color-palette.js +1 -1
  81. package/dist/components/kritzel-color.js +1 -1
  82. package/dist/components/kritzel-context-menu.js +1 -1
  83. package/dist/components/kritzel-controls.js +1 -1
  84. package/dist/components/kritzel-current-user-dialog.js +1 -1
  85. package/dist/components/kritzel-current-user.js +1 -1
  86. package/dist/components/kritzel-editor.js +1 -1
  87. package/dist/components/kritzel-engine.js +1 -1
  88. package/dist/components/kritzel-export.js +1 -1
  89. package/dist/components/kritzel-font-size.js +1 -1
  90. package/dist/components/kritzel-font.js +1 -1
  91. package/dist/components/kritzel-icon.js +1 -1
  92. package/dist/components/kritzel-input.js +1 -1
  93. package/dist/components/kritzel-login-dialog.js +1 -1
  94. package/dist/components/kritzel-master-detail.js +1 -1
  95. package/dist/components/kritzel-menu-item.js +1 -1
  96. package/dist/components/kritzel-menu.js +1 -1
  97. package/dist/components/kritzel-more-menu.js +1 -1
  98. package/dist/components/kritzel-numeric-input.js +1 -1
  99. package/dist/components/kritzel-opacity-slider.js +1 -1
  100. package/dist/components/kritzel-pill-tabs.js +1 -1
  101. package/dist/components/kritzel-portal.js +1 -1
  102. package/dist/components/kritzel-settings.js +1 -1
  103. package/dist/components/kritzel-share-dialog.js +1 -1
  104. package/dist/components/kritzel-slide-toggle.js +1 -1
  105. package/dist/components/kritzel-split-button.js +1 -1
  106. package/dist/components/kritzel-stroke-size.js +1 -1
  107. package/dist/components/kritzel-tool-config.js +1 -1
  108. package/dist/components/kritzel-tooltip.js +1 -1
  109. package/dist/components/kritzel-utility-panel.js +1 -1
  110. package/dist/components/kritzel-watermark.d.ts +11 -0
  111. package/dist/components/kritzel-watermark.js +1 -0
  112. package/dist/components/kritzel-workspace-manager.js +1 -1
  113. package/dist/components/kritzel-zoom-panel.d.ts +11 -0
  114. package/dist/components/kritzel-zoom-panel.js +1 -0
  115. package/dist/components/{p-B5xxfwKF.js → p-3HxnBrCM.js} +1 -1
  116. package/dist/components/p-6RjeGuvH.js +1 -0
  117. package/dist/components/p-7NsK0uHu.js +1 -0
  118. package/dist/components/{p-dcAernE1.js → p-BCNyR5Sw.js} +1 -1
  119. package/dist/components/{p-C2SX-XRr.js → p-BG6hOSrm.js} +1 -1
  120. package/dist/components/p-BKJSh8qQ.js +1 -0
  121. package/dist/components/{p-SptaSMno.js → p-BKvHg9cv.js} +1 -1
  122. package/dist/components/p-Bc55X65h.js +1 -0
  123. package/dist/components/p-BpnIvNvq.js +1 -0
  124. package/dist/components/p-BvRrA4hN.js +1 -0
  125. package/dist/components/{p-B2w8X7vn.js → p-BxpKq94F.js} +1 -1
  126. package/dist/components/{p-BFoK4W--.js → p-Bzv9Px8v.js} +1 -1
  127. package/dist/components/{p-COLHjboZ.js → p-C9HGoDHE.js} +1 -1
  128. package/dist/components/p-CEnEDaix.js +1 -0
  129. package/dist/components/p-CIcLzcfA.js +1 -0
  130. package/dist/components/p-CPtDfadX.js +1 -0
  131. package/dist/components/p-C_fKgKHu.js +9 -0
  132. package/dist/components/p-CdR76C4L.js +1 -0
  133. package/dist/components/p-Cu9KYyoq.js +1 -0
  134. package/dist/components/p-CyqRcqsO.js +1 -0
  135. package/dist/components/{p-UoPj5QjH.js → p-DDkmsPpV.js} +1 -1
  136. package/dist/components/{p-D-sRVAbQ.js → p-DI4vQRE3.js} +1 -1
  137. package/dist/components/{p-CJOhfMU5.js → p-DNdXJp8F.js} +1 -1
  138. package/dist/components/p-DX5K8xnh.js +1 -0
  139. package/dist/components/{p-DEy7zJCe.js → p-DZdgXCAx.js} +1 -1
  140. package/dist/components/p-DdH1cKED.js +1 -0
  141. package/dist/components/p-DgmtCdnL.js +1 -0
  142. package/dist/components/{p-BzYU3-MJ.js → p-DmWSRsjK.js} +1 -1
  143. package/dist/components/{p-Bj2laX89.js → p-Dz-Ti24X.js} +1 -1
  144. package/dist/components/{p-BiG1dxPS.js → p-F5_X4dZG.js} +1 -1
  145. package/dist/components/{p-x6doYeiI.js → p-IpoC5EEY.js} +1 -1
  146. package/dist/components/p-Jn6TNdfe.js +1 -0
  147. package/dist/components/{p-BfNHpqQ8.js → p-NuLP1xHe.js} +1 -1
  148. package/dist/components/{p-skWUIStn.js → p-SDZNC8GF.js} +1 -1
  149. package/dist/components/{p-BYmp9Ovv.js → p-U4oawa1x.js} +1 -1
  150. package/dist/components/{p-DM11KXUT.js → p-f8aW1ye7.js} +1 -1
  151. package/dist/components/p-mz3pUWW4.js +1 -0
  152. package/dist/components/p-v7dxxrL5.js +1 -0
  153. package/dist/components/p-vAeiXe6c.js +1 -0
  154. package/dist/esm/index-Dhio9uis.js +2 -2
  155. package/dist/esm/index.js +2 -2
  156. package/dist/esm/{kritzel-active-users_42.entry.js → kritzel-active-users_44.entry.js} +709 -146
  157. package/dist/esm/loader.js +1 -1
  158. package/dist/esm/{schema.constants-DiCnmIYK.js → schema.constants-DchTXG3V.js} +1163 -172
  159. package/dist/esm/stencil.js +1 -1
  160. package/dist/stencil/index.esm.js +1 -1
  161. package/dist/stencil/p-86348986.entry.js +9 -0
  162. package/dist/stencil/p-DchTXG3V.js +1 -0
  163. package/dist/stencil/stencil.esm.js +1 -1
  164. package/dist/types/classes/core/core.class.d.ts +16 -0
  165. package/dist/types/classes/handlers/context-menu.handler.d.ts +13 -0
  166. package/dist/types/classes/managers/license.manager.d.ts +141 -0
  167. package/dist/types/classes/managers/localization.manager.d.ts +121 -0
  168. package/dist/types/classes/objects/custom-element.class.d.ts +2 -0
  169. package/dist/types/classes/objects/group.class.d.ts +6 -1
  170. package/dist/types/classes/objects/image.class.d.ts +1 -1
  171. package/dist/types/classes/objects/path.class.d.ts +3 -2
  172. package/dist/types/classes/objects/selection-group.class.d.ts +6 -1
  173. package/dist/types/classes/objects/shape.class.d.ts +2 -0
  174. package/dist/types/classes/objects/text.class.d.ts +2 -1
  175. package/dist/types/classes/tools/brush-tool.class.d.ts +1 -1
  176. package/dist/types/components/core/kritzel-editor/kritzel-editor.d.ts +53 -1
  177. package/dist/types/components/core/kritzel-engine/kritzel-engine.d.ts +55 -3
  178. package/dist/types/components/core/kritzel-watermark/kritzel-watermark.d.ts +20 -0
  179. package/dist/types/components/ui/kritzel-controls/kritzel-controls.d.ts +3 -0
  180. package/dist/types/components/ui/kritzel-current-user/kritzel-current-user.d.ts +3 -0
  181. package/dist/types/components/ui/kritzel-current-user-dialog/kritzel-current-user-dialog.d.ts +3 -0
  182. package/dist/types/components/ui/kritzel-export/kritzel-export.d.ts +4 -1
  183. package/dist/types/components/ui/kritzel-more-menu/kritzel-more-menu.d.ts +3 -0
  184. package/dist/types/components/ui/kritzel-settings/kritzel-settings.d.ts +16 -0
  185. package/dist/types/components/ui/kritzel-share-dialog/kritzel-share-dialog.d.ts +3 -0
  186. package/dist/types/components/ui/kritzel-tool-config/kritzel-tool-config.d.ts +3 -0
  187. package/dist/types/components/ui/kritzel-utility-panel/kritzel-utility-panel.d.ts +3 -0
  188. package/dist/types/components/ui/kritzel-workspace-manager/kritzel-workspace-manager.d.ts +3 -0
  189. package/dist/types/components/ui/kritzel-zoom-panel/kritzel-zoom-panel.d.ts +20 -0
  190. package/dist/types/components.d.ts +445 -26
  191. package/dist/types/constants/engine.constants.d.ts +2 -0
  192. package/dist/types/constants/license.constants.d.ts +25 -0
  193. package/dist/types/constants/version.d.ts +1 -1
  194. package/dist/types/helpers/localization.helper.d.ts +18 -0
  195. package/dist/types/helpers/math.helper.d.ts +1 -0
  196. package/dist/types/helpers/svg-export.helper.d.ts +81 -7
  197. package/dist/types/index.d.ts +13 -0
  198. package/dist/types/interfaces/context-menu-item.interface.d.ts +7 -1
  199. package/dist/types/interfaces/line-options.interface.d.ts +2 -0
  200. package/dist/types/interfaces/localization.interface.d.ts +143 -0
  201. package/dist/types/interfaces/path-options.interface.d.ts +2 -0
  202. package/dist/types/interfaces/settings.interface.d.ts +3 -0
  203. package/dist/types/interfaces/theme.interface.d.ts +27 -2
  204. package/dist/types/locales/de-locale.d.ts +5 -0
  205. package/dist/types/locales/en-locale.d.ts +6 -0
  206. package/dist/types/locales/fr-locale.d.ts +5 -0
  207. package/package.json +4 -7
  208. package/dist/components/p-2xYAGd0I.js +0 -1
  209. package/dist/components/p-B2Os1ya_.js +0 -1
  210. package/dist/components/p-BTEV1WwT.js +0 -1
  211. package/dist/components/p-BbactVA0.js +0 -1
  212. package/dist/components/p-BqwqGFQY.js +0 -1
  213. package/dist/components/p-C0TN5IAi.js +0 -1
  214. package/dist/components/p-CFgkUYoO.js +0 -1
  215. package/dist/components/p-COgo9OWy.js +0 -1
  216. package/dist/components/p-CUPYGT8c.js +0 -1
  217. package/dist/components/p-CcyIAi9S.js +0 -1
  218. package/dist/components/p-Cj78L1Kk.js +0 -1
  219. package/dist/components/p-CkAVEdDw.js +0 -9
  220. package/dist/components/p-CmuNn1Tc.js +0 -1
  221. package/dist/components/p-DDYoDSrm.js +0 -1
  222. package/dist/components/p-DbB730vO.js +0 -1
  223. package/dist/components/p-DlwYHzSj.js +0 -1
  224. package/dist/components/p-FK7b3BGt.js +0 -1
  225. package/dist/components/p-J9_SwObO.js +0 -1
  226. package/dist/stencil/p-67775031.entry.js +0 -9
  227. package/dist/stencil/p-DiCnmIYK.js +0 -1
@@ -862,8 +862,11 @@ function findDiffStart(a, b, pos) {
862
862
  if (!childA.sameMarkup(childB))
863
863
  return pos;
864
864
  if (childA.isText && childA.text != childB.text) {
865
- for (let j = 0; childA.text[j] == childB.text[j]; j++)
865
+ let tA = childA.text, tB = childB.text, j = 0;
866
+ for (; tA[j] == tB[j]; j++)
866
867
  pos++;
868
+ if (j && j < tA.length && j < tB.length && surrogateHigh(tA.charCodeAt(j - 1)) && surrogateLow(tA.charCodeAt(j)))
869
+ pos--;
867
870
  return pos;
868
871
  }
869
872
  if (childA.content.size || childB.content.size) {
@@ -887,12 +890,17 @@ function findDiffEnd(a, b, posA, posB) {
887
890
  if (!childA.sameMarkup(childB))
888
891
  return { a: posA, b: posB };
889
892
  if (childA.isText && childA.text != childB.text) {
890
- let same = 0, minSize = Math.min(childA.text.length, childB.text.length);
891
- while (same < minSize && childA.text[childA.text.length - same - 1] == childB.text[childB.text.length - same - 1]) {
892
- same++;
893
+ let tA = childA.text, tB = childB.text, iA = tA.length, iB = tB.length;
894
+ while (iA > 0 && iB > 0 && tA[iA - 1] == tB[iB - 1]) {
895
+ iA--;
896
+ iB--;
893
897
  posA--;
894
898
  posB--;
895
899
  }
900
+ if (iA && iB && iA < tA.length && surrogateHigh(tA.charCodeAt(iA - 1)) && surrogateLow(tA.charCodeAt(iA))) {
901
+ posA++;
902
+ posB++;
903
+ }
896
904
  return { a: posA, b: posB };
897
905
  }
898
906
  if (childA.content.size || childB.content.size) {
@@ -904,6 +912,8 @@ function findDiffEnd(a, b, posA, posB) {
904
912
  posB -= size;
905
913
  }
906
914
  }
915
+ function surrogateLow(ch) { return ch >= 0xDC00 && ch < 0xE000; }
916
+ function surrogateHigh(ch) { return ch >= 0xD800 && ch < 0xDC00; }
907
917
 
908
918
  /**
909
919
  A fragment represents a node's collection of child nodes.
@@ -1584,7 +1594,8 @@ function addRange($start, $end, depth, target) {
1584
1594
  addNode($end.nodeBefore, target);
1585
1595
  }
1586
1596
  function close(node, content) {
1587
- node.type.checkContent(content);
1597
+ if (!node.type.validContent(content))
1598
+ throw new ReplaceError("Invalid content for node " + node.type.name);
1588
1599
  return node.copy(content);
1589
1600
  }
1590
1601
  function replaceThreeWay($from, $start, $end, $to, depth) {
@@ -2895,13 +2906,12 @@ function computeAttrs(attrs, value) {
2895
2906
  return built;
2896
2907
  }
2897
2908
  function checkAttrs(attrs, values, type, name) {
2898
- for (let name in values)
2899
- if (!(name in attrs))
2900
- throw new RangeError(`Unsupported attribute ${name} for ${type} of type ${name}`);
2901
- for (let name in attrs) {
2902
- let attr = attrs[name];
2903
- if (attr.validate)
2904
- attr.validate(values[name]);
2909
+ for (let attr in values)
2910
+ if (!(attr in attrs))
2911
+ throw new RangeError(`Unsupported attribute ${attr} for ${type} of type ${name}`);
2912
+ for (let attr in attrs) {
2913
+ if (attrs[attr].validate)
2914
+ attrs[attr].validate(values[attr]);
2905
2915
  }
2906
2916
  }
2907
2917
  function initAttrs(typeName, attrs) {
@@ -3078,7 +3088,7 @@ let NodeType$1 = class NodeType {
3078
3088
  @internal
3079
3089
  */
3080
3090
  checkAttrs(attrs) {
3081
- checkAttrs(this.attrs, attrs, "node");
3091
+ checkAttrs(this.attrs, attrs, "node", this.name);
3082
3092
  }
3083
3093
  /**
3084
3094
  Check whether the given mark type is allowed in this node.
@@ -3229,7 +3239,7 @@ class MarkType {
3229
3239
  @internal
3230
3240
  */
3231
3241
  checkAttrs(attrs) {
3232
- checkAttrs(this.attrs, attrs, "mark");
3242
+ checkAttrs(this.attrs, attrs, "mark", this.name);
3233
3243
  }
3234
3244
  /**
3235
3245
  Queries whether a given mark type is
@@ -9664,14 +9674,14 @@ function selectionToDOM(view, force = false) {
9664
9674
  syncNodeSelection(view, sel);
9665
9675
  if (!editorOwnsSelection(view))
9666
9676
  return;
9667
- // The delayed drag selection causes issues with Cell Selections
9668
- // in Safari. And the drag selection delay is to workarond issues
9669
- // which only present in Chrome.
9670
- if (!force && view.input.mouseDown && view.input.mouseDown.allowDefault && chrome) {
9677
+ // Need to delay selection normalization during a native selection
9678
+ // drag on Chrome, or it will cause further dragging to glitch.
9679
+ let mouseDown = view.input.mouseDown;
9680
+ if (!force && chrome && mouseDown) {
9671
9681
  let domSel = view.domSelectionRange(), curSel = view.domObserver.currentSelection;
9672
9682
  if (domSel.anchorNode && curSel.anchorNode &&
9673
- isEquivalentPosition(domSel.anchorNode, domSel.anchorOffset, curSel.anchorNode, curSel.anchorOffset)) {
9674
- view.input.mouseDown.delayedSelectionSync = true;
9683
+ isEquivalentPosition(domSel.anchorNode, domSel.anchorOffset, curSel.anchorNode, curSel.anchorOffset) &&
9684
+ mouseDown.delaySelUpdate()) {
9675
9685
  view.domObserver.setCurSelection();
9676
9686
  return;
9677
9687
  }
@@ -10523,6 +10533,8 @@ function setSelectionOrigin(view, origin) {
10523
10533
  view.input.lastSelectionTime = Date.now();
10524
10534
  }
10525
10535
  function destroyInput(view) {
10536
+ if (view.input.mouseDown)
10537
+ view.input.mouseDown.done();
10526
10538
  view.domObserver.stop();
10527
10539
  for (let type in view.input.eventHandlers)
10528
10540
  view.dom.removeEventListener(type, view.input.eventHandlers[type]);
@@ -10561,7 +10573,7 @@ function dispatchEvent(view, event) {
10561
10573
  editHandlers.keydown = (view, _event) => {
10562
10574
  let event = _event;
10563
10575
  view.input.shiftKey = event.keyCode == 16 || event.shiftKey;
10564
- if (inOrNearComposition(view, event))
10576
+ if (inOrNearComposition(view))
10565
10577
  return;
10566
10578
  view.input.lastKeyCode = event.keyCode;
10567
10579
  view.input.lastKeyCodeTime = Date.now();
@@ -10599,7 +10611,7 @@ editHandlers.keyup = (view, event) => {
10599
10611
  };
10600
10612
  editHandlers.keypress = (view, _event) => {
10601
10613
  let event = _event;
10602
- if (inOrNearComposition(view, event) || !event.charCode ||
10614
+ if (inOrNearComposition(view) || !event.charCode ||
10603
10615
  event.ctrlKey && !event.altKey || mac$3 && event.metaKey)
10604
10616
  return;
10605
10617
  if (view.someProp("handleKeyPress", f => f(view, event))) {
@@ -10693,26 +10705,28 @@ function handleTripleClick(view, pos, inside, event) {
10693
10705
  function defaultTripleClick(view, inside, event) {
10694
10706
  if (event.button != 0)
10695
10707
  return false;
10696
- let doc = view.state.doc;
10697
- if (inside == -1) {
10698
- if (doc.inlineContent) {
10699
- updateSelection(view, TextSelection.create(doc, 0, doc.content.size));
10700
- return true;
10701
- }
10708
+ let selection = selectionForTripleClick(view, inside, true), doc = view.state.doc;
10709
+ if (!selection)
10702
10710
  return false;
10703
- }
10711
+ updateSelection(view, selection);
10712
+ if (selection instanceof TextSelection && doc.eq(view.state.doc))
10713
+ view.input.mouseDown = new TripleClickDrag(view, selection);
10714
+ return true;
10715
+ }
10716
+ function selectionForTripleClick(view, inside, selectNodes) {
10717
+ let doc = view.state.doc;
10718
+ if (inside == -1)
10719
+ return doc.inlineContent ? TextSelection.create(doc, 0, doc.content.size) : null;
10704
10720
  let $pos = doc.resolve(inside);
10705
10721
  for (let i = $pos.depth + 1; i > 0; i--) {
10706
10722
  let node = i > $pos.depth ? $pos.nodeAfter : $pos.node(i);
10707
10723
  let nodePos = $pos.before(i);
10708
10724
  if (node.inlineContent)
10709
- updateSelection(view, TextSelection.create(doc, nodePos + 1, nodePos + 1 + node.content.size));
10710
- else if (NodeSelection.isSelectable(node))
10711
- updateSelection(view, NodeSelection.create(doc, nodePos));
10712
- else
10713
- continue;
10714
- return true;
10725
+ return TextSelection.create(doc, nodePos + 1, nodePos + 1 + node.content.size);
10726
+ else if (selectNodes && NodeSelection.isSelectable(node))
10727
+ return NodeSelection.create(doc, nodePos);
10715
10728
  }
10729
+ return null;
10716
10730
  }
10717
10731
  function forceDOMFlush(view) {
10718
10732
  return endComposition(view);
@@ -10731,13 +10745,13 @@ handlers.mousedown = (view, _event) => {
10731
10745
  type = "tripleClick";
10732
10746
  }
10733
10747
  view.input.lastClick = { time: now, x: event.clientX, y: event.clientY, type, button: event.button };
10748
+ if (view.input.mouseDown)
10749
+ view.input.mouseDown.done();
10734
10750
  let pos = view.posAtCoords(eventCoords(event));
10735
10751
  if (!pos)
10736
10752
  return;
10737
10753
  if (type == "singleClick") {
10738
- if (view.input.mouseDown)
10739
- view.input.mouseDown.done();
10740
- view.input.mouseDown = new MouseDown(view, pos, event, !!flushed);
10754
+ view.input.mouseDown = new LeftMouseDown(view, pos, event, !!flushed);
10741
10755
  }
10742
10756
  else if ((type == "doubleClick" ? handleDoubleClick : handleTripleClick)(view, pos.pos, pos.inside, event)) {
10743
10757
  event.preventDefault();
@@ -10747,13 +10761,34 @@ handlers.mousedown = (view, _event) => {
10747
10761
  }
10748
10762
  };
10749
10763
  class MouseDown {
10750
- constructor(view, pos, event, flushed) {
10764
+ constructor(view) {
10751
10765
  this.view = view;
10766
+ this.mightDrag = null;
10767
+ view.root.addEventListener("mouseup", this.up = this.up.bind(this));
10768
+ view.root.addEventListener("mousemove", this.move = this.move.bind(this));
10769
+ }
10770
+ up(event) {
10771
+ this.done();
10772
+ }
10773
+ move(event) {
10774
+ if (event.buttons == 0)
10775
+ this.done();
10776
+ }
10777
+ done() {
10778
+ this.view.root.removeEventListener("mouseup", this.up);
10779
+ this.view.root.removeEventListener("mousemove", this.move);
10780
+ if (this.view.input.mouseDown == this)
10781
+ this.view.input.mouseDown = null;
10782
+ }
10783
+ delaySelUpdate() { return false; }
10784
+ }
10785
+ class LeftMouseDown extends MouseDown {
10786
+ constructor(view, pos, event, flushed) {
10787
+ super(view);
10752
10788
  this.pos = pos;
10753
10789
  this.event = event;
10754
10790
  this.flushed = flushed;
10755
10791
  this.delayedSelectionSync = false;
10756
- this.mightDrag = null;
10757
10792
  this.startDoc = view.state.doc;
10758
10793
  this.selectNode = !!event[selectNodeModifier];
10759
10794
  this.allowDefault = event.shiftKey;
@@ -10791,13 +10826,10 @@ class MouseDown {
10791
10826
  }, 20);
10792
10827
  this.view.domObserver.start();
10793
10828
  }
10794
- view.root.addEventListener("mouseup", this.up = this.up.bind(this));
10795
- view.root.addEventListener("mousemove", this.move = this.move.bind(this));
10796
10829
  setSelectionOrigin(view, "pointer");
10797
10830
  }
10798
10831
  done() {
10799
- this.view.root.removeEventListener("mouseup", this.up);
10800
- this.view.root.removeEventListener("mousemove", this.move);
10832
+ super.done();
10801
10833
  if (this.mightDrag && this.target) {
10802
10834
  this.view.domObserver.stop();
10803
10835
  if (this.mightDrag.addAttr)
@@ -10807,8 +10839,10 @@ class MouseDown {
10807
10839
  this.view.domObserver.start();
10808
10840
  }
10809
10841
  if (this.delayedSelectionSync)
10810
- setTimeout(() => selectionToDOM(this.view));
10811
- this.view.input.mouseDown = null;
10842
+ setTimeout(() => {
10843
+ if (!this.view.isDestroyed)
10844
+ selectionToDOM(this.view);
10845
+ });
10812
10846
  }
10813
10847
  up(event) {
10814
10848
  this.done();
@@ -10847,14 +10881,41 @@ class MouseDown {
10847
10881
  move(event) {
10848
10882
  this.updateAllowDefault(event);
10849
10883
  setSelectionOrigin(this.view, "pointer");
10850
- if (event.buttons == 0)
10851
- this.done();
10884
+ super.move(event);
10852
10885
  }
10853
10886
  updateAllowDefault(event) {
10854
10887
  if (!this.allowDefault && (Math.abs(this.event.x - event.clientX) > 4 ||
10855
10888
  Math.abs(this.event.y - event.clientY) > 4))
10856
10889
  this.allowDefault = true;
10857
10890
  }
10891
+ delaySelUpdate() {
10892
+ if (!this.allowDefault)
10893
+ return false;
10894
+ this.delayedSelectionSync = true;
10895
+ return true;
10896
+ }
10897
+ }
10898
+ class TripleClickDrag extends MouseDown {
10899
+ constructor(view, startSelection) {
10900
+ super(view);
10901
+ this.startSelection = startSelection;
10902
+ this.startDoc = view.state.doc;
10903
+ }
10904
+ move(event) {
10905
+ if (event.buttons == 0 || this.view.isDestroyed || !this.view.state.doc.eq(this.startDoc)) {
10906
+ this.done();
10907
+ return;
10908
+ }
10909
+ event.preventDefault();
10910
+ setSelectionOrigin(this.view, "pointer");
10911
+ let pos = this.view.posAtCoords(eventCoords(event));
10912
+ let target = pos && selectionForTripleClick(this.view, pos.inside, false);
10913
+ if (!target)
10914
+ return;
10915
+ let { doc } = this.view.state, start = this.startSelection;
10916
+ let [anchor, head] = target.from < start.from ? [start.to, target.from] : [start.from, target.to];
10917
+ updateSelection(this.view, TextSelection.create(doc, anchor, head));
10918
+ }
10858
10919
  }
10859
10920
  handlers.touchstart = view => {
10860
10921
  view.input.lastTouch = Date.now();
@@ -10879,7 +10940,7 @@ function inOrNearComposition(view, event) {
10879
10940
  // This guards against the case where compositionend is triggered without the keyboard
10880
10941
  // (e.g. character confirmation may be done with the mouse), and keydown is triggered
10881
10942
  // afterwards- we wouldn't want to ignore the keydown event in this case.
10882
- if (safari && Math.abs(event.timeStamp - view.input.compositionEndedAt) < 500) {
10943
+ if (safari && Math.abs(Date.now() - view.input.compositionEndedAt) < 500) {
10883
10944
  view.input.compositionEndedAt = -2e8;
10884
10945
  return true;
10885
10946
  }
@@ -10938,7 +10999,7 @@ function selectionBeforeUneditable(view) {
10938
10999
  editHandlers.compositionend = (view, event) => {
10939
11000
  if (view.composing) {
10940
11001
  view.input.composing = false;
10941
- view.input.compositionEndedAt = event.timeStamp;
11002
+ view.input.compositionEndedAt = Date.now();
10942
11003
  view.input.compositionPendingChanges = view.domObserver.pendingRecords().length ? view.input.compositionID : 0;
10943
11004
  view.input.compositionNode = null;
10944
11005
  if (view.input.badSafariComposition)
@@ -10957,7 +11018,7 @@ function scheduleComposeEnd(view, delay) {
10957
11018
  function clearComposition(view) {
10958
11019
  if (view.composing) {
10959
11020
  view.input.composing = false;
10960
- view.input.compositionEndedAt = timestampFromCustomEvent();
11021
+ view.input.compositionEndedAt = Date.now();
10961
11022
  }
10962
11023
  while (view.input.compositionNodes.length > 0)
10963
11024
  view.input.compositionNodes.pop().markParentsDirty();
@@ -10983,11 +11044,6 @@ function findCompositionNode(view) {
10983
11044
  }
10984
11045
  return textBefore || textAfter;
10985
11046
  }
10986
- function timestampFromCustomEvent() {
10987
- let event = document.createEvent("Event");
10988
- event.initEvent("event", true, true);
10989
- return event.timeStamp;
10990
- }
10991
11047
  /**
10992
11048
  @internal
10993
11049
  */
@@ -12144,7 +12200,10 @@ class DOMObserver {
12144
12200
  }
12145
12201
  }
12146
12202
  }
12147
- if (added.some(n => n.nodeName == "BR") && (view.input.lastKeyCode == 8 || view.input.lastKeyCode == 46)) {
12203
+ if (added.some(n => n.nodeName == "BR") &&
12204
+ (view.input.lastKeyCode == 8 || view.input.lastKeyCode == 46 ||
12205
+ chrome && (view.composing || view.input.compositionEndedAt > Date.now() - 50) &&
12206
+ mutations.some(m => m.type == "childList" && m.removedNodes.length))) {
12148
12207
  // Browsers sometimes insert a bogus break node if you
12149
12208
  // backspace out the last bit of text before an inline-flex node (#1552)
12150
12209
  for (let node of added)
@@ -12700,38 +12759,28 @@ function skipClosingAndOpening($pos, fromEnd, mayOpen) {
12700
12759
  return end;
12701
12760
  }
12702
12761
  function findDiff(a, b, pos, preferredPos, preferredSide) {
12703
- let start = a.findDiffStart(b, pos);
12762
+ let start = a.findDiffStart(b, pos), lenA = pos + a.size, lenB = pos + b.size;
12704
12763
  if (start == null)
12705
12764
  return null;
12706
- let { a: endA, b: endB } = a.findDiffEnd(b, pos + a.size, pos + b.size);
12765
+ let { a: endA, b: endB } = a.findDiffEnd(b, lenA, lenB);
12707
12766
  if (preferredSide == "end") {
12708
12767
  let adjust = Math.max(0, start - Math.min(endA, endB));
12709
12768
  preferredPos -= endA + adjust - start;
12710
12769
  }
12711
- if (endA < start && a.size < b.size) {
12770
+ if (endA < start && lenA < lenB) {
12712
12771
  let move = preferredPos <= start && preferredPos >= endA ? start - preferredPos : 0;
12713
12772
  start -= move;
12714
- if (start && start < b.size && isSurrogatePair(b.textBetween(start - 1, start + 1)))
12715
- start += move ? 1 : -1;
12716
12773
  endB = start + (endB - endA);
12717
12774
  endA = start;
12718
12775
  }
12719
12776
  else if (endB < start) {
12720
12777
  let move = preferredPos <= start && preferredPos >= endB ? start - preferredPos : 0;
12721
12778
  start -= move;
12722
- if (start && start < a.size && isSurrogatePair(a.textBetween(start - 1, start + 1)))
12723
- start += move ? 1 : -1;
12724
12779
  endA = start + (endA - endB);
12725
12780
  endB = start;
12726
12781
  }
12727
12782
  return { start, endA, endB };
12728
12783
  }
12729
- function isSurrogatePair(str) {
12730
- if (str.length != 2)
12731
- return false;
12732
- let a = str.charCodeAt(0), b = str.charCodeAt(1);
12733
- return a >= 0xDC00 && a <= 0xDFFF && b >= 0xD800 && b <= 0xDBFF;
12734
- }
12735
12784
  /**
12736
12785
  An editor view manages the DOM structure that represents an
12737
12786
  editable document. Its state and behavior are determined by its
@@ -12923,9 +12972,10 @@ class EditorView {
12923
12972
  // a DOM selection change and the "selectionchange" event for it
12924
12973
  // can cause a spurious DOM selection update, disrupting mouse
12925
12974
  // drag selection.
12975
+ let mouseDown = this.input.mouseDown;
12926
12976
  if (forceSelUpdate ||
12927
- !(this.input.mouseDown && this.domObserver.currentSelection.eq(this.domSelectionRange()) &&
12928
- anchorInRightPlace(this))) {
12977
+ !(mouseDown && this.domObserver.currentSelection.eq(this.domSelectionRange()) &&
12978
+ anchorInRightPlace(this) && mouseDown.delaySelUpdate())) {
12929
12979
  selectionToDOM(this, forceSelUpdate);
12930
12980
  }
12931
12981
  else {
@@ -14622,6 +14672,24 @@ const lightTheme = {
14622
14672
  buttonHoverBackgroundColor: 'hsl(0, 0%, 0%, 4.3%)',
14623
14673
  separatorColor: 'hsl(0, 0%, 0%, 8%)',
14624
14674
  },
14675
+ watermark: {
14676
+ background: 'rgba(255, 255, 255, 0.6)',
14677
+ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.12)',
14678
+ color: 'rgba(60, 60, 60, 0.75)',
14679
+ },
14680
+ zoomPanel: {
14681
+ backgroundColor: '#ffffff',
14682
+ border: '1px solid #ebebeb',
14683
+ borderRadius: '12px',
14684
+ boxShadow: '0 0 3px rgba(0, 0, 0, 0.08)',
14685
+ buttonActiveBackgroundColor: 'hsl(0, 0%, 0%, 8.6%)',
14686
+ buttonBorderRadius: '8px',
14687
+ buttonHoverBackgroundColor: 'hsl(0, 0%, 0%, 4.3%)',
14688
+ buttonSize: '32px',
14689
+ gap: '4px',
14690
+ iconColor: '#000000',
14691
+ padding: '4px',
14692
+ },
14625
14693
  };
14626
14694
 
14627
14695
  /**
@@ -14899,6 +14967,24 @@ const darkTheme = {
14899
14967
  buttonHoverBackgroundColor: 'hsl(0, 0%, 100%, 8%)',
14900
14968
  separatorColor: 'hsl(0, 0%, 100%, 12%)',
14901
14969
  },
14970
+ watermark: {
14971
+ background: 'rgba(40, 40, 40, 0.6)',
14972
+ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.35)',
14973
+ color: 'rgba(220, 220, 220, 0.8)',
14974
+ },
14975
+ zoomPanel: {
14976
+ backgroundColor: '#2a2a2a',
14977
+ border: '1px solid #3a3a3a',
14978
+ borderRadius: '12px',
14979
+ boxShadow: '0 0 3px rgba(0, 0, 0, 0.4)',
14980
+ buttonActiveBackgroundColor: 'hsl(0, 0%, 100%, 12%)',
14981
+ buttonBorderRadius: '8px',
14982
+ buttonHoverBackgroundColor: 'hsl(0, 0%, 100%, 8%)',
14983
+ buttonSize: '32px',
14984
+ gap: '4px',
14985
+ iconColor: '#e0e0e0',
14986
+ padding: '4px',
14987
+ },
14902
14988
  };
14903
14989
 
14904
14990
  class ThemeHelper {
@@ -14972,7 +15058,7 @@ class ThemeHelper {
14972
15058
  }
14973
15059
 
14974
15060
  /** Key used to store the settings object in localStorage. */
14975
- const SETTINGS_STORAGE_KEY = 'kritzel-settings';
15061
+ const SETTINGS_STORAGE_KEY$1 = 'kritzel-settings';
14976
15062
  /** Default theme used when no stored preference exists. */
14977
15063
  const DEFAULT_THEME = 'light';
14978
15064
  /**
@@ -14999,7 +15085,7 @@ class KritzelThemeManager {
14999
15085
  */
15000
15086
  constructor(core) {
15001
15087
  this._core = core;
15002
- this._storageKey = core.editorId ? `${SETTINGS_STORAGE_KEY}-${core.editorId}` : SETTINGS_STORAGE_KEY;
15088
+ this._storageKey = core.editorId ? `${SETTINGS_STORAGE_KEY$1}-${core.editorId}` : SETTINGS_STORAGE_KEY$1;
15003
15089
  this._themeRegistry.set('light', lightTheme);
15004
15090
  this._themeRegistry.set('dark', darkTheme);
15005
15091
  this._currentTheme = this.getStoredTheme();
@@ -15115,7 +15201,7 @@ class KritzelThemeManager {
15115
15201
  if (typeof localStorage === 'undefined') {
15116
15202
  return DEFAULT_THEME;
15117
15203
  }
15118
- const stored = localStorage.getItem(SETTINGS_STORAGE_KEY);
15204
+ const stored = localStorage.getItem(SETTINGS_STORAGE_KEY$1);
15119
15205
  if (!stored) {
15120
15206
  return DEFAULT_THEME;
15121
15207
  }
@@ -15279,6 +15365,15 @@ class KritzelColorHelper {
15279
15365
  }
15280
15366
  }
15281
15367
 
15368
+ class KritzelMathHelper {
15369
+ static average(a, b) {
15370
+ return (a + b) / 2;
15371
+ }
15372
+ static degreesToRadians(degrees) {
15373
+ return degrees * (Math.PI / 180);
15374
+ }
15375
+ }
15376
+
15282
15377
  /**
15283
15378
  * Represents a text object on the canvas that supports rich text editing via ProseMirror.
15284
15379
  * Extends the base object class to inherit common object properties and behaviors.
@@ -15343,6 +15438,7 @@ class KritzelText extends KritzelBaseObject {
15343
15438
  */
15344
15439
  constructor(config) {
15345
15440
  super();
15441
+ this.rotation = KritzelMathHelper.degreesToRadians(config?.rotation ?? 0);
15346
15442
  // Always create the editor so setContent() works immediately
15347
15443
  this.editor = this.createEditor();
15348
15444
  if (config) {
@@ -15381,7 +15477,7 @@ class KritzelText extends KritzelBaseObject {
15381
15477
  * @param scale - Optional scale factor (defaults to current viewport scale).
15382
15478
  * @returns A new, fully initialized KritzelText instance.
15383
15479
  */
15384
- static create(core, fontSize, fontFamily, scale) {
15480
+ static create(core, fontSize, fontFamily, scale, rotation) {
15385
15481
  const object = new KritzelText();
15386
15482
  object._core = core;
15387
15483
  object.id = object.generateId();
@@ -15391,6 +15487,7 @@ class KritzelText extends KritzelBaseObject {
15391
15487
  object.fontFamily = fontFamily || 'Arial';
15392
15488
  object.translateX = 0;
15393
15489
  object.translateY = 0;
15490
+ object.rotation = KritzelMathHelper.degreesToRadians(rotation ?? 0);
15394
15491
  const coreScale = core.store.state.scale;
15395
15492
  const effectiveScale = coreScale < 0 ? coreScale : 1;
15396
15493
  object.width = object.initialWidth / effectiveScale;
@@ -15742,12 +15839,6 @@ function requireCjs () {
15742
15839
 
15743
15840
  var cjsExports = requireCjs();
15744
15841
 
15745
- class KritzelMathHelper {
15746
- static average(a, b) {
15747
- return (a + b) / 2;
15748
- }
15749
- }
15750
-
15751
15842
  class KritzelPath extends KritzelBaseObject {
15752
15843
  __class__ = 'KritzelPath';
15753
15844
  points;
@@ -15790,6 +15881,7 @@ class KritzelPath extends KritzelBaseObject {
15790
15881
  this.points = config?.points ?? [];
15791
15882
  this.translateX = config?.translateX ?? 0;
15792
15883
  this.translateY = config?.translateY ?? 0;
15884
+ this.rotation = KritzelMathHelper.degreesToRadians(config?.rotation ?? 0);
15793
15885
  this.scale = config?.scale ?? 1;
15794
15886
  this.strokeWidth = config?.strokeWidth ?? 8;
15795
15887
  this.fill = config?.fill ?? { light: '#000000', dark: '#ffffff' };
@@ -15813,6 +15905,7 @@ class KritzelPath extends KritzelBaseObject {
15813
15905
  object.points = options?.points ?? [];
15814
15906
  object.translateX = options?.translateX ?? 0;
15815
15907
  object.translateY = options?.translateY ?? 0;
15908
+ object.rotation = KritzelMathHelper.degreesToRadians(options?.rotation ?? 0);
15816
15909
  object.scale = options?.scale ?? 1;
15817
15910
  object.strokeWidth = options?.strokeWidth ?? 8;
15818
15911
  object.fill = options?.fill ?? { light: '#000000', dark: '#ffffff' };
@@ -15862,8 +15955,12 @@ class KritzelPath extends KritzelBaseObject {
15862
15955
  this.height = Math.max(...this.points.map(p => p[1])) - Math.min(...this.points.map(p => p[1])) + this.strokeWidth;
15863
15956
  this.x = Math.min(...this.points.map(p => p[0])) - this.strokeWidth / 2;
15864
15957
  this.y = Math.min(...this.points.map(p => p[1])) - this.strokeWidth / 2;
15865
- this.translateX = x;
15866
- this.translateY = y;
15958
+ if (x !== null) {
15959
+ this.translateX = x;
15960
+ }
15961
+ if (y !== null) {
15962
+ this.translateY = y;
15963
+ }
15867
15964
  this._adjustedPoints = null;
15868
15965
  this._core.store.objects.update(this);
15869
15966
  }
@@ -16069,19 +16166,14 @@ class KritzelPath extends KritzelBaseObject {
16069
16166
  }
16070
16167
  /**
16071
16168
  * Updates width, height, x, y, translateX, and translateY based on current points.
16072
- * Accounts for rotation and stroke width in the calculations.
16169
+ * Uses the unrotated local points and stroke width for base dimensions.
16073
16170
  * Called during initial setup to establish the path's dimensions and position.
16074
16171
  */
16075
16172
  updateDimensions() {
16076
- const rotatedPoints = this.points.map(([x, y]) => {
16077
- const rotatedX = x * Math.cos(this.rotation) - y * Math.sin(this.rotation);
16078
- const rotatedY = x * Math.sin(this.rotation) + y * Math.cos(this.rotation);
16079
- return [rotatedX, rotatedY];
16080
- });
16081
- const minX = Math.min(...rotatedPoints.map(p => p[0] - this.strokeWidth / 2));
16082
- const minY = Math.min(...rotatedPoints.map(p => p[1] - this.strokeWidth / 2));
16083
- const maxX = Math.max(...rotatedPoints.map(p => p[0] + this.strokeWidth / 2));
16084
- const maxY = Math.max(...rotatedPoints.map(p => p[1] + this.strokeWidth / 2));
16173
+ const minX = Math.min(...this.points.map(p => p[0])) - this.strokeWidth / 2;
16174
+ const minY = Math.min(...this.points.map(p => p[1])) - this.strokeWidth / 2;
16175
+ const maxX = Math.max(...this.points.map(p => p[0])) + this.strokeWidth / 2;
16176
+ const maxY = Math.max(...this.points.map(p => p[1])) + this.strokeWidth / 2;
16085
16177
  this.width = maxX - minX + this.lineSlack;
16086
16178
  this.height = maxY - minY + this.lineSlack;
16087
16179
  this.x = minX;
@@ -17148,6 +17240,7 @@ class KritzelImage extends KritzelBaseObject {
17148
17240
  this.y = config?.y || 0;
17149
17241
  this.translateX = config?.translateX || 0;
17150
17242
  this.translateY = config?.translateY || 0;
17243
+ this.rotation = KritzelMathHelper.degreesToRadians(config?.rotation ?? 0);
17151
17244
  this.scale = config?.scale || 1;
17152
17245
  this.width = config?.width || 0;
17153
17246
  this.height = config?.height || 0;
@@ -17161,17 +17254,18 @@ class KritzelImage extends KritzelBaseObject {
17161
17254
  * @param core - The KritzelCore instance providing access to store and state management.
17162
17255
  * @returns A new KritzelImage instance configured with the core context.
17163
17256
  */
17164
- static create(core) {
17165
- const object = new KritzelImage();
17257
+ static create(core, config) {
17258
+ const object = new KritzelImage(config);
17166
17259
  object._core = core;
17167
17260
  object.id = object.generateId();
17168
17261
  object.workspaceId = core.store.state.activeWorkspace.id;
17169
17262
  object.userId = core.user?.id;
17170
- object.x = 0;
17171
- object.y = 0;
17172
- object.translateX = 0;
17173
- object.translateY = 0;
17174
- object.scale = object._core.store.state.scale;
17263
+ object.x = config?.x ?? 0;
17264
+ object.y = config?.y ?? 0;
17265
+ object.translateX = config?.translateX ?? 0;
17266
+ object.translateY = config?.translateY ?? 0;
17267
+ object.scale = config?.scale ?? object._core.store.state.scale;
17268
+ object.rotation = KritzelMathHelper.degreesToRadians(config?.rotation ?? 0);
17175
17269
  object.zIndex = core.store.currentZIndex;
17176
17270
  return object;
17177
17271
  }
@@ -17550,6 +17644,7 @@ class KritzelLine extends KritzelBaseObject {
17550
17644
  this.controlY = config?.controlY;
17551
17645
  this.translateX = config?.translateX ?? 0;
17552
17646
  this.translateY = config?.translateY ?? 0;
17647
+ this.rotation = KritzelMathHelper.degreesToRadians(config?.rotation ?? 0);
17553
17648
  this.scale = config?.scale ?? 1;
17554
17649
  this.strokeWidth = config?.strokeWidth ?? 4;
17555
17650
  this.stroke = config?.stroke ?? { light: '#000000', dark: '#ffffff' };
@@ -17580,6 +17675,7 @@ class KritzelLine extends KritzelBaseObject {
17580
17675
  object.controlY = options?.controlY;
17581
17676
  object.translateX = options?.translateX ?? 0;
17582
17677
  object.translateY = options?.translateY ?? 0;
17678
+ object.rotation = KritzelMathHelper.degreesToRadians(options?.rotation ?? 0);
17583
17679
  object.scale = options?.scale ?? 1;
17584
17680
  object.strokeWidth = options?.strokeWidth ?? 4;
17585
17681
  object.stroke = options?.stroke ?? { light: '#000000', dark: '#ffffff' };
@@ -18274,6 +18370,10 @@ class KritzelClassHelper {
18274
18370
  */
18275
18371
  class KritzelGroup extends KritzelBaseObject {
18276
18372
  __class__ = 'KritzelGroup';
18373
+ constructor(config) {
18374
+ super();
18375
+ this.rotation = KritzelMathHelper.degreesToRadians(config?.rotation ?? 0);
18376
+ }
18277
18377
  /**
18278
18378
  * IDs of child objects within this group.
18279
18379
  * Children can be any KritzelBaseObject, including other KritzelGroups for nesting.
@@ -18325,8 +18425,8 @@ class KritzelGroup extends KritzelBaseObject {
18325
18425
  * @param core - The KritzelCore instance providing access to store and state management.
18326
18426
  * @returns A new KritzelGroup instance configured with the core context.
18327
18427
  */
18328
- static create(core) {
18329
- const group = new KritzelGroup();
18428
+ static create(core, config) {
18429
+ const group = new KritzelGroup(config);
18330
18430
  group._core = core;
18331
18431
  group.id = group.generateId();
18332
18432
  group.workspaceId = core.store.state.activeWorkspace.id;
@@ -18915,6 +19015,7 @@ class KritzelShape extends KritzelBaseObject {
18915
19015
  this.y = config.y ?? 0;
18916
19016
  this.translateX = config.translateX ?? 0;
18917
19017
  this.translateY = config.translateY ?? 0;
19018
+ this.rotation = KritzelMathHelper.degreesToRadians(config.rotation ?? 0);
18918
19019
  this.width = config.width ?? 100;
18919
19020
  this.height = config.height ?? 100;
18920
19021
  this.shapeType = config.shapeType ?? exports.ShapeType.Rectangle;
@@ -18946,6 +19047,7 @@ class KritzelShape extends KritzelBaseObject {
18946
19047
  object.y = config?.y ?? 0;
18947
19048
  object.translateX = config?.translateX ?? 0;
18948
19049
  object.translateY = config?.translateY ?? 0;
19050
+ object.rotation = KritzelMathHelper.degreesToRadians(config?.rotation ?? 0);
18949
19051
  object.width = config?.width ?? 100;
18950
19052
  object.height = config?.height ?? 100;
18951
19053
  object.shapeType = config?.shapeType ?? exports.ShapeType.Rectangle;
@@ -19444,71 +19546,6 @@ class KritzelShape extends KritzelBaseObject {
19444
19546
  }
19445
19547
  }
19446
19548
 
19447
- exports.KritzelMouseButton = void 0;
19448
- (function (KritzelMouseButton) {
19449
- KritzelMouseButton[KritzelMouseButton["Left"] = 0] = "Left";
19450
- KritzelMouseButton[KritzelMouseButton["Middle"] = 1] = "Middle";
19451
- KritzelMouseButton[KritzelMouseButton["Right"] = 2] = "Right";
19452
- })(exports.KritzelMouseButton || (exports.KritzelMouseButton = {}));
19453
-
19454
- class KritzelEventHelper {
19455
- static isRightClick(ev) {
19456
- return ev.button === exports.KritzelMouseButton.Right;
19457
- }
19458
- static isLeftClick(ev) {
19459
- return ev.button === exports.KritzelMouseButton.Left;
19460
- }
19461
- static isPointerEventOnContextMenu(event) {
19462
- const path = event.composedPath();
19463
- const contextMenu = path.find(element => element.classList && element.classList.contains('context-menu'));
19464
- return !!contextMenu;
19465
- }
19466
- static onLongPress(event, onSuccess, onCancel) {
19467
- if (event.pointerType !== 'touch') {
19468
- onCancel?.();
19469
- return () => { };
19470
- }
19471
- const startX = event.clientX;
19472
- const startY = event.clientY;
19473
- const target = event.target;
19474
- if (!target) {
19475
- onCancel?.();
19476
- return () => { };
19477
- }
19478
- const longPressTimeout = 400;
19479
- const moveThreshold = 10;
19480
- const timer = setTimeout(() => {
19481
- removeListeners();
19482
- onSuccess(event);
19483
- }, longPressTimeout);
19484
- const cancel = () => {
19485
- clearTimeout(timer);
19486
- removeListeners();
19487
- onCancel?.();
19488
- };
19489
- const onPointerMove = (e) => {
19490
- if (Math.abs(e.clientX - startX) > moveThreshold || Math.abs(e.clientY - startY) > moveThreshold) {
19491
- cancel();
19492
- }
19493
- };
19494
- const onPointerUp = () => {
19495
- cancel();
19496
- };
19497
- const onPointerCancel = () => {
19498
- cancel();
19499
- };
19500
- const removeListeners = () => {
19501
- target.removeEventListener('pointermove', onPointerMove);
19502
- target.removeEventListener('pointerup', onPointerUp);
19503
- target.removeEventListener('pointercancel', onPointerCancel);
19504
- };
19505
- target.addEventListener('pointermove', onPointerMove, { passive: true });
19506
- target.addEventListener('pointerup', onPointerUp, { once: true });
19507
- target.addEventListener('pointercancel', onPointerCancel, { once: true });
19508
- return cancel;
19509
- }
19510
- }
19511
-
19512
19549
  /**
19513
19550
  * Abstract base class for all drawing tools in Kritzel.
19514
19551
  * Provides common functionality and defines the interface that all tools must implement.
@@ -19597,6 +19634,71 @@ class KritzelBaseTool {
19597
19634
  }
19598
19635
  }
19599
19636
 
19637
+ exports.KritzelMouseButton = void 0;
19638
+ (function (KritzelMouseButton) {
19639
+ KritzelMouseButton[KritzelMouseButton["Left"] = 0] = "Left";
19640
+ KritzelMouseButton[KritzelMouseButton["Middle"] = 1] = "Middle";
19641
+ KritzelMouseButton[KritzelMouseButton["Right"] = 2] = "Right";
19642
+ })(exports.KritzelMouseButton || (exports.KritzelMouseButton = {}));
19643
+
19644
+ class KritzelEventHelper {
19645
+ static isRightClick(ev) {
19646
+ return ev.button === exports.KritzelMouseButton.Right;
19647
+ }
19648
+ static isLeftClick(ev) {
19649
+ return ev.button === exports.KritzelMouseButton.Left;
19650
+ }
19651
+ static isPointerEventOnContextMenu(event) {
19652
+ const path = event.composedPath();
19653
+ const contextMenu = path.find(element => element.classList && element.classList.contains('context-menu'));
19654
+ return !!contextMenu;
19655
+ }
19656
+ static onLongPress(event, onSuccess, onCancel) {
19657
+ if (event.pointerType !== 'touch') {
19658
+ onCancel?.();
19659
+ return () => { };
19660
+ }
19661
+ const startX = event.clientX;
19662
+ const startY = event.clientY;
19663
+ const target = event.target;
19664
+ if (!target) {
19665
+ onCancel?.();
19666
+ return () => { };
19667
+ }
19668
+ const longPressTimeout = 400;
19669
+ const moveThreshold = 10;
19670
+ const timer = setTimeout(() => {
19671
+ removeListeners();
19672
+ onSuccess(event);
19673
+ }, longPressTimeout);
19674
+ const cancel = () => {
19675
+ clearTimeout(timer);
19676
+ removeListeners();
19677
+ onCancel?.();
19678
+ };
19679
+ const onPointerMove = (e) => {
19680
+ if (Math.abs(e.clientX - startX) > moveThreshold || Math.abs(e.clientY - startY) > moveThreshold) {
19681
+ cancel();
19682
+ }
19683
+ };
19684
+ const onPointerUp = () => {
19685
+ cancel();
19686
+ };
19687
+ const onPointerCancel = () => {
19688
+ cancel();
19689
+ };
19690
+ const removeListeners = () => {
19691
+ target.removeEventListener('pointermove', onPointerMove);
19692
+ target.removeEventListener('pointerup', onPointerUp);
19693
+ target.removeEventListener('pointercancel', onPointerCancel);
19694
+ };
19695
+ target.addEventListener('pointermove', onPointerMove, { passive: true });
19696
+ target.addEventListener('pointerup', onPointerUp, { once: true });
19697
+ target.addEventListener('pointercancel', onPointerCancel, { once: true });
19698
+ return cancel;
19699
+ }
19700
+ }
19701
+
19600
19702
  const DEFAULT_STROKE_SIZES = [4, 6, 8, 12, 16, 24];
19601
19703
  const DEFAULT_FONT_SIZES = [8, 10, 12, 16, 20, 24];
19602
19704
 
@@ -19625,7 +19727,7 @@ class KritzelBrushTool extends KritzelBaseTool {
19625
19727
  * websocket traffic without visible quality loss — `perfect-freehand`
19626
19728
  * already smooths the rendered stroke.
19627
19729
  */
19628
- static MIN_POINT_DISTANCE_PX = 5;
19730
+ static MIN_POINT_DISTANCE_PX = 3;
19629
19731
  /** Tracks the ID of the path currently being drawn */
19630
19732
  _currentPathId = null;
19631
19733
  /**
@@ -19839,6 +19941,10 @@ class KritzelBrushTool extends KritzelBaseTool {
19839
19941
  */
19840
19942
  class KritzelSelectionGroup extends KritzelBaseObject {
19841
19943
  __class__ = 'KritzelSelectionGroup';
19944
+ constructor(config) {
19945
+ super();
19946
+ this.rotation = KritzelMathHelper.degreesToRadians(config?.rotation ?? 0);
19947
+ }
19842
19948
  // Store only object IDs instead of full objects
19843
19949
  _objectIds = [];
19844
19950
  // Cached objects array - invalidated when objectIds changes
@@ -19922,8 +20028,8 @@ class KritzelSelectionGroup extends KritzelBaseObject {
19922
20028
  * @param core - The KritzelCore instance to associate with this selection group
19923
20029
  * @returns A new KritzelSelectionGroup instance configured with default settings
19924
20030
  */
19925
- static create(core) {
19926
- const object = new KritzelSelectionGroup();
20031
+ static create(core, config) {
20032
+ const object = new KritzelSelectionGroup(config);
19927
20033
  object._core = core;
19928
20034
  object.id = object.generateId();
19929
20035
  object.workspaceId = core.store.state.activeWorkspace.id;
@@ -21540,6 +21646,7 @@ KritzelIconRegistry.registerIcons({
21540
21646
  'undo': '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-undo-icon lucide-undo"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/></svg>',
21541
21647
  'redo': '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-redo-icon lucide-redo"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7"/></svg>',
21542
21648
  'plus': '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus-icon lucide-plus"><path d="M5 12h14"/><path d="M12 5v14"/></svg>',
21649
+ 'minus': '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-minus-icon lucide-minus"><path d="M5 12h14"/></svg>',
21543
21650
  'ellipsis-vertical': '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ellipsis-vertical-icon lucide-ellipsis-vertical"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>',
21544
21651
  'x': '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',
21545
21652
  'check': '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check-icon lucide-check"><path d="M20 6 9 17l-5-5"/></svg>',
@@ -28313,6 +28420,890 @@ class KritzelAnchorManager {
28313
28420
  }
28314
28421
  }
28315
28422
 
28423
+ /**
28424
+ * Pure static utilities for localization term resolution.
28425
+ */
28426
+ class LocalizationHelper {
28427
+ /**
28428
+ * Interpolates `{placeholder}` tokens in a term template with the provided
28429
+ * variables. Tokens without a matching variable are left untouched.
28430
+ *
28431
+ * @param template - The raw term string, e.g. `'Expires {date}'`
28432
+ * @param vars - Map of placeholder names to replacement values
28433
+ * @returns The interpolated string
28434
+ *
28435
+ * @example
28436
+ * LocalizationHelper.interpolate('Hello {name}', { name: 'Ada' }); // 'Hello Ada'
28437
+ */
28438
+ static interpolate(template, vars) {
28439
+ if (!vars) {
28440
+ return template;
28441
+ }
28442
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
28443
+ const value = vars[key];
28444
+ return value === undefined || value === null ? match : String(value);
28445
+ });
28446
+ }
28447
+ }
28448
+
28449
+ /**
28450
+ * Built-in English locale (default). Provides a complete set of term values
28451
+ * that act as the ultimate fallback when other locales omit a key.
28452
+ */
28453
+ const EN_LOCALE = {
28454
+ code: 'en',
28455
+ label: 'English',
28456
+ terms: {
28457
+ // Context menu
28458
+ 'menu.copy': 'Copy',
28459
+ 'menu.cut': 'Cut',
28460
+ 'menu.paste': 'Paste',
28461
+ 'menu.selectAll': 'Select All',
28462
+ 'menu.order': 'Order',
28463
+ 'menu.bringToFront': 'Bring to Front',
28464
+ 'menu.sendToBack': 'Send to Back',
28465
+ 'menu.moveUp': 'Move Up',
28466
+ 'menu.moveDown': 'Move Down',
28467
+ 'menu.align': 'Align',
28468
+ 'menu.alignLeft': 'Align Left',
28469
+ 'menu.alignCenterHorizontal': 'Align Center Horizontally',
28470
+ 'menu.alignRight': 'Align Right',
28471
+ 'menu.alignTop': 'Align Top',
28472
+ 'menu.alignCenterVertical': 'Align Center Vertically',
28473
+ 'menu.alignBottom': 'Align Bottom',
28474
+ 'menu.group': 'Group',
28475
+ 'menu.ungroup': 'Ungroup',
28476
+ 'menu.export': 'Export',
28477
+ 'menu.exportAsSvg': 'Export as SVG',
28478
+ 'menu.exportAsPng': 'Export as PNG',
28479
+ 'menu.delete': 'Delete',
28480
+ // More menu
28481
+ 'menu.share': 'Share',
28482
+ 'menu.import': 'Import',
28483
+ 'menu.settings': 'Settings',
28484
+ 'menu.logout': 'Logout',
28485
+ // Settings dialog
28486
+ 'settings.dialogTitle': 'Settings',
28487
+ 'settings.categories.general': 'General',
28488
+ 'settings.categories.viewport': 'Viewport',
28489
+ 'settings.categories.shortcuts': 'Keyboard Shortcuts',
28490
+ 'settings.categories.developer': 'Developer Options',
28491
+ 'settings.categories.about': 'About',
28492
+ 'settings.general.title': 'General Settings',
28493
+ 'settings.general.theme.label': 'Theme',
28494
+ 'settings.general.theme.description': 'Select a registered color theme for the editor interface.',
28495
+ 'settings.general.language.label': 'Language',
28496
+ 'settings.general.language.description': 'Select the display language for the editor interface.',
28497
+ 'settings.general.lockDrawingScale.label': 'Lock Drawing Scale',
28498
+ 'settings.general.lockDrawingScale.description': 'When enabled, drawn objects maintain a fixed visual size regardless of the current zoom level.',
28499
+ 'settings.viewport.title': 'Viewport Settings',
28500
+ 'settings.viewport.minZoom.label': 'Minimum Zoom Level',
28501
+ 'settings.viewport.minZoom.description': 'Sets the minimum zoom level. Lower values allow zooming out further to see more of the canvas.',
28502
+ 'settings.viewport.maxZoom.label': 'Maximum Zoom Level',
28503
+ 'settings.viewport.maxZoom.description': 'Sets the maximum zoom level. Higher values allow zooming in closer for detailed work.',
28504
+ 'settings.viewport.boundaryLeft.label': 'Viewport Boundary Left',
28505
+ 'settings.viewport.boundaryLeft.description': 'Left boundary in world coordinates. Set to limit how far left the viewport can pan.',
28506
+ 'settings.viewport.boundaryRight.label': 'Viewport Boundary Right',
28507
+ 'settings.viewport.boundaryRight.description': 'Right boundary in world coordinates. Set to limit how far right the viewport can pan.',
28508
+ 'settings.viewport.boundaryTop.label': 'Viewport Boundary Top',
28509
+ 'settings.viewport.boundaryTop.description': 'Top boundary in world coordinates. Set to limit how far up the viewport can pan.',
28510
+ 'settings.viewport.boundaryBottom.label': 'Viewport Boundary Bottom',
28511
+ 'settings.viewport.boundaryBottom.description': 'Bottom boundary in world coordinates. Set to limit how far down the viewport can pan.',
28512
+ 'settings.viewport.boundaryPlaceholder': 'Infinite',
28513
+ 'settings.shortcuts.title': 'Keyboard Shortcuts',
28514
+ 'settings.developer.title': 'Developer Options',
28515
+ 'settings.developer.showViewportInfo.label': 'Show Viewport Info',
28516
+ 'settings.developer.showViewportInfo.description': 'Display viewport debug information such as position, zoom level, and boundaries.',
28517
+ 'settings.developer.showObjectInfo.label': 'Show Object Info',
28518
+ 'settings.developer.showObjectInfo.description': 'Display debug information about objects on the canvas.',
28519
+ 'settings.developer.showSyncProviderInfo.label': 'Show Sync Provider Info',
28520
+ 'settings.developer.showSyncProviderInfo.description': 'Display debug information about the sync provider connection status.',
28521
+ 'settings.developer.showMigrationInfo.label': 'Show Migration Info',
28522
+ 'settings.developer.showMigrationInfo.description': 'Display debug information about data migrations.',
28523
+ 'settings.about.title': 'About',
28524
+ 'settings.about.description': 'Kritzel - A drawing application',
28525
+ // Export dialog
28526
+ 'export.dialogTitle': 'Export',
28527
+ 'export.tabs.viewport': 'Export Viewport',
28528
+ 'export.tabs.workspace': 'Export Workspace',
28529
+ 'export.format.label': 'Format',
28530
+ 'export.filename.label': 'Filename',
28531
+ 'export.filename.placeholder': 'Enter filename',
28532
+ 'export.exportButton': 'Export',
28533
+ // Workspace manager
28534
+ 'workspace.sharedTooltip': 'Shared workspace',
28535
+ 'workspace.rename': 'Rename',
28536
+ 'workspace.delete': 'Delete',
28537
+ // Zoom panel
28538
+ 'zoom.zoomIn': 'Zoom in',
28539
+ 'zoom.zoomOut': 'Zoom out',
28540
+ // Utility panel
28541
+ 'utility.undo': 'Undo',
28542
+ 'utility.redo': 'Redo',
28543
+ 'utility.delete': 'Delete selected items',
28544
+ // Share dialog
28545
+ 'share.dialogTitle': 'Share Workspace',
28546
+ 'share.linkSharing.label': 'Link sharing',
28547
+ 'share.linkSharing.enabledDescription': 'Anyone with the link can access this workspace.',
28548
+ 'share.linkSharing.disabledDescription': 'Link sharing is disabled. Only you can access this workspace.',
28549
+ 'share.linkSharing.toggleLabel': 'Enable link sharing',
28550
+ 'share.copyLink.title': 'Copy link',
28551
+ 'share.copyLink.copied': 'Copied!',
28552
+ // Login dialog
28553
+ 'login.dialogTitle': 'Sign in',
28554
+ // Current user dialog
28555
+ 'currentUser.dialogTitle': 'Account',
28556
+ // Back to content
28557
+ 'backToContent.label': 'Back to content',
28558
+ // Tool config
28559
+ 'toolConfig.collapse': 'Collapse',
28560
+ 'toolConfig.expand': 'Expand',
28561
+ // More menu button
28562
+ 'moreMenu.ariaLabel': 'More options',
28563
+ // Engine
28564
+ 'engine.loading': 'Loading...',
28565
+ // Watermark
28566
+ 'watermark.poweredBy': 'Powered by Kritzel',
28567
+ },
28568
+ };
28569
+
28570
+ /**
28571
+ * Built-in German locale.
28572
+ */
28573
+ const DE_LOCALE = {
28574
+ code: 'de',
28575
+ label: 'Deutsch',
28576
+ terms: {
28577
+ // Context menu
28578
+ 'menu.copy': 'Kopieren',
28579
+ 'menu.cut': 'Ausschneiden',
28580
+ 'menu.paste': 'Einfügen',
28581
+ 'menu.selectAll': 'Alles auswählen',
28582
+ 'menu.order': 'Anordnen',
28583
+ 'menu.bringToFront': 'In den Vordergrund',
28584
+ 'menu.sendToBack': 'In den Hintergrund',
28585
+ 'menu.moveUp': 'Nach vorne',
28586
+ 'menu.moveDown': 'Nach hinten',
28587
+ 'menu.align': 'Ausrichten',
28588
+ 'menu.alignLeft': 'Linksbündig ausrichten',
28589
+ 'menu.alignCenterHorizontal': 'Horizontal zentrieren',
28590
+ 'menu.alignRight': 'Rechtsbündig ausrichten',
28591
+ 'menu.alignTop': 'Oben ausrichten',
28592
+ 'menu.alignCenterVertical': 'Vertikal zentrieren',
28593
+ 'menu.alignBottom': 'Unten ausrichten',
28594
+ 'menu.group': 'Gruppieren',
28595
+ 'menu.ungroup': 'Gruppierung aufheben',
28596
+ 'menu.export': 'Exportieren',
28597
+ 'menu.exportAsSvg': 'Als SVG exportieren',
28598
+ 'menu.exportAsPng': 'Als PNG exportieren',
28599
+ 'menu.delete': 'Löschen',
28600
+ // More menu
28601
+ 'menu.share': 'Teilen',
28602
+ 'menu.import': 'Importieren',
28603
+ 'menu.settings': 'Einstellungen',
28604
+ 'menu.logout': 'Abmelden',
28605
+ // Settings dialog
28606
+ 'settings.dialogTitle': 'Einstellungen',
28607
+ 'settings.categories.general': 'Allgemein',
28608
+ 'settings.categories.viewport': 'Ansichtsfenster',
28609
+ 'settings.categories.shortcuts': 'Tastenkürzel',
28610
+ 'settings.categories.developer': 'Entwickleroptionen',
28611
+ 'settings.categories.about': 'Über',
28612
+ 'settings.general.title': 'Allgemeine Einstellungen',
28613
+ 'settings.general.theme.label': 'Design',
28614
+ 'settings.general.theme.description': 'Wählen Sie ein registriertes Farbdesign für die Editor-Oberfläche.',
28615
+ 'settings.general.language.label': 'Sprache',
28616
+ 'settings.general.language.description': 'Wählen Sie die Anzeigesprache für die Editor-Oberfläche.',
28617
+ 'settings.general.lockDrawingScale.label': 'Zeichenskalierung sperren',
28618
+ 'settings.general.lockDrawingScale.description': 'Wenn aktiviert, behalten gezeichnete Objekte unabhängig von der aktuellen Zoomstufe eine feste visuelle Größe.',
28619
+ 'settings.viewport.title': 'Ansichtsfenster-Einstellungen',
28620
+ 'settings.viewport.minZoom.label': 'Minimale Zoomstufe',
28621
+ 'settings.viewport.minZoom.description': 'Legt die minimale Zoomstufe fest. Niedrigere Werte ermöglichen weiteres Herauszoomen, um mehr von der Zeichenfläche zu sehen.',
28622
+ 'settings.viewport.maxZoom.label': 'Maximale Zoomstufe',
28623
+ 'settings.viewport.maxZoom.description': 'Legt die maximale Zoomstufe fest. Höhere Werte ermöglichen näheres Heranzoomen für Detailarbeit.',
28624
+ 'settings.viewport.boundaryLeft.label': 'Ansichtsfenster-Grenze links',
28625
+ 'settings.viewport.boundaryLeft.description': 'Linke Grenze in Weltkoordinaten. Legen Sie fest, wie weit das Ansichtsfenster nach links verschoben werden kann.',
28626
+ 'settings.viewport.boundaryRight.label': 'Ansichtsfenster-Grenze rechts',
28627
+ 'settings.viewport.boundaryRight.description': 'Rechte Grenze in Weltkoordinaten. Legen Sie fest, wie weit das Ansichtsfenster nach rechts verschoben werden kann.',
28628
+ 'settings.viewport.boundaryTop.label': 'Ansichtsfenster-Grenze oben',
28629
+ 'settings.viewport.boundaryTop.description': 'Obere Grenze in Weltkoordinaten. Legen Sie fest, wie weit das Ansichtsfenster nach oben verschoben werden kann.',
28630
+ 'settings.viewport.boundaryBottom.label': 'Ansichtsfenster-Grenze unten',
28631
+ 'settings.viewport.boundaryBottom.description': 'Untere Grenze in Weltkoordinaten. Legen Sie fest, wie weit das Ansichtsfenster nach unten verschoben werden kann.',
28632
+ 'settings.viewport.boundaryPlaceholder': 'Unendlich',
28633
+ 'settings.shortcuts.title': 'Tastenkürzel',
28634
+ 'settings.developer.title': 'Entwickleroptionen',
28635
+ 'settings.developer.showViewportInfo.label': 'Ansichtsfenster-Infos anzeigen',
28636
+ 'settings.developer.showViewportInfo.description': 'Zeigt Debug-Informationen zum Ansichtsfenster an, z. B. Position, Zoomstufe und Grenzen.',
28637
+ 'settings.developer.showObjectInfo.label': 'Objekt-Infos anzeigen',
28638
+ 'settings.developer.showObjectInfo.description': 'Zeigt Debug-Informationen zu Objekten auf der Zeichenfläche an.',
28639
+ 'settings.developer.showSyncProviderInfo.label': 'Sync-Provider-Infos anzeigen',
28640
+ 'settings.developer.showSyncProviderInfo.description': 'Zeigt Debug-Informationen zum Verbindungsstatus des Sync-Providers an.',
28641
+ 'settings.developer.showMigrationInfo.label': 'Migrations-Infos anzeigen',
28642
+ 'settings.developer.showMigrationInfo.description': 'Zeigt Debug-Informationen zu Datenmigrationen an.',
28643
+ 'settings.about.title': 'Über',
28644
+ 'settings.about.description': 'Kritzel – Eine Zeichenanwendung',
28645
+ // Export dialog
28646
+ 'export.dialogTitle': 'Exportieren',
28647
+ 'export.tabs.viewport': 'Ansichtsfenster exportieren',
28648
+ 'export.tabs.workspace': 'Arbeitsbereich exportieren',
28649
+ 'export.format.label': 'Format',
28650
+ 'export.filename.label': 'Dateiname',
28651
+ 'export.filename.placeholder': 'Dateiname eingeben',
28652
+ 'export.exportButton': 'Exportieren',
28653
+ // Workspace manager
28654
+ 'workspace.sharedTooltip': 'Geteilter Arbeitsbereich',
28655
+ 'workspace.rename': 'Umbenennen',
28656
+ 'workspace.delete': 'Löschen',
28657
+ // Zoom panel
28658
+ 'zoom.zoomIn': 'Vergrößern',
28659
+ 'zoom.zoomOut': 'Verkleinern',
28660
+ // Utility panel
28661
+ 'utility.undo': 'Rückgängig',
28662
+ 'utility.redo': 'Wiederholen',
28663
+ 'utility.delete': 'Ausgewählte Elemente löschen',
28664
+ // Share dialog
28665
+ 'share.dialogTitle': 'Arbeitsbereich teilen',
28666
+ 'share.linkSharing.label': 'Link-Freigabe',
28667
+ 'share.linkSharing.enabledDescription': 'Jeder mit dem Link kann auf diesen Arbeitsbereich zugreifen.',
28668
+ 'share.linkSharing.disabledDescription': 'Die Link-Freigabe ist deaktiviert. Nur Sie können auf diesen Arbeitsbereich zugreifen.',
28669
+ 'share.linkSharing.toggleLabel': 'Link-Freigabe aktivieren',
28670
+ 'share.copyLink.title': 'Link kopieren',
28671
+ 'share.copyLink.copied': 'Kopiert!',
28672
+ // Login dialog
28673
+ 'login.dialogTitle': 'Anmelden',
28674
+ // Current user dialog
28675
+ 'currentUser.dialogTitle': 'Konto',
28676
+ // Back to content
28677
+ 'backToContent.label': 'Zurück zum Inhalt',
28678
+ // Tool config
28679
+ 'toolConfig.collapse': 'Einklappen',
28680
+ 'toolConfig.expand': 'Ausklappen',
28681
+ // More menu button
28682
+ 'moreMenu.ariaLabel': 'Weitere Optionen',
28683
+ // Engine
28684
+ 'engine.loading': 'Wird geladen...',
28685
+ // Watermark
28686
+ 'watermark.poweredBy': 'Bereitgestellt von Kritzel',
28687
+ },
28688
+ };
28689
+
28690
+ /**
28691
+ * Built-in French locale.
28692
+ */
28693
+ const FR_LOCALE = {
28694
+ code: 'fr',
28695
+ label: 'Français',
28696
+ terms: {
28697
+ // Context menu
28698
+ 'menu.copy': 'Copier',
28699
+ 'menu.cut': 'Couper',
28700
+ 'menu.paste': 'Coller',
28701
+ 'menu.selectAll': 'Tout sélectionner',
28702
+ 'menu.order': 'Ordre',
28703
+ 'menu.bringToFront': 'Mettre au premier plan',
28704
+ 'menu.sendToBack': "Mettre à l'arrière-plan",
28705
+ 'menu.moveUp': 'Avancer',
28706
+ 'menu.moveDown': 'Reculer',
28707
+ 'menu.align': 'Aligner',
28708
+ 'menu.alignLeft': 'Aligner à gauche',
28709
+ 'menu.alignCenterHorizontal': 'Centrer horizontalement',
28710
+ 'menu.alignRight': 'Aligner à droite',
28711
+ 'menu.alignTop': 'Aligner en haut',
28712
+ 'menu.alignCenterVertical': 'Centrer verticalement',
28713
+ 'menu.alignBottom': 'Aligner en bas',
28714
+ 'menu.group': 'Grouper',
28715
+ 'menu.ungroup': 'Dégrouper',
28716
+ 'menu.export': 'Exporter',
28717
+ 'menu.exportAsSvg': 'Exporter en SVG',
28718
+ 'menu.exportAsPng': 'Exporter en PNG',
28719
+ 'menu.delete': 'Supprimer',
28720
+ // More menu
28721
+ 'menu.share': 'Partager',
28722
+ 'menu.import': 'Importer',
28723
+ 'menu.settings': 'Paramètres',
28724
+ 'menu.logout': 'Déconnexion',
28725
+ // Settings dialog
28726
+ 'settings.dialogTitle': 'Paramètres',
28727
+ 'settings.categories.general': 'Général',
28728
+ 'settings.categories.viewport': "Fenêtre d'affichage",
28729
+ 'settings.categories.shortcuts': 'Raccourcis clavier',
28730
+ 'settings.categories.developer': 'Options développeur',
28731
+ 'settings.categories.about': 'À propos',
28732
+ 'settings.general.title': 'Paramètres généraux',
28733
+ 'settings.general.theme.label': 'Thème',
28734
+ 'settings.general.theme.description': "Sélectionnez un thème de couleurs enregistré pour l'interface de l'éditeur.",
28735
+ 'settings.general.language.label': 'Langue',
28736
+ 'settings.general.language.description': "Sélectionnez la langue d'affichage de l'interface de l'éditeur.",
28737
+ 'settings.general.lockDrawingScale.label': "Verrouiller l'échelle de dessin",
28738
+ 'settings.general.lockDrawingScale.description': "Lorsque cette option est activée, les objets dessinés conservent une taille visuelle fixe quel que soit le niveau de zoom actuel.",
28739
+ 'settings.viewport.title': "Paramètres de la fenêtre d'affichage",
28740
+ 'settings.viewport.minZoom.label': 'Niveau de zoom minimal',
28741
+ 'settings.viewport.minZoom.description': 'Définit le niveau de zoom minimal. Des valeurs plus faibles permettent de dézoomer davantage pour voir une plus grande partie du canevas.',
28742
+ 'settings.viewport.maxZoom.label': 'Niveau de zoom maximal',
28743
+ 'settings.viewport.maxZoom.description': 'Définit le niveau de zoom maximal. Des valeurs plus élevées permettent de zoomer davantage pour un travail détaillé.',
28744
+ 'settings.viewport.boundaryLeft.label': "Limite gauche de la fenêtre d'affichage",
28745
+ 'settings.viewport.boundaryLeft.description': "Limite gauche en coordonnées du monde. Définissez jusqu'où la fenêtre d'affichage peut se déplacer vers la gauche.",
28746
+ 'settings.viewport.boundaryRight.label': "Limite droite de la fenêtre d'affichage",
28747
+ 'settings.viewport.boundaryRight.description': "Limite droite en coordonnées du monde. Définissez jusqu'où la fenêtre d'affichage peut se déplacer vers la droite.",
28748
+ 'settings.viewport.boundaryTop.label': "Limite supérieure de la fenêtre d'affichage",
28749
+ 'settings.viewport.boundaryTop.description': "Limite supérieure en coordonnées du monde. Définissez jusqu'où la fenêtre d'affichage peut se déplacer vers le haut.",
28750
+ 'settings.viewport.boundaryBottom.label': "Limite inférieure de la fenêtre d'affichage",
28751
+ 'settings.viewport.boundaryBottom.description': "Limite inférieure en coordonnées du monde. Définissez jusqu'où la fenêtre d'affichage peut se déplacer vers le bas.",
28752
+ 'settings.viewport.boundaryPlaceholder': 'Infini',
28753
+ 'settings.shortcuts.title': 'Raccourcis clavier',
28754
+ 'settings.developer.title': 'Options développeur',
28755
+ 'settings.developer.showViewportInfo.label': "Afficher les infos de la fenêtre d'affichage",
28756
+ 'settings.developer.showViewportInfo.description': "Affiche les informations de débogage de la fenêtre d'affichage telles que la position, le niveau de zoom et les limites.",
28757
+ 'settings.developer.showObjectInfo.label': 'Afficher les infos des objets',
28758
+ 'settings.developer.showObjectInfo.description': 'Affiche les informations de débogage des objets sur le canevas.',
28759
+ 'settings.developer.showSyncProviderInfo.label': 'Afficher les infos du fournisseur de synchronisation',
28760
+ 'settings.developer.showSyncProviderInfo.description': "Affiche les informations de débogage sur l'état de connexion du fournisseur de synchronisation.",
28761
+ 'settings.developer.showMigrationInfo.label': 'Afficher les infos de migration',
28762
+ 'settings.developer.showMigrationInfo.description': 'Affiche les informations de débogage sur les migrations de données.',
28763
+ 'settings.about.title': 'À propos',
28764
+ 'settings.about.description': 'Kritzel - Une application de dessin',
28765
+ // Export dialog
28766
+ 'export.dialogTitle': 'Exporter',
28767
+ 'export.tabs.viewport': "Exporter la fenêtre d'affichage",
28768
+ 'export.tabs.workspace': "Exporter l'espace de travail",
28769
+ 'export.format.label': 'Format',
28770
+ 'export.filename.label': 'Nom du fichier',
28771
+ 'export.filename.placeholder': 'Saisir le nom du fichier',
28772
+ 'export.exportButton': 'Exporter',
28773
+ // Workspace manager
28774
+ 'workspace.sharedTooltip': 'Espace de travail partagé',
28775
+ 'workspace.rename': 'Renommer',
28776
+ 'workspace.delete': 'Supprimer',
28777
+ // Zoom panel
28778
+ 'zoom.zoomIn': 'Zoom avant',
28779
+ 'zoom.zoomOut': 'Zoom arrière',
28780
+ // Utility panel
28781
+ 'utility.undo': 'Annuler',
28782
+ 'utility.redo': 'Rétablir',
28783
+ 'utility.delete': 'Supprimer les éléments sélectionnés',
28784
+ // Share dialog
28785
+ 'share.dialogTitle': "Partager l'espace de travail",
28786
+ 'share.linkSharing.label': 'Partage par lien',
28787
+ 'share.linkSharing.enabledDescription': 'Toute personne disposant du lien peut accéder à cet espace de travail.',
28788
+ 'share.linkSharing.disabledDescription': 'Le partage par lien est désactivé. Vous seul pouvez accéder à cet espace de travail.',
28789
+ 'share.linkSharing.toggleLabel': 'Activer le partage par lien',
28790
+ 'share.copyLink.title': 'Copier le lien',
28791
+ 'share.copyLink.copied': 'Copié !',
28792
+ // Login dialog
28793
+ 'login.dialogTitle': 'Se connecter',
28794
+ // Current user dialog
28795
+ 'currentUser.dialogTitle': 'Compte',
28796
+ // Back to content
28797
+ 'backToContent.label': 'Retour au contenu',
28798
+ // Tool config
28799
+ 'toolConfig.collapse': 'Réduire',
28800
+ 'toolConfig.expand': 'Développer',
28801
+ // More menu button
28802
+ 'moreMenu.ariaLabel': "Plus d'options",
28803
+ // Engine
28804
+ 'engine.loading': 'Chargement...',
28805
+ // Watermark
28806
+ 'watermark.poweredBy': 'Propulsé par Kritzel',
28807
+ },
28808
+ };
28809
+
28810
+ /** Key used to store the settings object in localStorage. */
28811
+ const SETTINGS_STORAGE_KEY = 'kritzel-settings';
28812
+ /** Default locale used when no stored preference exists. */
28813
+ const DEFAULT_LOCALE = 'en';
28814
+ /**
28815
+ * Manages localization state and term resolution across the Kritzel editor.
28816
+ *
28817
+ * The manager is owned per {@link KritzelCore} instance (like the theme manager)
28818
+ * so that multiple editors on the same page can use different languages in
28819
+ * isolation.
28820
+ *
28821
+ * Term resolution follows a deterministic fallback chain for each key:
28822
+ * 1. active locale's custom terms
28823
+ * 2. active locale's built-in terms
28824
+ * 3. fallback locale's custom terms
28825
+ * 4. fallback locale's built-in terms
28826
+ * 5. the key itself (last resort)
28827
+ *
28828
+ * Persistence of the selected locale is handled by the KritzelSettings
28829
+ * component; this manager only reads the stored value.
28830
+ */
28831
+ class KritzelLocalizationManager {
28832
+ _core;
28833
+ _storageKey;
28834
+ /** Built-in locale definitions, keyed by locale code. */
28835
+ _builtinLocales = new Map();
28836
+ /** Consumer-registered locale definitions, keyed by locale code. */
28837
+ _customLocales = new Map();
28838
+ _currentLocale = DEFAULT_LOCALE;
28839
+ _fallbackLocale = DEFAULT_LOCALE;
28840
+ /**
28841
+ * Creates a new KritzelLocalizationManager instance.
28842
+ * Seeds the built-in locales and initializes the current locale from the
28843
+ * settings object in localStorage (or the default locale).
28844
+ *
28845
+ * @param core - The KritzelCore instance this manager belongs to
28846
+ */
28847
+ constructor(core) {
28848
+ this._core = core;
28849
+ this._storageKey = core.editorId ? `${SETTINGS_STORAGE_KEY}-${core.editorId}` : SETTINGS_STORAGE_KEY;
28850
+ this._builtinLocales.set(EN_LOCALE.code, EN_LOCALE);
28851
+ this._builtinLocales.set(DE_LOCALE.code, DE_LOCALE);
28852
+ this._builtinLocales.set(FR_LOCALE.code, FR_LOCALE);
28853
+ this._currentLocale = this.getStoredLocale();
28854
+ }
28855
+ /**
28856
+ * Gets the currently active locale code.
28857
+ */
28858
+ get currentLocale() {
28859
+ return this._currentLocale;
28860
+ }
28861
+ /**
28862
+ * Gets the fallback locale code used when a term is missing from the active locale.
28863
+ */
28864
+ get fallbackLocale() {
28865
+ return this._fallbackLocale;
28866
+ }
28867
+ /**
28868
+ * Sets the fallback locale code used when a term is missing from the active locale.
28869
+ *
28870
+ * @param code - The fallback locale code
28871
+ */
28872
+ setFallbackLocale(code) {
28873
+ this._fallbackLocale = code;
28874
+ }
28875
+ /**
28876
+ * Registers consumer-provided locale definitions. Definitions are merged by
28877
+ * code; registering the same code again replaces the previous definition.
28878
+ * A registered locale may provide a partial set of terms — missing terms are
28879
+ * resolved through the fallback chain.
28880
+ *
28881
+ * @param locales - The locale definitions to register
28882
+ */
28883
+ registerLocales(locales) {
28884
+ for (const locale of locales) {
28885
+ this._customLocales.set(locale.code, locale);
28886
+ }
28887
+ }
28888
+ /**
28889
+ * Sets the active locale and triggers a re-render so UI strings update.
28890
+ *
28891
+ * @param code - The locale code to activate
28892
+ */
28893
+ setLocale(code) {
28894
+ this._currentLocale = code;
28895
+ this._core.rerender();
28896
+ }
28897
+ /**
28898
+ * Gets the list of available locale codes (built-in and registered).
28899
+ */
28900
+ getAvailableLocales() {
28901
+ return Array.from(new Set([...this._builtinLocales.keys(), ...this._customLocales.keys()]));
28902
+ }
28903
+ /**
28904
+ * Gets the available locales as `{ code, label }` pairs for use in a selector.
28905
+ * A registered locale's label takes precedence over the built-in label; the
28906
+ * code is used as the label when none is provided.
28907
+ */
28908
+ getAvailableLocaleOptions() {
28909
+ return this.getAvailableLocales().map(code => ({ code, label: this.getLocaleLabel(code) }));
28910
+ }
28911
+ /**
28912
+ * Gets the display label for a locale code, falling back to the code itself.
28913
+ *
28914
+ * @param code - The locale code
28915
+ */
28916
+ getLocaleLabel(code) {
28917
+ return this._customLocales.get(code)?.label ?? this._builtinLocales.get(code)?.label ?? code;
28918
+ }
28919
+ /**
28920
+ * Resolves a term key to its translated string for the active locale and
28921
+ * interpolates any `{placeholder}` tokens.
28922
+ *
28923
+ * @param key - The term key to resolve
28924
+ * @param vars - Optional values for `{placeholder}` interpolation
28925
+ * @returns The translated, interpolated string (or the key if unresolved)
28926
+ *
28927
+ * @example
28928
+ * manager.translate('menu.copy'); // 'Copy'
28929
+ * manager.translate('share.expires', { date: '5/1' }); // 'Expires 5/1'
28930
+ */
28931
+ translate(key, vars) {
28932
+ const resolved = this._customLocales.get(this._currentLocale)?.terms[key] ??
28933
+ this._builtinLocales.get(this._currentLocale)?.terms[key] ??
28934
+ this._customLocales.get(this._fallbackLocale)?.terms[key] ??
28935
+ this._builtinLocales.get(this._fallbackLocale)?.terms[key] ??
28936
+ key;
28937
+ return LocalizationHelper.interpolate(resolved, vars);
28938
+ }
28939
+ /**
28940
+ * Resolves every known term key for the active locale into a flat map.
28941
+ *
28942
+ * The key set is the union of all keys defined across the built-in and
28943
+ * registered locales, so consumer-added keys are included. Each value is
28944
+ * resolved through the same fallback chain as {@link translate} (without
28945
+ * interpolation, since these are shared, variable-free UI labels).
28946
+ *
28947
+ * @returns A map of every term key to its resolved string for the active locale
28948
+ */
28949
+ getAllTerms() {
28950
+ const keys = new Set();
28951
+ for (const locale of [...this._builtinLocales.values(), ...this._customLocales.values()]) {
28952
+ for (const key of Object.keys(locale.terms)) {
28953
+ keys.add(key);
28954
+ }
28955
+ }
28956
+ const result = {};
28957
+ for (const key of keys) {
28958
+ result[key] = this.translate(key);
28959
+ }
28960
+ return result;
28961
+ }
28962
+ /**
28963
+ * Reads the stored locale from the settings object in localStorage using this
28964
+ * instance's namespaced key.
28965
+ *
28966
+ * @returns The stored locale code if valid, or the default locale
28967
+ */
28968
+ getStoredLocale() {
28969
+ if (typeof localStorage === 'undefined') {
28970
+ return DEFAULT_LOCALE;
28971
+ }
28972
+ const stored = localStorage.getItem(this._storageKey);
28973
+ if (!stored) {
28974
+ return DEFAULT_LOCALE;
28975
+ }
28976
+ try {
28977
+ const parsed = JSON.parse(stored);
28978
+ if (typeof parsed?.locale === 'string') {
28979
+ return parsed.locale;
28980
+ }
28981
+ }
28982
+ catch {
28983
+ // Invalid JSON, use default
28984
+ }
28985
+ return DEFAULT_LOCALE;
28986
+ }
28987
+ /**
28988
+ * Cleans up the localization manager state.
28989
+ * Provided for lifecycle parity with other managers.
28990
+ */
28991
+ cleanup() {
28992
+ this._customLocales.clear();
28993
+ }
28994
+ }
28995
+
28996
+ /**
28997
+ * License verification constants.
28998
+ *
28999
+ * Kritzel licenses are offline Ed25519 signed tokens. The owner signs each
29000
+ * customer's token with a private key that never leaves their machine
29001
+ * (see scripts/license/); the library verifies tokens against the public key
29002
+ * below. The public key is safe to ship — it can only *verify* signatures,
29003
+ * never forge them.
29004
+ */
29005
+ /**
29006
+ * Token format version prefix. A valid license key looks like:
29007
+ *
29008
+ * KRTZL1.<base64url(payload JSON)>.<base64url(ed25519 signature)>
29009
+ *
29010
+ * Bump the version (and this prefix) if the token scheme ever changes.
29011
+ */
29012
+ const KRITZEL_LICENSE_TOKEN_PREFIX = 'KRTZL1.';
29013
+ /**
29014
+ * Raw 32-byte Ed25519 public key (base64url) used to verify license tokens.
29015
+ *
29016
+ * Generate the matching key pair with `npm run license:keygen` and paste the
29017
+ * printed public key here. Replacing this value invalidates every license
29018
+ * signed with the previous key.
29019
+ */
29020
+ const KRITZEL_LICENSE_PUBLIC_KEY = 'I-pYJlxDEjT94rwSaqhXW5Sv__fMKS-JKKnV3pg0PaM';
29021
+
29022
+ /** Minimum delay (ms) between periodic license re-validations. */
29023
+ const MIN_REVALIDATION_DELAY_MS = 30_000;
29024
+ /** Maximum delay (ms) between periodic license re-validations. */
29025
+ const MAX_REVALIDATION_DELAY_MS = 60_000;
29026
+ /**
29027
+ * Manages license validation state for the Kritzel editor.
29028
+ *
29029
+ * The manager is owned per {@link KritzelCore} instance (like the theme and
29030
+ * localization managers) so that multiple editors on the same page validate
29031
+ * independently.
29032
+ *
29033
+ * A license key is an offline Ed25519 signed token of the form
29034
+ * `KRTZL1.<base64url(payload)>.<base64url(signature)>`. The library verifies it
29035
+ * against the embedded {@link KRITZEL_LICENSE_PUBLIC_KEY}; only the owner can
29036
+ * mint tokens, with the matching private key (see scripts/license/).
29037
+ *
29038
+ * Responsibilities:
29039
+ * - Cheaply reject absent/malformed/expired keys synchronously so the watermark
29040
+ * never flashes off for an obviously invalid key.
29041
+ * - Verify the signature asynchronously via WebCrypto, then drive a rerender if
29042
+ * the licensed state changed so the watermark is re-asserted on the reactive
29043
+ * render path.
29044
+ * - Periodically re-check on a randomized interval so the check is harder to
29045
+ * stub out programmatically than a fixed `setInterval`. Re-checks reuse the
29046
+ * cached verified token and only re-evaluate expiry, so crypto is not re-run
29047
+ * on an unchanged key every tick.
29048
+ *
29049
+ * Failure semantics (fail open):
29050
+ * - No key provided -> unlicensed (free tier, watermark shown). The normal
29051
+ * default, not an error.
29052
+ * - Key present but cleanly invalid (malformed, bad signature, expired) ->
29053
+ * unlicensed.
29054
+ * - Key present but validation throws an *unexpected* internal error, or the
29055
+ * runtime lacks Ed25519 support in WebCrypto -> fail open and treat as
29056
+ * licensed so a library defect or old browser never breaks a paying
29057
+ * customer's app.
29058
+ */
29059
+ class KritzelLicenseManager {
29060
+ _core;
29061
+ /** The most recently provided license key, re-checked on each periodic tick. */
29062
+ _licenseKey;
29063
+ /** Cached result of the latest validation. */
29064
+ _isLicensed = false;
29065
+ /**
29066
+ * The token whose signature most recently verified successfully. Lets the
29067
+ * periodic re-check skip crypto and only re-evaluate expiry while the key is
29068
+ * unchanged.
29069
+ */
29070
+ _verifiedToken;
29071
+ /**
29072
+ * Monotonic id incremented on every {@link validate} call. An in-flight async
29073
+ * verification only commits its result if its id still matches, so a stale
29074
+ * verification resolving after a newer call can never clobber fresh state.
29075
+ */
29076
+ _validationId = 0;
29077
+ /** Cached imported public key, lazily created on first signature check. */
29078
+ _publicKeyPromise;
29079
+ /** Handle for the self-rescheduling re-validation timer. */
29080
+ _timer = null;
29081
+ /**
29082
+ * Creates a new KritzelLicenseManager instance.
29083
+ * @param core - The KritzelCore instance this manager belongs to
29084
+ */
29085
+ constructor(core) {
29086
+ this._core = core;
29087
+ }
29088
+ /**
29089
+ * Whether the editor is currently considered licensed (watermark hidden).
29090
+ */
29091
+ get isLicensed() {
29092
+ return this._isLicensed;
29093
+ }
29094
+ /**
29095
+ * Validates the given license key.
29096
+ *
29097
+ * Runs a synchronous structural pre-check first: an absent, malformed, or
29098
+ * already-expired key resolves the state immediately (no async work). A
29099
+ * structurally valid key then has its signature verified asynchronously; the
29100
+ * licensed state is committed (and a rerender triggered) only if it changed.
29101
+ *
29102
+ * The return value reflects the state known *synchronously* at call time. The
29103
+ * authoritative result for a structurally valid key arrives shortly after via
29104
+ * the async verification and the reactive rerender.
29105
+ *
29106
+ * @param key - The license key to validate, or undefined to clear the license
29107
+ * @returns The licensed state known synchronously at call time
29108
+ */
29109
+ validate(key) {
29110
+ this._licenseKey = key;
29111
+ const validationId = ++this._validationId;
29112
+ let pre;
29113
+ try {
29114
+ pre = this.preCheck(key);
29115
+ }
29116
+ catch {
29117
+ // Fail open: an unexpected internal error in the cheap pre-check must
29118
+ // never break a paying customer's app.
29119
+ this.commit(true, validationId);
29120
+ return this._isLicensed;
29121
+ }
29122
+ if (pre.status === 'rejected') {
29123
+ this._verifiedToken = undefined;
29124
+ this.commit(false, validationId);
29125
+ return this._isLicensed;
29126
+ }
29127
+ // Structurally valid and unexpired. If we already verified this exact token,
29128
+ // the signature is still good, so license it without re-running crypto.
29129
+ if (this._verifiedToken === key) {
29130
+ this.commit(true, validationId);
29131
+ return this._isLicensed;
29132
+ }
29133
+ // Otherwise verify the signature asynchronously. Leave the current state
29134
+ // untouched until it settles so a forged token cannot even briefly hide the
29135
+ // watermark (default state is unlicensed), and a valid renewal does not flap.
29136
+ this.verifySignature(pre.payloadSegment, pre.signatureSegment)
29137
+ .then(valid => {
29138
+ if (validationId !== this._validationId) {
29139
+ return; // A newer validate() superseded this one.
29140
+ }
29141
+ if (valid) {
29142
+ this._verifiedToken = key;
29143
+ this.commit(true, validationId);
29144
+ }
29145
+ else {
29146
+ this._verifiedToken = undefined;
29147
+ this.commit(false, validationId);
29148
+ }
29149
+ })
29150
+ .catch(() => {
29151
+ if (validationId !== this._validationId) {
29152
+ return;
29153
+ }
29154
+ // Fail open on unexpected crypto errors / missing Ed25519 support.
29155
+ this.commit(true, validationId);
29156
+ });
29157
+ return this._isLicensed;
29158
+ }
29159
+ /**
29160
+ * Starts periodic re-validation on a randomized interval between
29161
+ * {@link MIN_REVALIDATION_DELAY_MS} and {@link MAX_REVALIDATION_DELAY_MS}.
29162
+ *
29163
+ * Uses a self-rescheduling `setTimeout` (a fresh random delay each cycle)
29164
+ * rather than a fixed `setInterval`, making the check fractionally harder to
29165
+ * predict and no-op programmatically. Any existing timer is cleared first so
29166
+ * the method is idempotent.
29167
+ */
29168
+ startPeriodicValidation() {
29169
+ this.stopPeriodicValidation();
29170
+ this.scheduleNextValidation();
29171
+ }
29172
+ /**
29173
+ * Stops periodic re-validation and releases the timer.
29174
+ */
29175
+ stopPeriodicValidation() {
29176
+ if (this._timer !== null) {
29177
+ clearTimeout(this._timer);
29178
+ this._timer = null;
29179
+ }
29180
+ }
29181
+ /**
29182
+ * Cleanup hook called when the editor is torn down.
29183
+ */
29184
+ destroy() {
29185
+ this.stopPeriodicValidation();
29186
+ }
29187
+ /**
29188
+ * Commits a new licensed state, triggering a rerender only when it changed.
29189
+ * The {@link validationId} guard is checked by async callers; synchronous
29190
+ * callers pass the current id.
29191
+ */
29192
+ commit(nextLicensed, validationId) {
29193
+ if (validationId !== this._validationId) {
29194
+ return;
29195
+ }
29196
+ if (nextLicensed !== this._isLicensed) {
29197
+ this._isLicensed = nextLicensed;
29198
+ this._core.rerender();
29199
+ }
29200
+ }
29201
+ /**
29202
+ * Schedules the next re-validation tick at a randomized delay.
29203
+ */
29204
+ scheduleNextValidation() {
29205
+ const delay = this.getRandomRevalidationDelay();
29206
+ this._timer = setTimeout(() => {
29207
+ this.validate(this._licenseKey);
29208
+ this.scheduleNextValidation();
29209
+ }, delay);
29210
+ }
29211
+ /**
29212
+ * Returns a random delay within the configured re-validation window.
29213
+ */
29214
+ getRandomRevalidationDelay() {
29215
+ const span = MAX_REVALIDATION_DELAY_MS - MIN_REVALIDATION_DELAY_MS;
29216
+ return MIN_REVALIDATION_DELAY_MS + Math.floor(Math.random() * (span + 1));
29217
+ }
29218
+ /**
29219
+ * Synchronous structural pre-check of a license key: verifies the format
29220
+ * prefix, three-part structure, JSON payload, and (if present) that the token
29221
+ * has not expired. Does not verify the signature.
29222
+ *
29223
+ * @returns `rejected` for absent/malformed/expired keys, or `pass` with the
29224
+ * parsed segments and claims for a structurally valid, unexpired token.
29225
+ */
29226
+ preCheck(key) {
29227
+ if (key === undefined || key === null) {
29228
+ return { status: 'rejected' };
29229
+ }
29230
+ const token = key.trim();
29231
+ if (token.length === 0 || !token.startsWith(KRITZEL_LICENSE_TOKEN_PREFIX)) {
29232
+ return { status: 'rejected' };
29233
+ }
29234
+ const body = token.slice(KRITZEL_LICENSE_TOKEN_PREFIX.length);
29235
+ const parts = body.split('.');
29236
+ if (parts.length !== 2 || parts[0].length === 0 || parts[1].length === 0) {
29237
+ return { status: 'rejected' };
29238
+ }
29239
+ const [payloadSegment, signatureSegment] = parts;
29240
+ let claims;
29241
+ try {
29242
+ claims = JSON.parse(this.base64UrlToString(payloadSegment));
29243
+ }
29244
+ catch {
29245
+ return { status: 'rejected' };
29246
+ }
29247
+ if (claims === null || typeof claims !== 'object') {
29248
+ return { status: 'rejected' };
29249
+ }
29250
+ if (typeof claims.exp === 'number' && claims.exp * 1000 <= Date.now()) {
29251
+ return { status: 'rejected' }; // Cleanly expired.
29252
+ }
29253
+ return { status: 'pass', payloadSegment, signatureSegment, claims };
29254
+ }
29255
+ /**
29256
+ * Verifies the Ed25519 signature of the token against the embedded public
29257
+ * key. Resolves true/false for a definitive valid/invalid signature.
29258
+ *
29259
+ * Rejects (rather than resolving false) on unexpected crypto failures or when
29260
+ * the runtime lacks Ed25519 support, so the caller fails open.
29261
+ */
29262
+ async verifySignature(payloadSegment, signatureSegment) {
29263
+ const publicKey = await this.getPublicKey();
29264
+ const data = this.utf8Bytes(payloadSegment);
29265
+ const signature = this.base64UrlToBytes(signatureSegment);
29266
+ return crypto.subtle.verify('Ed25519', publicKey, signature, data);
29267
+ }
29268
+ /**
29269
+ * Imports and caches the embedded Ed25519 public key for signature checks.
29270
+ */
29271
+ getPublicKey() {
29272
+ if (!this._publicKeyPromise) {
29273
+ const raw = this.base64UrlToBytes(KRITZEL_LICENSE_PUBLIC_KEY);
29274
+ this._publicKeyPromise = Promise.resolve().then(() => crypto.subtle.importKey('raw', raw, { name: 'Ed25519' }, false, ['verify']));
29275
+ // If import fails (e.g. no Ed25519 support), drop the cache so a later
29276
+ // tick can retry, and let the rejection propagate to fail open.
29277
+ this._publicKeyPromise.catch(() => {
29278
+ this._publicKeyPromise = undefined;
29279
+ });
29280
+ }
29281
+ return this._publicKeyPromise;
29282
+ }
29283
+ /** Decodes a base64url string to its UTF-8 text. */
29284
+ base64UrlToString(value) {
29285
+ return new TextDecoder().decode(this.base64UrlToBytes(value));
29286
+ }
29287
+ /** Decodes a base64url string to raw bytes. */
29288
+ base64UrlToBytes(value) {
29289
+ const base64 = value.replace(/-/g, '+').replace(/_/g, '/');
29290
+ const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');
29291
+ const binary = atob(padded);
29292
+ const bytes = new Uint8Array(new ArrayBuffer(binary.length));
29293
+ for (let i = 0; i < binary.length; i++) {
29294
+ bytes[i] = binary.charCodeAt(i);
29295
+ }
29296
+ return bytes;
29297
+ }
29298
+ /** Encodes a string to its UTF-8 bytes. */
29299
+ utf8Bytes(value) {
29300
+ const encoded = new TextEncoder().encode(value);
29301
+ const bytes = new Uint8Array(new ArrayBuffer(encoded.length));
29302
+ bytes.set(encoded);
29303
+ return bytes;
29304
+ }
29305
+ }
29306
+
28316
29307
  const DEFAULT_BRUSH_CONFIG = {
28317
29308
  type: 'pen',
28318
29309
  color: DEFAULT_COLOR_PALETTE[0],
@@ -28495,6 +29486,9 @@ exports.DEFAULT_COLOR_PALETTE = DEFAULT_COLOR_PALETTE;
28495
29486
  exports.DEFAULT_LINE_TOOL_CONFIG = DEFAULT_LINE_TOOL_CONFIG;
28496
29487
  exports.DEFAULT_STROKE_SIZES = DEFAULT_STROKE_SIZES;
28497
29488
  exports.DEFAULT_TEXT_CONFIG = DEFAULT_TEXT_CONFIG;
29489
+ exports.DE_LOCALE = DE_LOCALE;
29490
+ exports.EN_LOCALE = EN_LOCALE;
29491
+ exports.FR_LOCALE = FR_LOCALE;
28498
29492
  exports.HocuspocusProvider = HocuspocusProvider;
28499
29493
  exports.HocuspocusProviderWebsocket = HocuspocusProviderWebsocket;
28500
29494
  exports.IndexedDBAssetProvider = IndexedDBAssetProvider;
@@ -28502,6 +29496,7 @@ exports.KritzelAnchorManager = KritzelAnchorManager;
28502
29496
  exports.KritzelAssetResolver = KritzelAssetResolver;
28503
29497
  exports.KritzelBaseHandler = KritzelBaseHandler;
28504
29498
  exports.KritzelBaseObject = KritzelBaseObject;
29499
+ exports.KritzelBaseTool = KritzelBaseTool;
28505
29500
  exports.KritzelBrushTool = KritzelBrushTool;
28506
29501
  exports.KritzelClassHelper = KritzelClassHelper;
28507
29502
  exports.KritzelColorHelper = KritzelColorHelper;
@@ -28514,8 +29509,11 @@ exports.KritzelIconRegistry = KritzelIconRegistry;
28514
29509
  exports.KritzelImage = KritzelImage;
28515
29510
  exports.KritzelImageTool = KritzelImageTool;
28516
29511
  exports.KritzelKeyboardHelper = KritzelKeyboardHelper;
29512
+ exports.KritzelLicenseManager = KritzelLicenseManager;
28517
29513
  exports.KritzelLine = KritzelLine;
28518
29514
  exports.KritzelLineTool = KritzelLineTool;
29515
+ exports.KritzelLocalizationManager = KritzelLocalizationManager;
29516
+ exports.KritzelMathHelper = KritzelMathHelper;
28519
29517
  exports.KritzelPath = KritzelPath;
28520
29518
  exports.KritzelSelectionBox = KritzelSelectionBox;
28521
29519
  exports.KritzelSelectionGroup = KritzelSelectionGroup;