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
@@ -840,8 +840,11 @@ function findDiffStart(a, b, pos) {
840
840
  if (!childA.sameMarkup(childB))
841
841
  return pos;
842
842
  if (childA.isText && childA.text != childB.text) {
843
- for (let j = 0; childA.text[j] == childB.text[j]; j++)
843
+ let tA = childA.text, tB = childB.text, j = 0;
844
+ for (; tA[j] == tB[j]; j++)
844
845
  pos++;
846
+ if (j && j < tA.length && j < tB.length && surrogateHigh(tA.charCodeAt(j - 1)) && surrogateLow(tA.charCodeAt(j)))
847
+ pos--;
845
848
  return pos;
846
849
  }
847
850
  if (childA.content.size || childB.content.size) {
@@ -865,12 +868,17 @@ function findDiffEnd(a, b, posA, posB) {
865
868
  if (!childA.sameMarkup(childB))
866
869
  return { a: posA, b: posB };
867
870
  if (childA.isText && childA.text != childB.text) {
868
- let same = 0, minSize = Math.min(childA.text.length, childB.text.length);
869
- while (same < minSize && childA.text[childA.text.length - same - 1] == childB.text[childB.text.length - same - 1]) {
870
- same++;
871
+ let tA = childA.text, tB = childB.text, iA = tA.length, iB = tB.length;
872
+ while (iA > 0 && iB > 0 && tA[iA - 1] == tB[iB - 1]) {
873
+ iA--;
874
+ iB--;
871
875
  posA--;
872
876
  posB--;
873
877
  }
878
+ if (iA && iB && iA < tA.length && surrogateHigh(tA.charCodeAt(iA - 1)) && surrogateLow(tA.charCodeAt(iA))) {
879
+ posA++;
880
+ posB++;
881
+ }
874
882
  return { a: posA, b: posB };
875
883
  }
876
884
  if (childA.content.size || childB.content.size) {
@@ -882,6 +890,8 @@ function findDiffEnd(a, b, posA, posB) {
882
890
  posB -= size;
883
891
  }
884
892
  }
893
+ function surrogateLow(ch) { return ch >= 0xDC00 && ch < 0xE000; }
894
+ function surrogateHigh(ch) { return ch >= 0xD800 && ch < 0xDC00; }
885
895
 
886
896
  /**
887
897
  A fragment represents a node's collection of child nodes.
@@ -1562,7 +1572,8 @@ function addRange($start, $end, depth, target) {
1562
1572
  addNode($end.nodeBefore, target);
1563
1573
  }
1564
1574
  function close(node, content) {
1565
- node.type.checkContent(content);
1575
+ if (!node.type.validContent(content))
1576
+ throw new ReplaceError("Invalid content for node " + node.type.name);
1566
1577
  return node.copy(content);
1567
1578
  }
1568
1579
  function replaceThreeWay($from, $start, $end, $to, depth) {
@@ -2873,13 +2884,12 @@ function computeAttrs(attrs, value) {
2873
2884
  return built;
2874
2885
  }
2875
2886
  function checkAttrs(attrs, values, type, name) {
2876
- for (let name in values)
2877
- if (!(name in attrs))
2878
- throw new RangeError(`Unsupported attribute ${name} for ${type} of type ${name}`);
2879
- for (let name in attrs) {
2880
- let attr = attrs[name];
2881
- if (attr.validate)
2882
- attr.validate(values[name]);
2887
+ for (let attr in values)
2888
+ if (!(attr in attrs))
2889
+ throw new RangeError(`Unsupported attribute ${attr} for ${type} of type ${name}`);
2890
+ for (let attr in attrs) {
2891
+ if (attrs[attr].validate)
2892
+ attrs[attr].validate(values[attr]);
2883
2893
  }
2884
2894
  }
2885
2895
  function initAttrs(typeName, attrs) {
@@ -3056,7 +3066,7 @@ let NodeType$1 = class NodeType {
3056
3066
  @internal
3057
3067
  */
3058
3068
  checkAttrs(attrs) {
3059
- checkAttrs(this.attrs, attrs, "node");
3069
+ checkAttrs(this.attrs, attrs, "node", this.name);
3060
3070
  }
3061
3071
  /**
3062
3072
  Check whether the given mark type is allowed in this node.
@@ -3207,7 +3217,7 @@ class MarkType {
3207
3217
  @internal
3208
3218
  */
3209
3219
  checkAttrs(attrs) {
3210
- checkAttrs(this.attrs, attrs, "mark");
3220
+ checkAttrs(this.attrs, attrs, "mark", this.name);
3211
3221
  }
3212
3222
  /**
3213
3223
  Queries whether a given mark type is
@@ -9642,14 +9652,14 @@ function selectionToDOM(view, force = false) {
9642
9652
  syncNodeSelection(view, sel);
9643
9653
  if (!editorOwnsSelection(view))
9644
9654
  return;
9645
- // The delayed drag selection causes issues with Cell Selections
9646
- // in Safari. And the drag selection delay is to workarond issues
9647
- // which only present in Chrome.
9648
- if (!force && view.input.mouseDown && view.input.mouseDown.allowDefault && chrome) {
9655
+ // Need to delay selection normalization during a native selection
9656
+ // drag on Chrome, or it will cause further dragging to glitch.
9657
+ let mouseDown = view.input.mouseDown;
9658
+ if (!force && chrome && mouseDown) {
9649
9659
  let domSel = view.domSelectionRange(), curSel = view.domObserver.currentSelection;
9650
9660
  if (domSel.anchorNode && curSel.anchorNode &&
9651
- isEquivalentPosition(domSel.anchorNode, domSel.anchorOffset, curSel.anchorNode, curSel.anchorOffset)) {
9652
- view.input.mouseDown.delayedSelectionSync = true;
9661
+ isEquivalentPosition(domSel.anchorNode, domSel.anchorOffset, curSel.anchorNode, curSel.anchorOffset) &&
9662
+ mouseDown.delaySelUpdate()) {
9653
9663
  view.domObserver.setCurSelection();
9654
9664
  return;
9655
9665
  }
@@ -10501,6 +10511,8 @@ function setSelectionOrigin(view, origin) {
10501
10511
  view.input.lastSelectionTime = Date.now();
10502
10512
  }
10503
10513
  function destroyInput(view) {
10514
+ if (view.input.mouseDown)
10515
+ view.input.mouseDown.done();
10504
10516
  view.domObserver.stop();
10505
10517
  for (let type in view.input.eventHandlers)
10506
10518
  view.dom.removeEventListener(type, view.input.eventHandlers[type]);
@@ -10539,7 +10551,7 @@ function dispatchEvent(view, event) {
10539
10551
  editHandlers.keydown = (view, _event) => {
10540
10552
  let event = _event;
10541
10553
  view.input.shiftKey = event.keyCode == 16 || event.shiftKey;
10542
- if (inOrNearComposition(view, event))
10554
+ if (inOrNearComposition(view))
10543
10555
  return;
10544
10556
  view.input.lastKeyCode = event.keyCode;
10545
10557
  view.input.lastKeyCodeTime = Date.now();
@@ -10577,7 +10589,7 @@ editHandlers.keyup = (view, event) => {
10577
10589
  };
10578
10590
  editHandlers.keypress = (view, _event) => {
10579
10591
  let event = _event;
10580
- if (inOrNearComposition(view, event) || !event.charCode ||
10592
+ if (inOrNearComposition(view) || !event.charCode ||
10581
10593
  event.ctrlKey && !event.altKey || mac$3 && event.metaKey)
10582
10594
  return;
10583
10595
  if (view.someProp("handleKeyPress", f => f(view, event))) {
@@ -10671,26 +10683,28 @@ function handleTripleClick(view, pos, inside, event) {
10671
10683
  function defaultTripleClick(view, inside, event) {
10672
10684
  if (event.button != 0)
10673
10685
  return false;
10674
- let doc = view.state.doc;
10675
- if (inside == -1) {
10676
- if (doc.inlineContent) {
10677
- updateSelection(view, TextSelection.create(doc, 0, doc.content.size));
10678
- return true;
10679
- }
10686
+ let selection = selectionForTripleClick(view, inside, true), doc = view.state.doc;
10687
+ if (!selection)
10680
10688
  return false;
10681
- }
10689
+ updateSelection(view, selection);
10690
+ if (selection instanceof TextSelection && doc.eq(view.state.doc))
10691
+ view.input.mouseDown = new TripleClickDrag(view, selection);
10692
+ return true;
10693
+ }
10694
+ function selectionForTripleClick(view, inside, selectNodes) {
10695
+ let doc = view.state.doc;
10696
+ if (inside == -1)
10697
+ return doc.inlineContent ? TextSelection.create(doc, 0, doc.content.size) : null;
10682
10698
  let $pos = doc.resolve(inside);
10683
10699
  for (let i = $pos.depth + 1; i > 0; i--) {
10684
10700
  let node = i > $pos.depth ? $pos.nodeAfter : $pos.node(i);
10685
10701
  let nodePos = $pos.before(i);
10686
10702
  if (node.inlineContent)
10687
- updateSelection(view, TextSelection.create(doc, nodePos + 1, nodePos + 1 + node.content.size));
10688
- else if (NodeSelection.isSelectable(node))
10689
- updateSelection(view, NodeSelection.create(doc, nodePos));
10690
- else
10691
- continue;
10692
- return true;
10703
+ return TextSelection.create(doc, nodePos + 1, nodePos + 1 + node.content.size);
10704
+ else if (selectNodes && NodeSelection.isSelectable(node))
10705
+ return NodeSelection.create(doc, nodePos);
10693
10706
  }
10707
+ return null;
10694
10708
  }
10695
10709
  function forceDOMFlush(view) {
10696
10710
  return endComposition(view);
@@ -10709,13 +10723,13 @@ handlers.mousedown = (view, _event) => {
10709
10723
  type = "tripleClick";
10710
10724
  }
10711
10725
  view.input.lastClick = { time: now, x: event.clientX, y: event.clientY, type, button: event.button };
10726
+ if (view.input.mouseDown)
10727
+ view.input.mouseDown.done();
10712
10728
  let pos = view.posAtCoords(eventCoords(event));
10713
10729
  if (!pos)
10714
10730
  return;
10715
10731
  if (type == "singleClick") {
10716
- if (view.input.mouseDown)
10717
- view.input.mouseDown.done();
10718
- view.input.mouseDown = new MouseDown(view, pos, event, !!flushed);
10732
+ view.input.mouseDown = new LeftMouseDown(view, pos, event, !!flushed);
10719
10733
  }
10720
10734
  else if ((type == "doubleClick" ? handleDoubleClick : handleTripleClick)(view, pos.pos, pos.inside, event)) {
10721
10735
  event.preventDefault();
@@ -10725,13 +10739,34 @@ handlers.mousedown = (view, _event) => {
10725
10739
  }
10726
10740
  };
10727
10741
  class MouseDown {
10728
- constructor(view, pos, event, flushed) {
10742
+ constructor(view) {
10729
10743
  this.view = view;
10744
+ this.mightDrag = null;
10745
+ view.root.addEventListener("mouseup", this.up = this.up.bind(this));
10746
+ view.root.addEventListener("mousemove", this.move = this.move.bind(this));
10747
+ }
10748
+ up(event) {
10749
+ this.done();
10750
+ }
10751
+ move(event) {
10752
+ if (event.buttons == 0)
10753
+ this.done();
10754
+ }
10755
+ done() {
10756
+ this.view.root.removeEventListener("mouseup", this.up);
10757
+ this.view.root.removeEventListener("mousemove", this.move);
10758
+ if (this.view.input.mouseDown == this)
10759
+ this.view.input.mouseDown = null;
10760
+ }
10761
+ delaySelUpdate() { return false; }
10762
+ }
10763
+ class LeftMouseDown extends MouseDown {
10764
+ constructor(view, pos, event, flushed) {
10765
+ super(view);
10730
10766
  this.pos = pos;
10731
10767
  this.event = event;
10732
10768
  this.flushed = flushed;
10733
10769
  this.delayedSelectionSync = false;
10734
- this.mightDrag = null;
10735
10770
  this.startDoc = view.state.doc;
10736
10771
  this.selectNode = !!event[selectNodeModifier];
10737
10772
  this.allowDefault = event.shiftKey;
@@ -10769,13 +10804,10 @@ class MouseDown {
10769
10804
  }, 20);
10770
10805
  this.view.domObserver.start();
10771
10806
  }
10772
- view.root.addEventListener("mouseup", this.up = this.up.bind(this));
10773
- view.root.addEventListener("mousemove", this.move = this.move.bind(this));
10774
10807
  setSelectionOrigin(view, "pointer");
10775
10808
  }
10776
10809
  done() {
10777
- this.view.root.removeEventListener("mouseup", this.up);
10778
- this.view.root.removeEventListener("mousemove", this.move);
10810
+ super.done();
10779
10811
  if (this.mightDrag && this.target) {
10780
10812
  this.view.domObserver.stop();
10781
10813
  if (this.mightDrag.addAttr)
@@ -10785,8 +10817,10 @@ class MouseDown {
10785
10817
  this.view.domObserver.start();
10786
10818
  }
10787
10819
  if (this.delayedSelectionSync)
10788
- setTimeout(() => selectionToDOM(this.view));
10789
- this.view.input.mouseDown = null;
10820
+ setTimeout(() => {
10821
+ if (!this.view.isDestroyed)
10822
+ selectionToDOM(this.view);
10823
+ });
10790
10824
  }
10791
10825
  up(event) {
10792
10826
  this.done();
@@ -10825,14 +10859,41 @@ class MouseDown {
10825
10859
  move(event) {
10826
10860
  this.updateAllowDefault(event);
10827
10861
  setSelectionOrigin(this.view, "pointer");
10828
- if (event.buttons == 0)
10829
- this.done();
10862
+ super.move(event);
10830
10863
  }
10831
10864
  updateAllowDefault(event) {
10832
10865
  if (!this.allowDefault && (Math.abs(this.event.x - event.clientX) > 4 ||
10833
10866
  Math.abs(this.event.y - event.clientY) > 4))
10834
10867
  this.allowDefault = true;
10835
10868
  }
10869
+ delaySelUpdate() {
10870
+ if (!this.allowDefault)
10871
+ return false;
10872
+ this.delayedSelectionSync = true;
10873
+ return true;
10874
+ }
10875
+ }
10876
+ class TripleClickDrag extends MouseDown {
10877
+ constructor(view, startSelection) {
10878
+ super(view);
10879
+ this.startSelection = startSelection;
10880
+ this.startDoc = view.state.doc;
10881
+ }
10882
+ move(event) {
10883
+ if (event.buttons == 0 || this.view.isDestroyed || !this.view.state.doc.eq(this.startDoc)) {
10884
+ this.done();
10885
+ return;
10886
+ }
10887
+ event.preventDefault();
10888
+ setSelectionOrigin(this.view, "pointer");
10889
+ let pos = this.view.posAtCoords(eventCoords(event));
10890
+ let target = pos && selectionForTripleClick(this.view, pos.inside, false);
10891
+ if (!target)
10892
+ return;
10893
+ let { doc } = this.view.state, start = this.startSelection;
10894
+ let [anchor, head] = target.from < start.from ? [start.to, target.from] : [start.from, target.to];
10895
+ updateSelection(this.view, TextSelection.create(doc, anchor, head));
10896
+ }
10836
10897
  }
10837
10898
  handlers.touchstart = view => {
10838
10899
  view.input.lastTouch = Date.now();
@@ -10857,7 +10918,7 @@ function inOrNearComposition(view, event) {
10857
10918
  // This guards against the case where compositionend is triggered without the keyboard
10858
10919
  // (e.g. character confirmation may be done with the mouse), and keydown is triggered
10859
10920
  // afterwards- we wouldn't want to ignore the keydown event in this case.
10860
- if (safari && Math.abs(event.timeStamp - view.input.compositionEndedAt) < 500) {
10921
+ if (safari && Math.abs(Date.now() - view.input.compositionEndedAt) < 500) {
10861
10922
  view.input.compositionEndedAt = -2e8;
10862
10923
  return true;
10863
10924
  }
@@ -10916,7 +10977,7 @@ function selectionBeforeUneditable(view) {
10916
10977
  editHandlers.compositionend = (view, event) => {
10917
10978
  if (view.composing) {
10918
10979
  view.input.composing = false;
10919
- view.input.compositionEndedAt = event.timeStamp;
10980
+ view.input.compositionEndedAt = Date.now();
10920
10981
  view.input.compositionPendingChanges = view.domObserver.pendingRecords().length ? view.input.compositionID : 0;
10921
10982
  view.input.compositionNode = null;
10922
10983
  if (view.input.badSafariComposition)
@@ -10935,7 +10996,7 @@ function scheduleComposeEnd(view, delay) {
10935
10996
  function clearComposition(view) {
10936
10997
  if (view.composing) {
10937
10998
  view.input.composing = false;
10938
- view.input.compositionEndedAt = timestampFromCustomEvent();
10999
+ view.input.compositionEndedAt = Date.now();
10939
11000
  }
10940
11001
  while (view.input.compositionNodes.length > 0)
10941
11002
  view.input.compositionNodes.pop().markParentsDirty();
@@ -10961,11 +11022,6 @@ function findCompositionNode(view) {
10961
11022
  }
10962
11023
  return textBefore || textAfter;
10963
11024
  }
10964
- function timestampFromCustomEvent() {
10965
- let event = document.createEvent("Event");
10966
- event.initEvent("event", true, true);
10967
- return event.timeStamp;
10968
- }
10969
11025
  /**
10970
11026
  @internal
10971
11027
  */
@@ -12122,7 +12178,10 @@ class DOMObserver {
12122
12178
  }
12123
12179
  }
12124
12180
  }
12125
- if (added.some(n => n.nodeName == "BR") && (view.input.lastKeyCode == 8 || view.input.lastKeyCode == 46)) {
12181
+ if (added.some(n => n.nodeName == "BR") &&
12182
+ (view.input.lastKeyCode == 8 || view.input.lastKeyCode == 46 ||
12183
+ chrome && (view.composing || view.input.compositionEndedAt > Date.now() - 50) &&
12184
+ mutations.some(m => m.type == "childList" && m.removedNodes.length))) {
12126
12185
  // Browsers sometimes insert a bogus break node if you
12127
12186
  // backspace out the last bit of text before an inline-flex node (#1552)
12128
12187
  for (let node of added)
@@ -12678,38 +12737,28 @@ function skipClosingAndOpening($pos, fromEnd, mayOpen) {
12678
12737
  return end;
12679
12738
  }
12680
12739
  function findDiff(a, b, pos, preferredPos, preferredSide) {
12681
- let start = a.findDiffStart(b, pos);
12740
+ let start = a.findDiffStart(b, pos), lenA = pos + a.size, lenB = pos + b.size;
12682
12741
  if (start == null)
12683
12742
  return null;
12684
- let { a: endA, b: endB } = a.findDiffEnd(b, pos + a.size, pos + b.size);
12743
+ let { a: endA, b: endB } = a.findDiffEnd(b, lenA, lenB);
12685
12744
  if (preferredSide == "end") {
12686
12745
  let adjust = Math.max(0, start - Math.min(endA, endB));
12687
12746
  preferredPos -= endA + adjust - start;
12688
12747
  }
12689
- if (endA < start && a.size < b.size) {
12748
+ if (endA < start && lenA < lenB) {
12690
12749
  let move = preferredPos <= start && preferredPos >= endA ? start - preferredPos : 0;
12691
12750
  start -= move;
12692
- if (start && start < b.size && isSurrogatePair(b.textBetween(start - 1, start + 1)))
12693
- start += move ? 1 : -1;
12694
12751
  endB = start + (endB - endA);
12695
12752
  endA = start;
12696
12753
  }
12697
12754
  else if (endB < start) {
12698
12755
  let move = preferredPos <= start && preferredPos >= endB ? start - preferredPos : 0;
12699
12756
  start -= move;
12700
- if (start && start < a.size && isSurrogatePair(a.textBetween(start - 1, start + 1)))
12701
- start += move ? 1 : -1;
12702
12757
  endA = start + (endA - endB);
12703
12758
  endB = start;
12704
12759
  }
12705
12760
  return { start, endA, endB };
12706
12761
  }
12707
- function isSurrogatePair(str) {
12708
- if (str.length != 2)
12709
- return false;
12710
- let a = str.charCodeAt(0), b = str.charCodeAt(1);
12711
- return a >= 0xDC00 && a <= 0xDFFF && b >= 0xD800 && b <= 0xDBFF;
12712
- }
12713
12762
  /**
12714
12763
  An editor view manages the DOM structure that represents an
12715
12764
  editable document. Its state and behavior are determined by its
@@ -12901,9 +12950,10 @@ class EditorView {
12901
12950
  // a DOM selection change and the "selectionchange" event for it
12902
12951
  // can cause a spurious DOM selection update, disrupting mouse
12903
12952
  // drag selection.
12953
+ let mouseDown = this.input.mouseDown;
12904
12954
  if (forceSelUpdate ||
12905
- !(this.input.mouseDown && this.domObserver.currentSelection.eq(this.domSelectionRange()) &&
12906
- anchorInRightPlace(this))) {
12955
+ !(mouseDown && this.domObserver.currentSelection.eq(this.domSelectionRange()) &&
12956
+ anchorInRightPlace(this) && mouseDown.delaySelUpdate())) {
12907
12957
  selectionToDOM(this, forceSelUpdate);
12908
12958
  }
12909
12959
  else {
@@ -14600,6 +14650,24 @@ const lightTheme = {
14600
14650
  buttonHoverBackgroundColor: 'hsl(0, 0%, 0%, 4.3%)',
14601
14651
  separatorColor: 'hsl(0, 0%, 0%, 8%)',
14602
14652
  },
14653
+ watermark: {
14654
+ background: 'rgba(255, 255, 255, 0.6)',
14655
+ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.12)',
14656
+ color: 'rgba(60, 60, 60, 0.75)',
14657
+ },
14658
+ zoomPanel: {
14659
+ backgroundColor: '#ffffff',
14660
+ border: '1px solid #ebebeb',
14661
+ borderRadius: '12px',
14662
+ boxShadow: '0 0 3px rgba(0, 0, 0, 0.08)',
14663
+ buttonActiveBackgroundColor: 'hsl(0, 0%, 0%, 8.6%)',
14664
+ buttonBorderRadius: '8px',
14665
+ buttonHoverBackgroundColor: 'hsl(0, 0%, 0%, 4.3%)',
14666
+ buttonSize: '32px',
14667
+ gap: '4px',
14668
+ iconColor: '#000000',
14669
+ padding: '4px',
14670
+ },
14603
14671
  };
14604
14672
 
14605
14673
  /**
@@ -14877,6 +14945,24 @@ const darkTheme = {
14877
14945
  buttonHoverBackgroundColor: 'hsl(0, 0%, 100%, 8%)',
14878
14946
  separatorColor: 'hsl(0, 0%, 100%, 12%)',
14879
14947
  },
14948
+ watermark: {
14949
+ background: 'rgba(40, 40, 40, 0.6)',
14950
+ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.35)',
14951
+ color: 'rgba(220, 220, 220, 0.8)',
14952
+ },
14953
+ zoomPanel: {
14954
+ backgroundColor: '#2a2a2a',
14955
+ border: '1px solid #3a3a3a',
14956
+ borderRadius: '12px',
14957
+ boxShadow: '0 0 3px rgba(0, 0, 0, 0.4)',
14958
+ buttonActiveBackgroundColor: 'hsl(0, 0%, 100%, 12%)',
14959
+ buttonBorderRadius: '8px',
14960
+ buttonHoverBackgroundColor: 'hsl(0, 0%, 100%, 8%)',
14961
+ buttonSize: '32px',
14962
+ gap: '4px',
14963
+ iconColor: '#e0e0e0',
14964
+ padding: '4px',
14965
+ },
14880
14966
  };
14881
14967
 
14882
14968
  class ThemeHelper {
@@ -14950,7 +15036,7 @@ class ThemeHelper {
14950
15036
  }
14951
15037
 
14952
15038
  /** Key used to store the settings object in localStorage. */
14953
- const SETTINGS_STORAGE_KEY = 'kritzel-settings';
15039
+ const SETTINGS_STORAGE_KEY$1 = 'kritzel-settings';
14954
15040
  /** Default theme used when no stored preference exists. */
14955
15041
  const DEFAULT_THEME = 'light';
14956
15042
  /**
@@ -14977,7 +15063,7 @@ class KritzelThemeManager {
14977
15063
  */
14978
15064
  constructor(core) {
14979
15065
  this._core = core;
14980
- this._storageKey = core.editorId ? `${SETTINGS_STORAGE_KEY}-${core.editorId}` : SETTINGS_STORAGE_KEY;
15066
+ this._storageKey = core.editorId ? `${SETTINGS_STORAGE_KEY$1}-${core.editorId}` : SETTINGS_STORAGE_KEY$1;
14981
15067
  this._themeRegistry.set('light', lightTheme);
14982
15068
  this._themeRegistry.set('dark', darkTheme);
14983
15069
  this._currentTheme = this.getStoredTheme();
@@ -15093,7 +15179,7 @@ class KritzelThemeManager {
15093
15179
  if (typeof localStorage === 'undefined') {
15094
15180
  return DEFAULT_THEME;
15095
15181
  }
15096
- const stored = localStorage.getItem(SETTINGS_STORAGE_KEY);
15182
+ const stored = localStorage.getItem(SETTINGS_STORAGE_KEY$1);
15097
15183
  if (!stored) {
15098
15184
  return DEFAULT_THEME;
15099
15185
  }
@@ -15257,6 +15343,15 @@ class KritzelColorHelper {
15257
15343
  }
15258
15344
  }
15259
15345
 
15346
+ class KritzelMathHelper {
15347
+ static average(a, b) {
15348
+ return (a + b) / 2;
15349
+ }
15350
+ static degreesToRadians(degrees) {
15351
+ return degrees * (Math.PI / 180);
15352
+ }
15353
+ }
15354
+
15260
15355
  /**
15261
15356
  * Represents a text object on the canvas that supports rich text editing via ProseMirror.
15262
15357
  * Extends the base object class to inherit common object properties and behaviors.
@@ -15321,6 +15416,7 @@ class KritzelText extends KritzelBaseObject {
15321
15416
  */
15322
15417
  constructor(config) {
15323
15418
  super();
15419
+ this.rotation = KritzelMathHelper.degreesToRadians(config?.rotation ?? 0);
15324
15420
  // Always create the editor so setContent() works immediately
15325
15421
  this.editor = this.createEditor();
15326
15422
  if (config) {
@@ -15359,7 +15455,7 @@ class KritzelText extends KritzelBaseObject {
15359
15455
  * @param scale - Optional scale factor (defaults to current viewport scale).
15360
15456
  * @returns A new, fully initialized KritzelText instance.
15361
15457
  */
15362
- static create(core, fontSize, fontFamily, scale) {
15458
+ static create(core, fontSize, fontFamily, scale, rotation) {
15363
15459
  const object = new KritzelText();
15364
15460
  object._core = core;
15365
15461
  object.id = object.generateId();
@@ -15369,6 +15465,7 @@ class KritzelText extends KritzelBaseObject {
15369
15465
  object.fontFamily = fontFamily || 'Arial';
15370
15466
  object.translateX = 0;
15371
15467
  object.translateY = 0;
15468
+ object.rotation = KritzelMathHelper.degreesToRadians(rotation ?? 0);
15372
15469
  const coreScale = core.store.state.scale;
15373
15470
  const effectiveScale = coreScale < 0 ? coreScale : 1;
15374
15471
  object.width = object.initialWidth / effectiveScale;
@@ -15720,12 +15817,6 @@ function requireCjs () {
15720
15817
 
15721
15818
  var cjsExports = requireCjs();
15722
15819
 
15723
- class KritzelMathHelper {
15724
- static average(a, b) {
15725
- return (a + b) / 2;
15726
- }
15727
- }
15728
-
15729
15820
  class KritzelPath extends KritzelBaseObject {
15730
15821
  __class__ = 'KritzelPath';
15731
15822
  points;
@@ -15768,6 +15859,7 @@ class KritzelPath extends KritzelBaseObject {
15768
15859
  this.points = config?.points ?? [];
15769
15860
  this.translateX = config?.translateX ?? 0;
15770
15861
  this.translateY = config?.translateY ?? 0;
15862
+ this.rotation = KritzelMathHelper.degreesToRadians(config?.rotation ?? 0);
15771
15863
  this.scale = config?.scale ?? 1;
15772
15864
  this.strokeWidth = config?.strokeWidth ?? 8;
15773
15865
  this.fill = config?.fill ?? { light: '#000000', dark: '#ffffff' };
@@ -15791,6 +15883,7 @@ class KritzelPath extends KritzelBaseObject {
15791
15883
  object.points = options?.points ?? [];
15792
15884
  object.translateX = options?.translateX ?? 0;
15793
15885
  object.translateY = options?.translateY ?? 0;
15886
+ object.rotation = KritzelMathHelper.degreesToRadians(options?.rotation ?? 0);
15794
15887
  object.scale = options?.scale ?? 1;
15795
15888
  object.strokeWidth = options?.strokeWidth ?? 8;
15796
15889
  object.fill = options?.fill ?? { light: '#000000', dark: '#ffffff' };
@@ -15840,8 +15933,12 @@ class KritzelPath extends KritzelBaseObject {
15840
15933
  this.height = Math.max(...this.points.map(p => p[1])) - Math.min(...this.points.map(p => p[1])) + this.strokeWidth;
15841
15934
  this.x = Math.min(...this.points.map(p => p[0])) - this.strokeWidth / 2;
15842
15935
  this.y = Math.min(...this.points.map(p => p[1])) - this.strokeWidth / 2;
15843
- this.translateX = x;
15844
- this.translateY = y;
15936
+ if (x !== null) {
15937
+ this.translateX = x;
15938
+ }
15939
+ if (y !== null) {
15940
+ this.translateY = y;
15941
+ }
15845
15942
  this._adjustedPoints = null;
15846
15943
  this._core.store.objects.update(this);
15847
15944
  }
@@ -16047,19 +16144,14 @@ class KritzelPath extends KritzelBaseObject {
16047
16144
  }
16048
16145
  /**
16049
16146
  * Updates width, height, x, y, translateX, and translateY based on current points.
16050
- * Accounts for rotation and stroke width in the calculations.
16147
+ * Uses the unrotated local points and stroke width for base dimensions.
16051
16148
  * Called during initial setup to establish the path's dimensions and position.
16052
16149
  */
16053
16150
  updateDimensions() {
16054
- const rotatedPoints = this.points.map(([x, y]) => {
16055
- const rotatedX = x * Math.cos(this.rotation) - y * Math.sin(this.rotation);
16056
- const rotatedY = x * Math.sin(this.rotation) + y * Math.cos(this.rotation);
16057
- return [rotatedX, rotatedY];
16058
- });
16059
- const minX = Math.min(...rotatedPoints.map(p => p[0] - this.strokeWidth / 2));
16060
- const minY = Math.min(...rotatedPoints.map(p => p[1] - this.strokeWidth / 2));
16061
- const maxX = Math.max(...rotatedPoints.map(p => p[0] + this.strokeWidth / 2));
16062
- const maxY = Math.max(...rotatedPoints.map(p => p[1] + this.strokeWidth / 2));
16151
+ const minX = Math.min(...this.points.map(p => p[0])) - this.strokeWidth / 2;
16152
+ const minY = Math.min(...this.points.map(p => p[1])) - this.strokeWidth / 2;
16153
+ const maxX = Math.max(...this.points.map(p => p[0])) + this.strokeWidth / 2;
16154
+ const maxY = Math.max(...this.points.map(p => p[1])) + this.strokeWidth / 2;
16063
16155
  this.width = maxX - minX + this.lineSlack;
16064
16156
  this.height = maxY - minY + this.lineSlack;
16065
16157
  this.x = minX;
@@ -17126,6 +17218,7 @@ class KritzelImage extends KritzelBaseObject {
17126
17218
  this.y = config?.y || 0;
17127
17219
  this.translateX = config?.translateX || 0;
17128
17220
  this.translateY = config?.translateY || 0;
17221
+ this.rotation = KritzelMathHelper.degreesToRadians(config?.rotation ?? 0);
17129
17222
  this.scale = config?.scale || 1;
17130
17223
  this.width = config?.width || 0;
17131
17224
  this.height = config?.height || 0;
@@ -17139,17 +17232,18 @@ class KritzelImage extends KritzelBaseObject {
17139
17232
  * @param core - The KritzelCore instance providing access to store and state management.
17140
17233
  * @returns A new KritzelImage instance configured with the core context.
17141
17234
  */
17142
- static create(core) {
17143
- const object = new KritzelImage();
17235
+ static create(core, config) {
17236
+ const object = new KritzelImage(config);
17144
17237
  object._core = core;
17145
17238
  object.id = object.generateId();
17146
17239
  object.workspaceId = core.store.state.activeWorkspace.id;
17147
17240
  object.userId = core.user?.id;
17148
- object.x = 0;
17149
- object.y = 0;
17150
- object.translateX = 0;
17151
- object.translateY = 0;
17152
- object.scale = object._core.store.state.scale;
17241
+ object.x = config?.x ?? 0;
17242
+ object.y = config?.y ?? 0;
17243
+ object.translateX = config?.translateX ?? 0;
17244
+ object.translateY = config?.translateY ?? 0;
17245
+ object.scale = config?.scale ?? object._core.store.state.scale;
17246
+ object.rotation = KritzelMathHelper.degreesToRadians(config?.rotation ?? 0);
17153
17247
  object.zIndex = core.store.currentZIndex;
17154
17248
  return object;
17155
17249
  }
@@ -17528,6 +17622,7 @@ class KritzelLine extends KritzelBaseObject {
17528
17622
  this.controlY = config?.controlY;
17529
17623
  this.translateX = config?.translateX ?? 0;
17530
17624
  this.translateY = config?.translateY ?? 0;
17625
+ this.rotation = KritzelMathHelper.degreesToRadians(config?.rotation ?? 0);
17531
17626
  this.scale = config?.scale ?? 1;
17532
17627
  this.strokeWidth = config?.strokeWidth ?? 4;
17533
17628
  this.stroke = config?.stroke ?? { light: '#000000', dark: '#ffffff' };
@@ -17558,6 +17653,7 @@ class KritzelLine extends KritzelBaseObject {
17558
17653
  object.controlY = options?.controlY;
17559
17654
  object.translateX = options?.translateX ?? 0;
17560
17655
  object.translateY = options?.translateY ?? 0;
17656
+ object.rotation = KritzelMathHelper.degreesToRadians(options?.rotation ?? 0);
17561
17657
  object.scale = options?.scale ?? 1;
17562
17658
  object.strokeWidth = options?.strokeWidth ?? 4;
17563
17659
  object.stroke = options?.stroke ?? { light: '#000000', dark: '#ffffff' };
@@ -18252,6 +18348,10 @@ class KritzelClassHelper {
18252
18348
  */
18253
18349
  class KritzelGroup extends KritzelBaseObject {
18254
18350
  __class__ = 'KritzelGroup';
18351
+ constructor(config) {
18352
+ super();
18353
+ this.rotation = KritzelMathHelper.degreesToRadians(config?.rotation ?? 0);
18354
+ }
18255
18355
  /**
18256
18356
  * IDs of child objects within this group.
18257
18357
  * Children can be any KritzelBaseObject, including other KritzelGroups for nesting.
@@ -18303,8 +18403,8 @@ class KritzelGroup extends KritzelBaseObject {
18303
18403
  * @param core - The KritzelCore instance providing access to store and state management.
18304
18404
  * @returns A new KritzelGroup instance configured with the core context.
18305
18405
  */
18306
- static create(core) {
18307
- const group = new KritzelGroup();
18406
+ static create(core, config) {
18407
+ const group = new KritzelGroup(config);
18308
18408
  group._core = core;
18309
18409
  group.id = group.generateId();
18310
18410
  group.workspaceId = core.store.state.activeWorkspace.id;
@@ -18893,6 +18993,7 @@ class KritzelShape extends KritzelBaseObject {
18893
18993
  this.y = config.y ?? 0;
18894
18994
  this.translateX = config.translateX ?? 0;
18895
18995
  this.translateY = config.translateY ?? 0;
18996
+ this.rotation = KritzelMathHelper.degreesToRadians(config.rotation ?? 0);
18896
18997
  this.width = config.width ?? 100;
18897
18998
  this.height = config.height ?? 100;
18898
18999
  this.shapeType = config.shapeType ?? ShapeType.Rectangle;
@@ -18924,6 +19025,7 @@ class KritzelShape extends KritzelBaseObject {
18924
19025
  object.y = config?.y ?? 0;
18925
19026
  object.translateX = config?.translateX ?? 0;
18926
19027
  object.translateY = config?.translateY ?? 0;
19028
+ object.rotation = KritzelMathHelper.degreesToRadians(config?.rotation ?? 0);
18927
19029
  object.width = config?.width ?? 100;
18928
19030
  object.height = config?.height ?? 100;
18929
19031
  object.shapeType = config?.shapeType ?? ShapeType.Rectangle;
@@ -19422,71 +19524,6 @@ class KritzelShape extends KritzelBaseObject {
19422
19524
  }
19423
19525
  }
19424
19526
 
19425
- var KritzelMouseButton;
19426
- (function (KritzelMouseButton) {
19427
- KritzelMouseButton[KritzelMouseButton["Left"] = 0] = "Left";
19428
- KritzelMouseButton[KritzelMouseButton["Middle"] = 1] = "Middle";
19429
- KritzelMouseButton[KritzelMouseButton["Right"] = 2] = "Right";
19430
- })(KritzelMouseButton || (KritzelMouseButton = {}));
19431
-
19432
- class KritzelEventHelper {
19433
- static isRightClick(ev) {
19434
- return ev.button === KritzelMouseButton.Right;
19435
- }
19436
- static isLeftClick(ev) {
19437
- return ev.button === KritzelMouseButton.Left;
19438
- }
19439
- static isPointerEventOnContextMenu(event) {
19440
- const path = event.composedPath();
19441
- const contextMenu = path.find(element => element.classList && element.classList.contains('context-menu'));
19442
- return !!contextMenu;
19443
- }
19444
- static onLongPress(event, onSuccess, onCancel) {
19445
- if (event.pointerType !== 'touch') {
19446
- onCancel?.();
19447
- return () => { };
19448
- }
19449
- const startX = event.clientX;
19450
- const startY = event.clientY;
19451
- const target = event.target;
19452
- if (!target) {
19453
- onCancel?.();
19454
- return () => { };
19455
- }
19456
- const longPressTimeout = 400;
19457
- const moveThreshold = 10;
19458
- const timer = setTimeout(() => {
19459
- removeListeners();
19460
- onSuccess(event);
19461
- }, longPressTimeout);
19462
- const cancel = () => {
19463
- clearTimeout(timer);
19464
- removeListeners();
19465
- onCancel?.();
19466
- };
19467
- const onPointerMove = (e) => {
19468
- if (Math.abs(e.clientX - startX) > moveThreshold || Math.abs(e.clientY - startY) > moveThreshold) {
19469
- cancel();
19470
- }
19471
- };
19472
- const onPointerUp = () => {
19473
- cancel();
19474
- };
19475
- const onPointerCancel = () => {
19476
- cancel();
19477
- };
19478
- const removeListeners = () => {
19479
- target.removeEventListener('pointermove', onPointerMove);
19480
- target.removeEventListener('pointerup', onPointerUp);
19481
- target.removeEventListener('pointercancel', onPointerCancel);
19482
- };
19483
- target.addEventListener('pointermove', onPointerMove, { passive: true });
19484
- target.addEventListener('pointerup', onPointerUp, { once: true });
19485
- target.addEventListener('pointercancel', onPointerCancel, { once: true });
19486
- return cancel;
19487
- }
19488
- }
19489
-
19490
19527
  /**
19491
19528
  * Abstract base class for all drawing tools in Kritzel.
19492
19529
  * Provides common functionality and defines the interface that all tools must implement.
@@ -19575,6 +19612,71 @@ class KritzelBaseTool {
19575
19612
  }
19576
19613
  }
19577
19614
 
19615
+ var KritzelMouseButton;
19616
+ (function (KritzelMouseButton) {
19617
+ KritzelMouseButton[KritzelMouseButton["Left"] = 0] = "Left";
19618
+ KritzelMouseButton[KritzelMouseButton["Middle"] = 1] = "Middle";
19619
+ KritzelMouseButton[KritzelMouseButton["Right"] = 2] = "Right";
19620
+ })(KritzelMouseButton || (KritzelMouseButton = {}));
19621
+
19622
+ class KritzelEventHelper {
19623
+ static isRightClick(ev) {
19624
+ return ev.button === KritzelMouseButton.Right;
19625
+ }
19626
+ static isLeftClick(ev) {
19627
+ return ev.button === KritzelMouseButton.Left;
19628
+ }
19629
+ static isPointerEventOnContextMenu(event) {
19630
+ const path = event.composedPath();
19631
+ const contextMenu = path.find(element => element.classList && element.classList.contains('context-menu'));
19632
+ return !!contextMenu;
19633
+ }
19634
+ static onLongPress(event, onSuccess, onCancel) {
19635
+ if (event.pointerType !== 'touch') {
19636
+ onCancel?.();
19637
+ return () => { };
19638
+ }
19639
+ const startX = event.clientX;
19640
+ const startY = event.clientY;
19641
+ const target = event.target;
19642
+ if (!target) {
19643
+ onCancel?.();
19644
+ return () => { };
19645
+ }
19646
+ const longPressTimeout = 400;
19647
+ const moveThreshold = 10;
19648
+ const timer = setTimeout(() => {
19649
+ removeListeners();
19650
+ onSuccess(event);
19651
+ }, longPressTimeout);
19652
+ const cancel = () => {
19653
+ clearTimeout(timer);
19654
+ removeListeners();
19655
+ onCancel?.();
19656
+ };
19657
+ const onPointerMove = (e) => {
19658
+ if (Math.abs(e.clientX - startX) > moveThreshold || Math.abs(e.clientY - startY) > moveThreshold) {
19659
+ cancel();
19660
+ }
19661
+ };
19662
+ const onPointerUp = () => {
19663
+ cancel();
19664
+ };
19665
+ const onPointerCancel = () => {
19666
+ cancel();
19667
+ };
19668
+ const removeListeners = () => {
19669
+ target.removeEventListener('pointermove', onPointerMove);
19670
+ target.removeEventListener('pointerup', onPointerUp);
19671
+ target.removeEventListener('pointercancel', onPointerCancel);
19672
+ };
19673
+ target.addEventListener('pointermove', onPointerMove, { passive: true });
19674
+ target.addEventListener('pointerup', onPointerUp, { once: true });
19675
+ target.addEventListener('pointercancel', onPointerCancel, { once: true });
19676
+ return cancel;
19677
+ }
19678
+ }
19679
+
19578
19680
  const DEFAULT_STROKE_SIZES = [4, 6, 8, 12, 16, 24];
19579
19681
  const DEFAULT_FONT_SIZES = [8, 10, 12, 16, 20, 24];
19580
19682
 
@@ -19603,7 +19705,7 @@ class KritzelBrushTool extends KritzelBaseTool {
19603
19705
  * websocket traffic without visible quality loss — `perfect-freehand`
19604
19706
  * already smooths the rendered stroke.
19605
19707
  */
19606
- static MIN_POINT_DISTANCE_PX = 5;
19708
+ static MIN_POINT_DISTANCE_PX = 3;
19607
19709
  /** Tracks the ID of the path currently being drawn */
19608
19710
  _currentPathId = null;
19609
19711
  /**
@@ -19817,6 +19919,10 @@ class KritzelBrushTool extends KritzelBaseTool {
19817
19919
  */
19818
19920
  class KritzelSelectionGroup extends KritzelBaseObject {
19819
19921
  __class__ = 'KritzelSelectionGroup';
19922
+ constructor(config) {
19923
+ super();
19924
+ this.rotation = KritzelMathHelper.degreesToRadians(config?.rotation ?? 0);
19925
+ }
19820
19926
  // Store only object IDs instead of full objects
19821
19927
  _objectIds = [];
19822
19928
  // Cached objects array - invalidated when objectIds changes
@@ -19900,8 +20006,8 @@ class KritzelSelectionGroup extends KritzelBaseObject {
19900
20006
  * @param core - The KritzelCore instance to associate with this selection group
19901
20007
  * @returns A new KritzelSelectionGroup instance configured with default settings
19902
20008
  */
19903
- static create(core) {
19904
- const object = new KritzelSelectionGroup();
20009
+ static create(core, config) {
20010
+ const object = new KritzelSelectionGroup(config);
19905
20011
  object._core = core;
19906
20012
  object.id = object.generateId();
19907
20013
  object.workspaceId = core.store.state.activeWorkspace.id;
@@ -21518,6 +21624,7 @@ KritzelIconRegistry.registerIcons({
21518
21624
  '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>',
21519
21625
  '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>',
21520
21626
  '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>',
21627
+ '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>',
21521
21628
  '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>',
21522
21629
  '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>',
21523
21630
  '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>',
@@ -28291,6 +28398,890 @@ class KritzelAnchorManager {
28291
28398
  }
28292
28399
  }
28293
28400
 
28401
+ /**
28402
+ * Pure static utilities for localization term resolution.
28403
+ */
28404
+ class LocalizationHelper {
28405
+ /**
28406
+ * Interpolates `{placeholder}` tokens in a term template with the provided
28407
+ * variables. Tokens without a matching variable are left untouched.
28408
+ *
28409
+ * @param template - The raw term string, e.g. `'Expires {date}'`
28410
+ * @param vars - Map of placeholder names to replacement values
28411
+ * @returns The interpolated string
28412
+ *
28413
+ * @example
28414
+ * LocalizationHelper.interpolate('Hello {name}', { name: 'Ada' }); // 'Hello Ada'
28415
+ */
28416
+ static interpolate(template, vars) {
28417
+ if (!vars) {
28418
+ return template;
28419
+ }
28420
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
28421
+ const value = vars[key];
28422
+ return value === undefined || value === null ? match : String(value);
28423
+ });
28424
+ }
28425
+ }
28426
+
28427
+ /**
28428
+ * Built-in English locale (default). Provides a complete set of term values
28429
+ * that act as the ultimate fallback when other locales omit a key.
28430
+ */
28431
+ const EN_LOCALE = {
28432
+ code: 'en',
28433
+ label: 'English',
28434
+ terms: {
28435
+ // Context menu
28436
+ 'menu.copy': 'Copy',
28437
+ 'menu.cut': 'Cut',
28438
+ 'menu.paste': 'Paste',
28439
+ 'menu.selectAll': 'Select All',
28440
+ 'menu.order': 'Order',
28441
+ 'menu.bringToFront': 'Bring to Front',
28442
+ 'menu.sendToBack': 'Send to Back',
28443
+ 'menu.moveUp': 'Move Up',
28444
+ 'menu.moveDown': 'Move Down',
28445
+ 'menu.align': 'Align',
28446
+ 'menu.alignLeft': 'Align Left',
28447
+ 'menu.alignCenterHorizontal': 'Align Center Horizontally',
28448
+ 'menu.alignRight': 'Align Right',
28449
+ 'menu.alignTop': 'Align Top',
28450
+ 'menu.alignCenterVertical': 'Align Center Vertically',
28451
+ 'menu.alignBottom': 'Align Bottom',
28452
+ 'menu.group': 'Group',
28453
+ 'menu.ungroup': 'Ungroup',
28454
+ 'menu.export': 'Export',
28455
+ 'menu.exportAsSvg': 'Export as SVG',
28456
+ 'menu.exportAsPng': 'Export as PNG',
28457
+ 'menu.delete': 'Delete',
28458
+ // More menu
28459
+ 'menu.share': 'Share',
28460
+ 'menu.import': 'Import',
28461
+ 'menu.settings': 'Settings',
28462
+ 'menu.logout': 'Logout',
28463
+ // Settings dialog
28464
+ 'settings.dialogTitle': 'Settings',
28465
+ 'settings.categories.general': 'General',
28466
+ 'settings.categories.viewport': 'Viewport',
28467
+ 'settings.categories.shortcuts': 'Keyboard Shortcuts',
28468
+ 'settings.categories.developer': 'Developer Options',
28469
+ 'settings.categories.about': 'About',
28470
+ 'settings.general.title': 'General Settings',
28471
+ 'settings.general.theme.label': 'Theme',
28472
+ 'settings.general.theme.description': 'Select a registered color theme for the editor interface.',
28473
+ 'settings.general.language.label': 'Language',
28474
+ 'settings.general.language.description': 'Select the display language for the editor interface.',
28475
+ 'settings.general.lockDrawingScale.label': 'Lock Drawing Scale',
28476
+ 'settings.general.lockDrawingScale.description': 'When enabled, drawn objects maintain a fixed visual size regardless of the current zoom level.',
28477
+ 'settings.viewport.title': 'Viewport Settings',
28478
+ 'settings.viewport.minZoom.label': 'Minimum Zoom Level',
28479
+ 'settings.viewport.minZoom.description': 'Sets the minimum zoom level. Lower values allow zooming out further to see more of the canvas.',
28480
+ 'settings.viewport.maxZoom.label': 'Maximum Zoom Level',
28481
+ 'settings.viewport.maxZoom.description': 'Sets the maximum zoom level. Higher values allow zooming in closer for detailed work.',
28482
+ 'settings.viewport.boundaryLeft.label': 'Viewport Boundary Left',
28483
+ 'settings.viewport.boundaryLeft.description': 'Left boundary in world coordinates. Set to limit how far left the viewport can pan.',
28484
+ 'settings.viewport.boundaryRight.label': 'Viewport Boundary Right',
28485
+ 'settings.viewport.boundaryRight.description': 'Right boundary in world coordinates. Set to limit how far right the viewport can pan.',
28486
+ 'settings.viewport.boundaryTop.label': 'Viewport Boundary Top',
28487
+ 'settings.viewport.boundaryTop.description': 'Top boundary in world coordinates. Set to limit how far up the viewport can pan.',
28488
+ 'settings.viewport.boundaryBottom.label': 'Viewport Boundary Bottom',
28489
+ 'settings.viewport.boundaryBottom.description': 'Bottom boundary in world coordinates. Set to limit how far down the viewport can pan.',
28490
+ 'settings.viewport.boundaryPlaceholder': 'Infinite',
28491
+ 'settings.shortcuts.title': 'Keyboard Shortcuts',
28492
+ 'settings.developer.title': 'Developer Options',
28493
+ 'settings.developer.showViewportInfo.label': 'Show Viewport Info',
28494
+ 'settings.developer.showViewportInfo.description': 'Display viewport debug information such as position, zoom level, and boundaries.',
28495
+ 'settings.developer.showObjectInfo.label': 'Show Object Info',
28496
+ 'settings.developer.showObjectInfo.description': 'Display debug information about objects on the canvas.',
28497
+ 'settings.developer.showSyncProviderInfo.label': 'Show Sync Provider Info',
28498
+ 'settings.developer.showSyncProviderInfo.description': 'Display debug information about the sync provider connection status.',
28499
+ 'settings.developer.showMigrationInfo.label': 'Show Migration Info',
28500
+ 'settings.developer.showMigrationInfo.description': 'Display debug information about data migrations.',
28501
+ 'settings.about.title': 'About',
28502
+ 'settings.about.description': 'Kritzel - A drawing application',
28503
+ // Export dialog
28504
+ 'export.dialogTitle': 'Export',
28505
+ 'export.tabs.viewport': 'Export Viewport',
28506
+ 'export.tabs.workspace': 'Export Workspace',
28507
+ 'export.format.label': 'Format',
28508
+ 'export.filename.label': 'Filename',
28509
+ 'export.filename.placeholder': 'Enter filename',
28510
+ 'export.exportButton': 'Export',
28511
+ // Workspace manager
28512
+ 'workspace.sharedTooltip': 'Shared workspace',
28513
+ 'workspace.rename': 'Rename',
28514
+ 'workspace.delete': 'Delete',
28515
+ // Zoom panel
28516
+ 'zoom.zoomIn': 'Zoom in',
28517
+ 'zoom.zoomOut': 'Zoom out',
28518
+ // Utility panel
28519
+ 'utility.undo': 'Undo',
28520
+ 'utility.redo': 'Redo',
28521
+ 'utility.delete': 'Delete selected items',
28522
+ // Share dialog
28523
+ 'share.dialogTitle': 'Share Workspace',
28524
+ 'share.linkSharing.label': 'Link sharing',
28525
+ 'share.linkSharing.enabledDescription': 'Anyone with the link can access this workspace.',
28526
+ 'share.linkSharing.disabledDescription': 'Link sharing is disabled. Only you can access this workspace.',
28527
+ 'share.linkSharing.toggleLabel': 'Enable link sharing',
28528
+ 'share.copyLink.title': 'Copy link',
28529
+ 'share.copyLink.copied': 'Copied!',
28530
+ // Login dialog
28531
+ 'login.dialogTitle': 'Sign in',
28532
+ // Current user dialog
28533
+ 'currentUser.dialogTitle': 'Account',
28534
+ // Back to content
28535
+ 'backToContent.label': 'Back to content',
28536
+ // Tool config
28537
+ 'toolConfig.collapse': 'Collapse',
28538
+ 'toolConfig.expand': 'Expand',
28539
+ // More menu button
28540
+ 'moreMenu.ariaLabel': 'More options',
28541
+ // Engine
28542
+ 'engine.loading': 'Loading...',
28543
+ // Watermark
28544
+ 'watermark.poweredBy': 'Powered by Kritzel',
28545
+ },
28546
+ };
28547
+
28548
+ /**
28549
+ * Built-in German locale.
28550
+ */
28551
+ const DE_LOCALE = {
28552
+ code: 'de',
28553
+ label: 'Deutsch',
28554
+ terms: {
28555
+ // Context menu
28556
+ 'menu.copy': 'Kopieren',
28557
+ 'menu.cut': 'Ausschneiden',
28558
+ 'menu.paste': 'Einfügen',
28559
+ 'menu.selectAll': 'Alles auswählen',
28560
+ 'menu.order': 'Anordnen',
28561
+ 'menu.bringToFront': 'In den Vordergrund',
28562
+ 'menu.sendToBack': 'In den Hintergrund',
28563
+ 'menu.moveUp': 'Nach vorne',
28564
+ 'menu.moveDown': 'Nach hinten',
28565
+ 'menu.align': 'Ausrichten',
28566
+ 'menu.alignLeft': 'Linksbündig ausrichten',
28567
+ 'menu.alignCenterHorizontal': 'Horizontal zentrieren',
28568
+ 'menu.alignRight': 'Rechtsbündig ausrichten',
28569
+ 'menu.alignTop': 'Oben ausrichten',
28570
+ 'menu.alignCenterVertical': 'Vertikal zentrieren',
28571
+ 'menu.alignBottom': 'Unten ausrichten',
28572
+ 'menu.group': 'Gruppieren',
28573
+ 'menu.ungroup': 'Gruppierung aufheben',
28574
+ 'menu.export': 'Exportieren',
28575
+ 'menu.exportAsSvg': 'Als SVG exportieren',
28576
+ 'menu.exportAsPng': 'Als PNG exportieren',
28577
+ 'menu.delete': 'Löschen',
28578
+ // More menu
28579
+ 'menu.share': 'Teilen',
28580
+ 'menu.import': 'Importieren',
28581
+ 'menu.settings': 'Einstellungen',
28582
+ 'menu.logout': 'Abmelden',
28583
+ // Settings dialog
28584
+ 'settings.dialogTitle': 'Einstellungen',
28585
+ 'settings.categories.general': 'Allgemein',
28586
+ 'settings.categories.viewport': 'Ansichtsfenster',
28587
+ 'settings.categories.shortcuts': 'Tastenkürzel',
28588
+ 'settings.categories.developer': 'Entwickleroptionen',
28589
+ 'settings.categories.about': 'Über',
28590
+ 'settings.general.title': 'Allgemeine Einstellungen',
28591
+ 'settings.general.theme.label': 'Design',
28592
+ 'settings.general.theme.description': 'Wählen Sie ein registriertes Farbdesign für die Editor-Oberfläche.',
28593
+ 'settings.general.language.label': 'Sprache',
28594
+ 'settings.general.language.description': 'Wählen Sie die Anzeigesprache für die Editor-Oberfläche.',
28595
+ 'settings.general.lockDrawingScale.label': 'Zeichenskalierung sperren',
28596
+ 'settings.general.lockDrawingScale.description': 'Wenn aktiviert, behalten gezeichnete Objekte unabhängig von der aktuellen Zoomstufe eine feste visuelle Größe.',
28597
+ 'settings.viewport.title': 'Ansichtsfenster-Einstellungen',
28598
+ 'settings.viewport.minZoom.label': 'Minimale Zoomstufe',
28599
+ 'settings.viewport.minZoom.description': 'Legt die minimale Zoomstufe fest. Niedrigere Werte ermöglichen weiteres Herauszoomen, um mehr von der Zeichenfläche zu sehen.',
28600
+ 'settings.viewport.maxZoom.label': 'Maximale Zoomstufe',
28601
+ 'settings.viewport.maxZoom.description': 'Legt die maximale Zoomstufe fest. Höhere Werte ermöglichen näheres Heranzoomen für Detailarbeit.',
28602
+ 'settings.viewport.boundaryLeft.label': 'Ansichtsfenster-Grenze links',
28603
+ 'settings.viewport.boundaryLeft.description': 'Linke Grenze in Weltkoordinaten. Legen Sie fest, wie weit das Ansichtsfenster nach links verschoben werden kann.',
28604
+ 'settings.viewport.boundaryRight.label': 'Ansichtsfenster-Grenze rechts',
28605
+ 'settings.viewport.boundaryRight.description': 'Rechte Grenze in Weltkoordinaten. Legen Sie fest, wie weit das Ansichtsfenster nach rechts verschoben werden kann.',
28606
+ 'settings.viewport.boundaryTop.label': 'Ansichtsfenster-Grenze oben',
28607
+ 'settings.viewport.boundaryTop.description': 'Obere Grenze in Weltkoordinaten. Legen Sie fest, wie weit das Ansichtsfenster nach oben verschoben werden kann.',
28608
+ 'settings.viewport.boundaryBottom.label': 'Ansichtsfenster-Grenze unten',
28609
+ 'settings.viewport.boundaryBottom.description': 'Untere Grenze in Weltkoordinaten. Legen Sie fest, wie weit das Ansichtsfenster nach unten verschoben werden kann.',
28610
+ 'settings.viewport.boundaryPlaceholder': 'Unendlich',
28611
+ 'settings.shortcuts.title': 'Tastenkürzel',
28612
+ 'settings.developer.title': 'Entwickleroptionen',
28613
+ 'settings.developer.showViewportInfo.label': 'Ansichtsfenster-Infos anzeigen',
28614
+ 'settings.developer.showViewportInfo.description': 'Zeigt Debug-Informationen zum Ansichtsfenster an, z. B. Position, Zoomstufe und Grenzen.',
28615
+ 'settings.developer.showObjectInfo.label': 'Objekt-Infos anzeigen',
28616
+ 'settings.developer.showObjectInfo.description': 'Zeigt Debug-Informationen zu Objekten auf der Zeichenfläche an.',
28617
+ 'settings.developer.showSyncProviderInfo.label': 'Sync-Provider-Infos anzeigen',
28618
+ 'settings.developer.showSyncProviderInfo.description': 'Zeigt Debug-Informationen zum Verbindungsstatus des Sync-Providers an.',
28619
+ 'settings.developer.showMigrationInfo.label': 'Migrations-Infos anzeigen',
28620
+ 'settings.developer.showMigrationInfo.description': 'Zeigt Debug-Informationen zu Datenmigrationen an.',
28621
+ 'settings.about.title': 'Über',
28622
+ 'settings.about.description': 'Kritzel – Eine Zeichenanwendung',
28623
+ // Export dialog
28624
+ 'export.dialogTitle': 'Exportieren',
28625
+ 'export.tabs.viewport': 'Ansichtsfenster exportieren',
28626
+ 'export.tabs.workspace': 'Arbeitsbereich exportieren',
28627
+ 'export.format.label': 'Format',
28628
+ 'export.filename.label': 'Dateiname',
28629
+ 'export.filename.placeholder': 'Dateiname eingeben',
28630
+ 'export.exportButton': 'Exportieren',
28631
+ // Workspace manager
28632
+ 'workspace.sharedTooltip': 'Geteilter Arbeitsbereich',
28633
+ 'workspace.rename': 'Umbenennen',
28634
+ 'workspace.delete': 'Löschen',
28635
+ // Zoom panel
28636
+ 'zoom.zoomIn': 'Vergrößern',
28637
+ 'zoom.zoomOut': 'Verkleinern',
28638
+ // Utility panel
28639
+ 'utility.undo': 'Rückgängig',
28640
+ 'utility.redo': 'Wiederholen',
28641
+ 'utility.delete': 'Ausgewählte Elemente löschen',
28642
+ // Share dialog
28643
+ 'share.dialogTitle': 'Arbeitsbereich teilen',
28644
+ 'share.linkSharing.label': 'Link-Freigabe',
28645
+ 'share.linkSharing.enabledDescription': 'Jeder mit dem Link kann auf diesen Arbeitsbereich zugreifen.',
28646
+ 'share.linkSharing.disabledDescription': 'Die Link-Freigabe ist deaktiviert. Nur Sie können auf diesen Arbeitsbereich zugreifen.',
28647
+ 'share.linkSharing.toggleLabel': 'Link-Freigabe aktivieren',
28648
+ 'share.copyLink.title': 'Link kopieren',
28649
+ 'share.copyLink.copied': 'Kopiert!',
28650
+ // Login dialog
28651
+ 'login.dialogTitle': 'Anmelden',
28652
+ // Current user dialog
28653
+ 'currentUser.dialogTitle': 'Konto',
28654
+ // Back to content
28655
+ 'backToContent.label': 'Zurück zum Inhalt',
28656
+ // Tool config
28657
+ 'toolConfig.collapse': 'Einklappen',
28658
+ 'toolConfig.expand': 'Ausklappen',
28659
+ // More menu button
28660
+ 'moreMenu.ariaLabel': 'Weitere Optionen',
28661
+ // Engine
28662
+ 'engine.loading': 'Wird geladen...',
28663
+ // Watermark
28664
+ 'watermark.poweredBy': 'Bereitgestellt von Kritzel',
28665
+ },
28666
+ };
28667
+
28668
+ /**
28669
+ * Built-in French locale.
28670
+ */
28671
+ const FR_LOCALE = {
28672
+ code: 'fr',
28673
+ label: 'Français',
28674
+ terms: {
28675
+ // Context menu
28676
+ 'menu.copy': 'Copier',
28677
+ 'menu.cut': 'Couper',
28678
+ 'menu.paste': 'Coller',
28679
+ 'menu.selectAll': 'Tout sélectionner',
28680
+ 'menu.order': 'Ordre',
28681
+ 'menu.bringToFront': 'Mettre au premier plan',
28682
+ 'menu.sendToBack': "Mettre à l'arrière-plan",
28683
+ 'menu.moveUp': 'Avancer',
28684
+ 'menu.moveDown': 'Reculer',
28685
+ 'menu.align': 'Aligner',
28686
+ 'menu.alignLeft': 'Aligner à gauche',
28687
+ 'menu.alignCenterHorizontal': 'Centrer horizontalement',
28688
+ 'menu.alignRight': 'Aligner à droite',
28689
+ 'menu.alignTop': 'Aligner en haut',
28690
+ 'menu.alignCenterVertical': 'Centrer verticalement',
28691
+ 'menu.alignBottom': 'Aligner en bas',
28692
+ 'menu.group': 'Grouper',
28693
+ 'menu.ungroup': 'Dégrouper',
28694
+ 'menu.export': 'Exporter',
28695
+ 'menu.exportAsSvg': 'Exporter en SVG',
28696
+ 'menu.exportAsPng': 'Exporter en PNG',
28697
+ 'menu.delete': 'Supprimer',
28698
+ // More menu
28699
+ 'menu.share': 'Partager',
28700
+ 'menu.import': 'Importer',
28701
+ 'menu.settings': 'Paramètres',
28702
+ 'menu.logout': 'Déconnexion',
28703
+ // Settings dialog
28704
+ 'settings.dialogTitle': 'Paramètres',
28705
+ 'settings.categories.general': 'Général',
28706
+ 'settings.categories.viewport': "Fenêtre d'affichage",
28707
+ 'settings.categories.shortcuts': 'Raccourcis clavier',
28708
+ 'settings.categories.developer': 'Options développeur',
28709
+ 'settings.categories.about': 'À propos',
28710
+ 'settings.general.title': 'Paramètres généraux',
28711
+ 'settings.general.theme.label': 'Thème',
28712
+ 'settings.general.theme.description': "Sélectionnez un thème de couleurs enregistré pour l'interface de l'éditeur.",
28713
+ 'settings.general.language.label': 'Langue',
28714
+ 'settings.general.language.description': "Sélectionnez la langue d'affichage de l'interface de l'éditeur.",
28715
+ 'settings.general.lockDrawingScale.label': "Verrouiller l'échelle de dessin",
28716
+ '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.",
28717
+ 'settings.viewport.title': "Paramètres de la fenêtre d'affichage",
28718
+ 'settings.viewport.minZoom.label': 'Niveau de zoom minimal',
28719
+ '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.',
28720
+ 'settings.viewport.maxZoom.label': 'Niveau de zoom maximal',
28721
+ '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é.',
28722
+ 'settings.viewport.boundaryLeft.label': "Limite gauche de la fenêtre d'affichage",
28723
+ '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.",
28724
+ 'settings.viewport.boundaryRight.label': "Limite droite de la fenêtre d'affichage",
28725
+ '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.",
28726
+ 'settings.viewport.boundaryTop.label': "Limite supérieure de la fenêtre d'affichage",
28727
+ '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.",
28728
+ 'settings.viewport.boundaryBottom.label': "Limite inférieure de la fenêtre d'affichage",
28729
+ '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.",
28730
+ 'settings.viewport.boundaryPlaceholder': 'Infini',
28731
+ 'settings.shortcuts.title': 'Raccourcis clavier',
28732
+ 'settings.developer.title': 'Options développeur',
28733
+ 'settings.developer.showViewportInfo.label': "Afficher les infos de la fenêtre d'affichage",
28734
+ '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.",
28735
+ 'settings.developer.showObjectInfo.label': 'Afficher les infos des objets',
28736
+ 'settings.developer.showObjectInfo.description': 'Affiche les informations de débogage des objets sur le canevas.',
28737
+ 'settings.developer.showSyncProviderInfo.label': 'Afficher les infos du fournisseur de synchronisation',
28738
+ 'settings.developer.showSyncProviderInfo.description': "Affiche les informations de débogage sur l'état de connexion du fournisseur de synchronisation.",
28739
+ 'settings.developer.showMigrationInfo.label': 'Afficher les infos de migration',
28740
+ 'settings.developer.showMigrationInfo.description': 'Affiche les informations de débogage sur les migrations de données.',
28741
+ 'settings.about.title': 'À propos',
28742
+ 'settings.about.description': 'Kritzel - Une application de dessin',
28743
+ // Export dialog
28744
+ 'export.dialogTitle': 'Exporter',
28745
+ 'export.tabs.viewport': "Exporter la fenêtre d'affichage",
28746
+ 'export.tabs.workspace': "Exporter l'espace de travail",
28747
+ 'export.format.label': 'Format',
28748
+ 'export.filename.label': 'Nom du fichier',
28749
+ 'export.filename.placeholder': 'Saisir le nom du fichier',
28750
+ 'export.exportButton': 'Exporter',
28751
+ // Workspace manager
28752
+ 'workspace.sharedTooltip': 'Espace de travail partagé',
28753
+ 'workspace.rename': 'Renommer',
28754
+ 'workspace.delete': 'Supprimer',
28755
+ // Zoom panel
28756
+ 'zoom.zoomIn': 'Zoom avant',
28757
+ 'zoom.zoomOut': 'Zoom arrière',
28758
+ // Utility panel
28759
+ 'utility.undo': 'Annuler',
28760
+ 'utility.redo': 'Rétablir',
28761
+ 'utility.delete': 'Supprimer les éléments sélectionnés',
28762
+ // Share dialog
28763
+ 'share.dialogTitle': "Partager l'espace de travail",
28764
+ 'share.linkSharing.label': 'Partage par lien',
28765
+ 'share.linkSharing.enabledDescription': 'Toute personne disposant du lien peut accéder à cet espace de travail.',
28766
+ 'share.linkSharing.disabledDescription': 'Le partage par lien est désactivé. Vous seul pouvez accéder à cet espace de travail.',
28767
+ 'share.linkSharing.toggleLabel': 'Activer le partage par lien',
28768
+ 'share.copyLink.title': 'Copier le lien',
28769
+ 'share.copyLink.copied': 'Copié !',
28770
+ // Login dialog
28771
+ 'login.dialogTitle': 'Se connecter',
28772
+ // Current user dialog
28773
+ 'currentUser.dialogTitle': 'Compte',
28774
+ // Back to content
28775
+ 'backToContent.label': 'Retour au contenu',
28776
+ // Tool config
28777
+ 'toolConfig.collapse': 'Réduire',
28778
+ 'toolConfig.expand': 'Développer',
28779
+ // More menu button
28780
+ 'moreMenu.ariaLabel': "Plus d'options",
28781
+ // Engine
28782
+ 'engine.loading': 'Chargement...',
28783
+ // Watermark
28784
+ 'watermark.poweredBy': 'Propulsé par Kritzel',
28785
+ },
28786
+ };
28787
+
28788
+ /** Key used to store the settings object in localStorage. */
28789
+ const SETTINGS_STORAGE_KEY = 'kritzel-settings';
28790
+ /** Default locale used when no stored preference exists. */
28791
+ const DEFAULT_LOCALE = 'en';
28792
+ /**
28793
+ * Manages localization state and term resolution across the Kritzel editor.
28794
+ *
28795
+ * The manager is owned per {@link KritzelCore} instance (like the theme manager)
28796
+ * so that multiple editors on the same page can use different languages in
28797
+ * isolation.
28798
+ *
28799
+ * Term resolution follows a deterministic fallback chain for each key:
28800
+ * 1. active locale's custom terms
28801
+ * 2. active locale's built-in terms
28802
+ * 3. fallback locale's custom terms
28803
+ * 4. fallback locale's built-in terms
28804
+ * 5. the key itself (last resort)
28805
+ *
28806
+ * Persistence of the selected locale is handled by the KritzelSettings
28807
+ * component; this manager only reads the stored value.
28808
+ */
28809
+ class KritzelLocalizationManager {
28810
+ _core;
28811
+ _storageKey;
28812
+ /** Built-in locale definitions, keyed by locale code. */
28813
+ _builtinLocales = new Map();
28814
+ /** Consumer-registered locale definitions, keyed by locale code. */
28815
+ _customLocales = new Map();
28816
+ _currentLocale = DEFAULT_LOCALE;
28817
+ _fallbackLocale = DEFAULT_LOCALE;
28818
+ /**
28819
+ * Creates a new KritzelLocalizationManager instance.
28820
+ * Seeds the built-in locales and initializes the current locale from the
28821
+ * settings object in localStorage (or the default locale).
28822
+ *
28823
+ * @param core - The KritzelCore instance this manager belongs to
28824
+ */
28825
+ constructor(core) {
28826
+ this._core = core;
28827
+ this._storageKey = core.editorId ? `${SETTINGS_STORAGE_KEY}-${core.editorId}` : SETTINGS_STORAGE_KEY;
28828
+ this._builtinLocales.set(EN_LOCALE.code, EN_LOCALE);
28829
+ this._builtinLocales.set(DE_LOCALE.code, DE_LOCALE);
28830
+ this._builtinLocales.set(FR_LOCALE.code, FR_LOCALE);
28831
+ this._currentLocale = this.getStoredLocale();
28832
+ }
28833
+ /**
28834
+ * Gets the currently active locale code.
28835
+ */
28836
+ get currentLocale() {
28837
+ return this._currentLocale;
28838
+ }
28839
+ /**
28840
+ * Gets the fallback locale code used when a term is missing from the active locale.
28841
+ */
28842
+ get fallbackLocale() {
28843
+ return this._fallbackLocale;
28844
+ }
28845
+ /**
28846
+ * Sets the fallback locale code used when a term is missing from the active locale.
28847
+ *
28848
+ * @param code - The fallback locale code
28849
+ */
28850
+ setFallbackLocale(code) {
28851
+ this._fallbackLocale = code;
28852
+ }
28853
+ /**
28854
+ * Registers consumer-provided locale definitions. Definitions are merged by
28855
+ * code; registering the same code again replaces the previous definition.
28856
+ * A registered locale may provide a partial set of terms — missing terms are
28857
+ * resolved through the fallback chain.
28858
+ *
28859
+ * @param locales - The locale definitions to register
28860
+ */
28861
+ registerLocales(locales) {
28862
+ for (const locale of locales) {
28863
+ this._customLocales.set(locale.code, locale);
28864
+ }
28865
+ }
28866
+ /**
28867
+ * Sets the active locale and triggers a re-render so UI strings update.
28868
+ *
28869
+ * @param code - The locale code to activate
28870
+ */
28871
+ setLocale(code) {
28872
+ this._currentLocale = code;
28873
+ this._core.rerender();
28874
+ }
28875
+ /**
28876
+ * Gets the list of available locale codes (built-in and registered).
28877
+ */
28878
+ getAvailableLocales() {
28879
+ return Array.from(new Set([...this._builtinLocales.keys(), ...this._customLocales.keys()]));
28880
+ }
28881
+ /**
28882
+ * Gets the available locales as `{ code, label }` pairs for use in a selector.
28883
+ * A registered locale's label takes precedence over the built-in label; the
28884
+ * code is used as the label when none is provided.
28885
+ */
28886
+ getAvailableLocaleOptions() {
28887
+ return this.getAvailableLocales().map(code => ({ code, label: this.getLocaleLabel(code) }));
28888
+ }
28889
+ /**
28890
+ * Gets the display label for a locale code, falling back to the code itself.
28891
+ *
28892
+ * @param code - The locale code
28893
+ */
28894
+ getLocaleLabel(code) {
28895
+ return this._customLocales.get(code)?.label ?? this._builtinLocales.get(code)?.label ?? code;
28896
+ }
28897
+ /**
28898
+ * Resolves a term key to its translated string for the active locale and
28899
+ * interpolates any `{placeholder}` tokens.
28900
+ *
28901
+ * @param key - The term key to resolve
28902
+ * @param vars - Optional values for `{placeholder}` interpolation
28903
+ * @returns The translated, interpolated string (or the key if unresolved)
28904
+ *
28905
+ * @example
28906
+ * manager.translate('menu.copy'); // 'Copy'
28907
+ * manager.translate('share.expires', { date: '5/1' }); // 'Expires 5/1'
28908
+ */
28909
+ translate(key, vars) {
28910
+ const resolved = this._customLocales.get(this._currentLocale)?.terms[key] ??
28911
+ this._builtinLocales.get(this._currentLocale)?.terms[key] ??
28912
+ this._customLocales.get(this._fallbackLocale)?.terms[key] ??
28913
+ this._builtinLocales.get(this._fallbackLocale)?.terms[key] ??
28914
+ key;
28915
+ return LocalizationHelper.interpolate(resolved, vars);
28916
+ }
28917
+ /**
28918
+ * Resolves every known term key for the active locale into a flat map.
28919
+ *
28920
+ * The key set is the union of all keys defined across the built-in and
28921
+ * registered locales, so consumer-added keys are included. Each value is
28922
+ * resolved through the same fallback chain as {@link translate} (without
28923
+ * interpolation, since these are shared, variable-free UI labels).
28924
+ *
28925
+ * @returns A map of every term key to its resolved string for the active locale
28926
+ */
28927
+ getAllTerms() {
28928
+ const keys = new Set();
28929
+ for (const locale of [...this._builtinLocales.values(), ...this._customLocales.values()]) {
28930
+ for (const key of Object.keys(locale.terms)) {
28931
+ keys.add(key);
28932
+ }
28933
+ }
28934
+ const result = {};
28935
+ for (const key of keys) {
28936
+ result[key] = this.translate(key);
28937
+ }
28938
+ return result;
28939
+ }
28940
+ /**
28941
+ * Reads the stored locale from the settings object in localStorage using this
28942
+ * instance's namespaced key.
28943
+ *
28944
+ * @returns The stored locale code if valid, or the default locale
28945
+ */
28946
+ getStoredLocale() {
28947
+ if (typeof localStorage === 'undefined') {
28948
+ return DEFAULT_LOCALE;
28949
+ }
28950
+ const stored = localStorage.getItem(this._storageKey);
28951
+ if (!stored) {
28952
+ return DEFAULT_LOCALE;
28953
+ }
28954
+ try {
28955
+ const parsed = JSON.parse(stored);
28956
+ if (typeof parsed?.locale === 'string') {
28957
+ return parsed.locale;
28958
+ }
28959
+ }
28960
+ catch {
28961
+ // Invalid JSON, use default
28962
+ }
28963
+ return DEFAULT_LOCALE;
28964
+ }
28965
+ /**
28966
+ * Cleans up the localization manager state.
28967
+ * Provided for lifecycle parity with other managers.
28968
+ */
28969
+ cleanup() {
28970
+ this._customLocales.clear();
28971
+ }
28972
+ }
28973
+
28974
+ /**
28975
+ * License verification constants.
28976
+ *
28977
+ * Kritzel licenses are offline Ed25519 signed tokens. The owner signs each
28978
+ * customer's token with a private key that never leaves their machine
28979
+ * (see scripts/license/); the library verifies tokens against the public key
28980
+ * below. The public key is safe to ship — it can only *verify* signatures,
28981
+ * never forge them.
28982
+ */
28983
+ /**
28984
+ * Token format version prefix. A valid license key looks like:
28985
+ *
28986
+ * KRTZL1.<base64url(payload JSON)>.<base64url(ed25519 signature)>
28987
+ *
28988
+ * Bump the version (and this prefix) if the token scheme ever changes.
28989
+ */
28990
+ const KRITZEL_LICENSE_TOKEN_PREFIX = 'KRTZL1.';
28991
+ /**
28992
+ * Raw 32-byte Ed25519 public key (base64url) used to verify license tokens.
28993
+ *
28994
+ * Generate the matching key pair with `npm run license:keygen` and paste the
28995
+ * printed public key here. Replacing this value invalidates every license
28996
+ * signed with the previous key.
28997
+ */
28998
+ const KRITZEL_LICENSE_PUBLIC_KEY = 'I-pYJlxDEjT94rwSaqhXW5Sv__fMKS-JKKnV3pg0PaM';
28999
+
29000
+ /** Minimum delay (ms) between periodic license re-validations. */
29001
+ const MIN_REVALIDATION_DELAY_MS = 30_000;
29002
+ /** Maximum delay (ms) between periodic license re-validations. */
29003
+ const MAX_REVALIDATION_DELAY_MS = 60_000;
29004
+ /**
29005
+ * Manages license validation state for the Kritzel editor.
29006
+ *
29007
+ * The manager is owned per {@link KritzelCore} instance (like the theme and
29008
+ * localization managers) so that multiple editors on the same page validate
29009
+ * independently.
29010
+ *
29011
+ * A license key is an offline Ed25519 signed token of the form
29012
+ * `KRTZL1.<base64url(payload)>.<base64url(signature)>`. The library verifies it
29013
+ * against the embedded {@link KRITZEL_LICENSE_PUBLIC_KEY}; only the owner can
29014
+ * mint tokens, with the matching private key (see scripts/license/).
29015
+ *
29016
+ * Responsibilities:
29017
+ * - Cheaply reject absent/malformed/expired keys synchronously so the watermark
29018
+ * never flashes off for an obviously invalid key.
29019
+ * - Verify the signature asynchronously via WebCrypto, then drive a rerender if
29020
+ * the licensed state changed so the watermark is re-asserted on the reactive
29021
+ * render path.
29022
+ * - Periodically re-check on a randomized interval so the check is harder to
29023
+ * stub out programmatically than a fixed `setInterval`. Re-checks reuse the
29024
+ * cached verified token and only re-evaluate expiry, so crypto is not re-run
29025
+ * on an unchanged key every tick.
29026
+ *
29027
+ * Failure semantics (fail open):
29028
+ * - No key provided -> unlicensed (free tier, watermark shown). The normal
29029
+ * default, not an error.
29030
+ * - Key present but cleanly invalid (malformed, bad signature, expired) ->
29031
+ * unlicensed.
29032
+ * - Key present but validation throws an *unexpected* internal error, or the
29033
+ * runtime lacks Ed25519 support in WebCrypto -> fail open and treat as
29034
+ * licensed so a library defect or old browser never breaks a paying
29035
+ * customer's app.
29036
+ */
29037
+ class KritzelLicenseManager {
29038
+ _core;
29039
+ /** The most recently provided license key, re-checked on each periodic tick. */
29040
+ _licenseKey;
29041
+ /** Cached result of the latest validation. */
29042
+ _isLicensed = false;
29043
+ /**
29044
+ * The token whose signature most recently verified successfully. Lets the
29045
+ * periodic re-check skip crypto and only re-evaluate expiry while the key is
29046
+ * unchanged.
29047
+ */
29048
+ _verifiedToken;
29049
+ /**
29050
+ * Monotonic id incremented on every {@link validate} call. An in-flight async
29051
+ * verification only commits its result if its id still matches, so a stale
29052
+ * verification resolving after a newer call can never clobber fresh state.
29053
+ */
29054
+ _validationId = 0;
29055
+ /** Cached imported public key, lazily created on first signature check. */
29056
+ _publicKeyPromise;
29057
+ /** Handle for the self-rescheduling re-validation timer. */
29058
+ _timer = null;
29059
+ /**
29060
+ * Creates a new KritzelLicenseManager instance.
29061
+ * @param core - The KritzelCore instance this manager belongs to
29062
+ */
29063
+ constructor(core) {
29064
+ this._core = core;
29065
+ }
29066
+ /**
29067
+ * Whether the editor is currently considered licensed (watermark hidden).
29068
+ */
29069
+ get isLicensed() {
29070
+ return this._isLicensed;
29071
+ }
29072
+ /**
29073
+ * Validates the given license key.
29074
+ *
29075
+ * Runs a synchronous structural pre-check first: an absent, malformed, or
29076
+ * already-expired key resolves the state immediately (no async work). A
29077
+ * structurally valid key then has its signature verified asynchronously; the
29078
+ * licensed state is committed (and a rerender triggered) only if it changed.
29079
+ *
29080
+ * The return value reflects the state known *synchronously* at call time. The
29081
+ * authoritative result for a structurally valid key arrives shortly after via
29082
+ * the async verification and the reactive rerender.
29083
+ *
29084
+ * @param key - The license key to validate, or undefined to clear the license
29085
+ * @returns The licensed state known synchronously at call time
29086
+ */
29087
+ validate(key) {
29088
+ this._licenseKey = key;
29089
+ const validationId = ++this._validationId;
29090
+ let pre;
29091
+ try {
29092
+ pre = this.preCheck(key);
29093
+ }
29094
+ catch {
29095
+ // Fail open: an unexpected internal error in the cheap pre-check must
29096
+ // never break a paying customer's app.
29097
+ this.commit(true, validationId);
29098
+ return this._isLicensed;
29099
+ }
29100
+ if (pre.status === 'rejected') {
29101
+ this._verifiedToken = undefined;
29102
+ this.commit(false, validationId);
29103
+ return this._isLicensed;
29104
+ }
29105
+ // Structurally valid and unexpired. If we already verified this exact token,
29106
+ // the signature is still good, so license it without re-running crypto.
29107
+ if (this._verifiedToken === key) {
29108
+ this.commit(true, validationId);
29109
+ return this._isLicensed;
29110
+ }
29111
+ // Otherwise verify the signature asynchronously. Leave the current state
29112
+ // untouched until it settles so a forged token cannot even briefly hide the
29113
+ // watermark (default state is unlicensed), and a valid renewal does not flap.
29114
+ this.verifySignature(pre.payloadSegment, pre.signatureSegment)
29115
+ .then(valid => {
29116
+ if (validationId !== this._validationId) {
29117
+ return; // A newer validate() superseded this one.
29118
+ }
29119
+ if (valid) {
29120
+ this._verifiedToken = key;
29121
+ this.commit(true, validationId);
29122
+ }
29123
+ else {
29124
+ this._verifiedToken = undefined;
29125
+ this.commit(false, validationId);
29126
+ }
29127
+ })
29128
+ .catch(() => {
29129
+ if (validationId !== this._validationId) {
29130
+ return;
29131
+ }
29132
+ // Fail open on unexpected crypto errors / missing Ed25519 support.
29133
+ this.commit(true, validationId);
29134
+ });
29135
+ return this._isLicensed;
29136
+ }
29137
+ /**
29138
+ * Starts periodic re-validation on a randomized interval between
29139
+ * {@link MIN_REVALIDATION_DELAY_MS} and {@link MAX_REVALIDATION_DELAY_MS}.
29140
+ *
29141
+ * Uses a self-rescheduling `setTimeout` (a fresh random delay each cycle)
29142
+ * rather than a fixed `setInterval`, making the check fractionally harder to
29143
+ * predict and no-op programmatically. Any existing timer is cleared first so
29144
+ * the method is idempotent.
29145
+ */
29146
+ startPeriodicValidation() {
29147
+ this.stopPeriodicValidation();
29148
+ this.scheduleNextValidation();
29149
+ }
29150
+ /**
29151
+ * Stops periodic re-validation and releases the timer.
29152
+ */
29153
+ stopPeriodicValidation() {
29154
+ if (this._timer !== null) {
29155
+ clearTimeout(this._timer);
29156
+ this._timer = null;
29157
+ }
29158
+ }
29159
+ /**
29160
+ * Cleanup hook called when the editor is torn down.
29161
+ */
29162
+ destroy() {
29163
+ this.stopPeriodicValidation();
29164
+ }
29165
+ /**
29166
+ * Commits a new licensed state, triggering a rerender only when it changed.
29167
+ * The {@link validationId} guard is checked by async callers; synchronous
29168
+ * callers pass the current id.
29169
+ */
29170
+ commit(nextLicensed, validationId) {
29171
+ if (validationId !== this._validationId) {
29172
+ return;
29173
+ }
29174
+ if (nextLicensed !== this._isLicensed) {
29175
+ this._isLicensed = nextLicensed;
29176
+ this._core.rerender();
29177
+ }
29178
+ }
29179
+ /**
29180
+ * Schedules the next re-validation tick at a randomized delay.
29181
+ */
29182
+ scheduleNextValidation() {
29183
+ const delay = this.getRandomRevalidationDelay();
29184
+ this._timer = setTimeout(() => {
29185
+ this.validate(this._licenseKey);
29186
+ this.scheduleNextValidation();
29187
+ }, delay);
29188
+ }
29189
+ /**
29190
+ * Returns a random delay within the configured re-validation window.
29191
+ */
29192
+ getRandomRevalidationDelay() {
29193
+ const span = MAX_REVALIDATION_DELAY_MS - MIN_REVALIDATION_DELAY_MS;
29194
+ return MIN_REVALIDATION_DELAY_MS + Math.floor(Math.random() * (span + 1));
29195
+ }
29196
+ /**
29197
+ * Synchronous structural pre-check of a license key: verifies the format
29198
+ * prefix, three-part structure, JSON payload, and (if present) that the token
29199
+ * has not expired. Does not verify the signature.
29200
+ *
29201
+ * @returns `rejected` for absent/malformed/expired keys, or `pass` with the
29202
+ * parsed segments and claims for a structurally valid, unexpired token.
29203
+ */
29204
+ preCheck(key) {
29205
+ if (key === undefined || key === null) {
29206
+ return { status: 'rejected' };
29207
+ }
29208
+ const token = key.trim();
29209
+ if (token.length === 0 || !token.startsWith(KRITZEL_LICENSE_TOKEN_PREFIX)) {
29210
+ return { status: 'rejected' };
29211
+ }
29212
+ const body = token.slice(KRITZEL_LICENSE_TOKEN_PREFIX.length);
29213
+ const parts = body.split('.');
29214
+ if (parts.length !== 2 || parts[0].length === 0 || parts[1].length === 0) {
29215
+ return { status: 'rejected' };
29216
+ }
29217
+ const [payloadSegment, signatureSegment] = parts;
29218
+ let claims;
29219
+ try {
29220
+ claims = JSON.parse(this.base64UrlToString(payloadSegment));
29221
+ }
29222
+ catch {
29223
+ return { status: 'rejected' };
29224
+ }
29225
+ if (claims === null || typeof claims !== 'object') {
29226
+ return { status: 'rejected' };
29227
+ }
29228
+ if (typeof claims.exp === 'number' && claims.exp * 1000 <= Date.now()) {
29229
+ return { status: 'rejected' }; // Cleanly expired.
29230
+ }
29231
+ return { status: 'pass', payloadSegment, signatureSegment, claims };
29232
+ }
29233
+ /**
29234
+ * Verifies the Ed25519 signature of the token against the embedded public
29235
+ * key. Resolves true/false for a definitive valid/invalid signature.
29236
+ *
29237
+ * Rejects (rather than resolving false) on unexpected crypto failures or when
29238
+ * the runtime lacks Ed25519 support, so the caller fails open.
29239
+ */
29240
+ async verifySignature(payloadSegment, signatureSegment) {
29241
+ const publicKey = await this.getPublicKey();
29242
+ const data = this.utf8Bytes(payloadSegment);
29243
+ const signature = this.base64UrlToBytes(signatureSegment);
29244
+ return crypto.subtle.verify('Ed25519', publicKey, signature, data);
29245
+ }
29246
+ /**
29247
+ * Imports and caches the embedded Ed25519 public key for signature checks.
29248
+ */
29249
+ getPublicKey() {
29250
+ if (!this._publicKeyPromise) {
29251
+ const raw = this.base64UrlToBytes(KRITZEL_LICENSE_PUBLIC_KEY);
29252
+ this._publicKeyPromise = Promise.resolve().then(() => crypto.subtle.importKey('raw', raw, { name: 'Ed25519' }, false, ['verify']));
29253
+ // If import fails (e.g. no Ed25519 support), drop the cache so a later
29254
+ // tick can retry, and let the rejection propagate to fail open.
29255
+ this._publicKeyPromise.catch(() => {
29256
+ this._publicKeyPromise = undefined;
29257
+ });
29258
+ }
29259
+ return this._publicKeyPromise;
29260
+ }
29261
+ /** Decodes a base64url string to its UTF-8 text. */
29262
+ base64UrlToString(value) {
29263
+ return new TextDecoder().decode(this.base64UrlToBytes(value));
29264
+ }
29265
+ /** Decodes a base64url string to raw bytes. */
29266
+ base64UrlToBytes(value) {
29267
+ const base64 = value.replace(/-/g, '+').replace(/_/g, '/');
29268
+ const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');
29269
+ const binary = atob(padded);
29270
+ const bytes = new Uint8Array(new ArrayBuffer(binary.length));
29271
+ for (let i = 0; i < binary.length; i++) {
29272
+ bytes[i] = binary.charCodeAt(i);
29273
+ }
29274
+ return bytes;
29275
+ }
29276
+ /** Encodes a string to its UTF-8 bytes. */
29277
+ utf8Bytes(value) {
29278
+ const encoded = new TextEncoder().encode(value);
29279
+ const bytes = new Uint8Array(new ArrayBuffer(encoded.length));
29280
+ bytes.set(encoded);
29281
+ return bytes;
29282
+ }
29283
+ }
29284
+
28294
29285
  const DEFAULT_BRUSH_CONFIG = {
28295
29286
  type: 'pen',
28296
29287
  color: DEFAULT_COLOR_PALETTE[0],
@@ -28463,4 +29454,4 @@ const CURRENT_APP_STATE_SCHEMA_VERSION = 2;
28463
29454
  /** Current schema version for workspace Y.Docs (drawable objects). */
28464
29455
  const CURRENT_WORKSPACE_SCHEMA_VERSION = 2;
28465
29456
 
28466
- export { AssetNotFoundError as A, APP_STATE_MIGRATIONS as B, WORKSPACE_MIGRATIONS as C, DEFAULT_BRUSH_CONFIG as D, CURRENT_APP_STATE_SCHEMA_VERSION as E, CURRENT_WORKSPACE_SCHEMA_VERSION as F, KritzelColorHelper as G, HocuspocusProvider as H, IndexedDBAssetProvider as I, KritzelDevicesHelper as J, KritzelBaseObject as K, KritzelMouseButton as L, DEFAULT_STROKE_SIZES as M, DEFAULT_COLOR_PALETTE as N, KritzelSelectionGroup as O, KritzelSelectionBox as P, KritzelIconRegistry as Q, KritzelKeyboardHelper as R, ShapeType as S, ThemeHelper as T, KritzelBaseHandler as U, ObjectHelper as V, WORKSPACE_EXPORT_VERSION as W, KritzelClassHelper as X, KritzelEventHelper as Y, HocuspocusProviderWebsocket as a, KritzelText as b, KritzelPath as c, KritzelImage as d, KritzelLine as e, KritzelGroup as f, KritzelShape as g, KritzelBrushTool as h, KritzelLineTool as i, KritzelEraserTool as j, KritzelImageTool as k, KritzelTextTool as l, KritzelShapeTool as m, KritzelCursorHelper as n, KritzelSelectionTool as o, KritzelAssetResolver as p, KritzelWorkspace as q, KritzelAnchorManager as r, KritzelThemeManager as s, DEFAULT_TEXT_CONFIG as t, DEFAULT_LINE_TOOL_CONFIG as u, DEFAULT_ASSET_STORAGE_CONFIG as v, lightTheme as w, darkTheme as x, KritzelAlignment as y, runMigrations as z };
29457
+ export { KritzelBaseHandler as $, AssetNotFoundError as A, darkTheme as B, DE_LOCALE as C, DEFAULT_BRUSH_CONFIG as D, EN_LOCALE as E, FR_LOCALE as F, KritzelAlignment as G, HocuspocusProvider as H, IndexedDBAssetProvider as I, runMigrations as J, KritzelBaseObject as K, APP_STATE_MIGRATIONS as L, WORKSPACE_MIGRATIONS as M, CURRENT_APP_STATE_SCHEMA_VERSION as N, CURRENT_WORKSPACE_SCHEMA_VERSION as O, KritzelColorHelper as P, KritzelDevicesHelper as Q, KritzelMouseButton as R, ShapeType as S, DEFAULT_STROKE_SIZES as T, DEFAULT_COLOR_PALETTE as U, ThemeHelper as V, WORKSPACE_EXPORT_VERSION as W, KritzelSelectionGroup as X, KritzelSelectionBox as Y, KritzelIconRegistry as Z, KritzelKeyboardHelper as _, HocuspocusProviderWebsocket as a, KritzelMathHelper as a0, ObjectHelper as a1, KritzelClassHelper as a2, KritzelEventHelper as a3, KritzelText as b, KritzelPath as c, KritzelImage as d, KritzelLine as e, KritzelGroup as f, KritzelShape as g, KritzelBaseTool as h, KritzelBrushTool as i, KritzelLineTool as j, KritzelEraserTool as k, KritzelImageTool as l, KritzelTextTool as m, KritzelShapeTool as n, KritzelCursorHelper as o, KritzelSelectionTool as p, KritzelAssetResolver as q, KritzelWorkspace as r, KritzelAnchorManager as s, KritzelThemeManager as t, KritzelLocalizationManager as u, KritzelLicenseManager as v, DEFAULT_TEXT_CONFIG as w, DEFAULT_LINE_TOOL_CONFIG as x, DEFAULT_ASSET_STORAGE_CONFIG as y, lightTheme as z };