@elixpo/lixsketch 5.4.6 → 5.5.0

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 (220) hide show
  1. package/dist/react/AIRenderer-M4LMJ66J.js +1105 -0
  2. package/dist/react/AIRenderer-M4LMJ66J.js.map +7 -0
  3. package/dist/react/Arrow-BJDAXPMH.js +8 -0
  4. package/dist/react/Arrow-BJDAXPMH.js.map +7 -0
  5. package/dist/react/Circle-KPG7TKIX.js +8 -0
  6. package/dist/react/Circle-KPG7TKIX.js.map +7 -0
  7. package/dist/react/CodeShape-K4DIZAIV.js +8 -0
  8. package/dist/react/CodeShape-K4DIZAIV.js.map +7 -0
  9. package/dist/react/CopyPaste-4CKJRHJ3.js +569 -0
  10. package/dist/react/CopyPaste-4CKJRHJ3.js.map +7 -0
  11. package/dist/react/EraserTrail-PA6IQLQN.js +20 -0
  12. package/dist/react/EraserTrail-PA6IQLQN.js.map +7 -0
  13. package/dist/react/EventDispatcher-SRFH6LQZ.js +463 -0
  14. package/dist/react/EventDispatcher-SRFH6LQZ.js.map +7 -0
  15. package/dist/react/Frame-6UR5XPAX.js +10 -0
  16. package/dist/react/Frame-6UR5XPAX.js.map +7 -0
  17. package/dist/react/FreehandStroke-CK24NUT3.js +8 -0
  18. package/dist/react/FreehandStroke-CK24NUT3.js.map +7 -0
  19. package/dist/react/GraphEngine-QE5B2PMR.js +340 -0
  20. package/dist/react/GraphEngine-QE5B2PMR.js.map +7 -0
  21. package/dist/react/GraphMathParser-2WMTY4IE.js +10 -0
  22. package/dist/react/GraphMathParser-2WMTY4IE.js.map +7 -0
  23. package/dist/react/IconShape-KMIA6T2H.js +10 -0
  24. package/dist/react/IconShape-KMIA6T2H.js.map +7 -0
  25. package/dist/react/ImageShape-CHHGBXUQ.js +10 -0
  26. package/dist/react/ImageShape-CHHGBXUQ.js.map +7 -0
  27. package/dist/react/LayerOrder-XCHF4MSS.js +66 -0
  28. package/dist/react/LayerOrder-XCHF4MSS.js.map +7 -0
  29. package/dist/react/Line-2KOQEEGA.js +10 -0
  30. package/dist/react/Line-2KOQEEGA.js.map +7 -0
  31. package/dist/react/LixScriptParser-JYWJX375.js +1033 -0
  32. package/dist/react/LixScriptParser-JYWJX375.js.map +7 -0
  33. package/dist/react/MermaidFlowchartRenderer-4QBAZ5MB.js +412 -0
  34. package/dist/react/MermaidFlowchartRenderer-4QBAZ5MB.js.map +7 -0
  35. package/dist/react/MermaidSequenceParser-UKG2NSZY.js +8 -0
  36. package/dist/react/MermaidSequenceParser-UKG2NSZY.js.map +7 -0
  37. package/dist/react/MermaidSequenceRenderer-HPLHY5II.js +387 -0
  38. package/dist/react/MermaidSequenceRenderer-HPLHY5II.js.map +7 -0
  39. package/dist/react/Rectangle-6FE7RN6W.js +8 -0
  40. package/dist/react/Rectangle-6FE7RN6W.js.map +7 -0
  41. package/dist/react/ResizeCode-WAMDTKSW.js +2 -0
  42. package/dist/react/ResizeCode-WAMDTKSW.js.map +7 -0
  43. package/dist/react/ResizeShapes-LKEQTITP.js +250 -0
  44. package/dist/react/ResizeShapes-LKEQTITP.js.map +7 -0
  45. package/dist/react/SceneSerializer-4U4B24JM.js +739 -0
  46. package/dist/react/SceneSerializer-4U4B24JM.js.map +7 -0
  47. package/dist/react/Selection-VVUHY5YJ.js +34 -0
  48. package/dist/react/Selection-VVUHY5YJ.js.map +7 -0
  49. package/dist/react/SketchEngine-YA4RG3KX.js +413 -0
  50. package/dist/react/SketchEngine-YA4RG3KX.js.map +7 -0
  51. package/dist/react/TextShape-54ZJ66E3.js +8 -0
  52. package/dist/react/TextShape-54ZJ66E3.js.map +7 -0
  53. package/dist/react/UndoRedo-M2JZUBKN.js +30 -0
  54. package/dist/react/UndoRedo-M2JZUBKN.js.map +7 -0
  55. package/dist/react/ZoomPan-U3TXC5AJ.js +186 -0
  56. package/dist/react/ZoomPan-U3TXC5AJ.js.map +7 -0
  57. package/dist/react/arrowTool-CWEMIRXK.js +17 -0
  58. package/dist/react/arrowTool-CWEMIRXK.js.map +7 -0
  59. package/dist/react/boxicons-5JNJQPUP.woff +0 -0
  60. package/dist/react/boxicons-6NVWQSCQ.eot +0 -0
  61. package/dist/react/boxicons-AQ5N6WP7.woff2 +0 -0
  62. package/dist/react/boxicons-GDR4JU3X.ttf +0 -0
  63. package/dist/react/boxicons-NNFHLMOR.svg +1660 -0
  64. package/dist/react/chunk-2OQXI7QB.js +564 -0
  65. package/dist/react/chunk-2OQXI7QB.js.map +7 -0
  66. package/dist/react/chunk-5OUBTHHO.js +187 -0
  67. package/dist/react/chunk-5OUBTHHO.js.map +7 -0
  68. package/dist/react/chunk-6PFXZKL2.js +628 -0
  69. package/dist/react/chunk-6PFXZKL2.js.map +7 -0
  70. package/dist/react/chunk-6SFNWGGZ.js +453 -0
  71. package/dist/react/chunk-6SFNWGGZ.js.map +7 -0
  72. package/dist/react/chunk-7TZSIR6V.js +210 -0
  73. package/dist/react/chunk-7TZSIR6V.js.map +7 -0
  74. package/dist/react/chunk-AJAZC37E.js +662 -0
  75. package/dist/react/chunk-AJAZC37E.js.map +7 -0
  76. package/dist/react/chunk-BU4D2WYQ.js +1213 -0
  77. package/dist/react/chunk-BU4D2WYQ.js.map +7 -0
  78. package/dist/react/chunk-BZE4CEBG.js +747 -0
  79. package/dist/react/chunk-BZE4CEBG.js.map +7 -0
  80. package/dist/react/chunk-CB2RANCB.js +212 -0
  81. package/dist/react/chunk-CB2RANCB.js.map +7 -0
  82. package/dist/react/chunk-DNAG7ML6.js +128 -0
  83. package/dist/react/chunk-DNAG7ML6.js.map +7 -0
  84. package/dist/react/chunk-HZWLPXFN.js +198 -0
  85. package/dist/react/chunk-HZWLPXFN.js.map +7 -0
  86. package/dist/react/chunk-I6HCCIXB.js +572 -0
  87. package/dist/react/chunk-I6HCCIXB.js.map +7 -0
  88. package/dist/react/chunk-IZ5OGWGW.js +235 -0
  89. package/dist/react/chunk-IZ5OGWGW.js.map +7 -0
  90. package/dist/react/chunk-JJCJNLNN.js +1490 -0
  91. package/dist/react/chunk-JJCJNLNN.js.map +7 -0
  92. package/dist/react/chunk-JQ3IRFDP.js +222 -0
  93. package/dist/react/chunk-JQ3IRFDP.js.map +7 -0
  94. package/dist/react/chunk-L5OARZJ7.js +1134 -0
  95. package/dist/react/chunk-L5OARZJ7.js.map +7 -0
  96. package/dist/react/chunk-LHRTXNVE.js +1600 -0
  97. package/dist/react/chunk-LHRTXNVE.js.map +7 -0
  98. package/dist/react/chunk-Q2GA4MBZ.js +1653 -0
  99. package/dist/react/chunk-Q2GA4MBZ.js.map +7 -0
  100. package/dist/react/chunk-QNSE5BAY.js +519 -0
  101. package/dist/react/chunk-QNSE5BAY.js.map +7 -0
  102. package/dist/react/chunk-QXFBG7OM.js +191 -0
  103. package/dist/react/chunk-QXFBG7OM.js.map +7 -0
  104. package/dist/react/chunk-T6ZVV5ZO.js +714 -0
  105. package/dist/react/chunk-T6ZVV5ZO.js.map +7 -0
  106. package/dist/react/chunk-URB6MY46.js +974 -0
  107. package/dist/react/chunk-URB6MY46.js.map +7 -0
  108. package/dist/react/chunk-VN62KLOC.js +931 -0
  109. package/dist/react/chunk-VN62KLOC.js.map +7 -0
  110. package/dist/react/chunk-VQVEPJSA.js +158 -0
  111. package/dist/react/chunk-VQVEPJSA.js.map +7 -0
  112. package/dist/react/chunk-XISEAGBV.js +1632 -0
  113. package/dist/react/chunk-XISEAGBV.js.map +7 -0
  114. package/dist/react/chunk-YKJUBFHE.js +575 -0
  115. package/dist/react/chunk-YKJUBFHE.js.map +7 -0
  116. package/dist/react/circleTool-NUATSEI2.js +14 -0
  117. package/dist/react/circleTool-NUATSEI2.js.map +7 -0
  118. package/dist/react/codeTool-MHCB2FNB.js +38 -0
  119. package/dist/react/codeTool-MHCB2FNB.js.map +7 -0
  120. package/dist/react/eraserTool-2GS2LQCS.js +103 -0
  121. package/dist/react/eraserTool-2GS2LQCS.js.map +7 -0
  122. package/dist/react/frameTool-PWVKKXSP.js +14 -0
  123. package/dist/react/frameTool-PWVKKXSP.js.map +7 -0
  124. package/dist/react/freehandTool-SLYGASVP.js +14 -0
  125. package/dist/react/freehandTool-SLYGASVP.js.map +7 -0
  126. package/dist/react/iconTool-GPZ7O7NH.js +18 -0
  127. package/dist/react/iconTool-GPZ7O7NH.js.map +7 -0
  128. package/dist/react/imageTool-D5R4XGEU.js +15 -0
  129. package/dist/react/imageTool-D5R4XGEU.js.map +7 -0
  130. package/dist/react/index.js +5342 -0
  131. package/dist/react/index.js.map +7 -0
  132. package/dist/react/laserTool-XVMV63AV.js +239 -0
  133. package/dist/react/laserTool-XVMV63AV.js.map +7 -0
  134. package/dist/react/lineTool-WNI5EMZ5.js +14 -0
  135. package/dist/react/lineTool-WNI5EMZ5.js.map +7 -0
  136. package/dist/react/lixCode-OJMVUXXI.ttf +0 -0
  137. package/dist/react/lixDefault-JEAPAUJL.ttf +0 -0
  138. package/dist/react/lixFancy-BPXYM5CQ.ttf +0 -0
  139. package/dist/react/lixFont-CPKEUDVM.woff2 +0 -0
  140. package/dist/react/rectangleTool-U7DZ6ZTA.js +14 -0
  141. package/dist/react/rectangleTool-U7DZ6ZTA.js.map +7 -0
  142. package/dist/react/styles.css +6627 -0
  143. package/dist/react/textTool-M7S6F3VC.js +21 -0
  144. package/dist/react/textTool-M7S6F3VC.js.map +7 -0
  145. package/package.json +39 -5
  146. package/src/EngineShortcuts.js +83 -27
  147. package/src/core/AIRenderer.js +30 -18
  148. package/src/core/CopyPaste.js +22 -3
  149. package/src/core/GraphEngine.js +64 -41
  150. package/src/core/MermaidFlowchartRenderer.js +153 -88
  151. package/src/core/MermaidSequenceRenderer.js +168 -93
  152. package/src/core/SceneSerializer.js +86 -2
  153. package/src/core/Selection.js +119 -19
  154. package/src/core/UndoRedo.js +82 -15
  155. package/src/core/ZoomPan.js +15 -5
  156. package/src/index.js +25 -1
  157. package/src/react/LixSketchCanvas.jsx +347 -0
  158. package/src/react/components/AppMenu.jsx +475 -0
  159. package/src/react/components/Footer.jsx +85 -0
  160. package/src/react/components/Toolbar.jsx +140 -0
  161. package/src/react/components/canvas/CanvasLoadingOverlay.jsx +76 -0
  162. package/src/react/components/canvas/ContextMenu.jsx +466 -0
  163. package/src/react/components/canvas/FindBar.jsx +177 -0
  164. package/src/react/components/canvas/ImageSourcePicker.jsx +133 -0
  165. package/src/react/components/canvas/MultiSelectActions.jsx +120 -0
  166. package/src/react/components/canvas/SVGCanvas.jsx +145 -0
  167. package/src/react/components/canvas/SaveStatusIndicator.jsx +29 -0
  168. package/src/react/components/modals/CanvasPropertiesModal.jsx +358 -0
  169. package/src/react/components/modals/CommandPalette.jsx +307 -0
  170. package/src/react/components/modals/ExportImageModal.jsx +287 -0
  171. package/src/react/components/modals/GraphModal.jsx +174 -0
  172. package/src/react/components/modals/HelpModal.jsx +208 -0
  173. package/src/react/components/modals/LixScriptModal.jsx +130 -0
  174. package/src/react/components/modals/ShortcutsModal.jsx +125 -0
  175. package/src/react/components/sidebars/ArrowSidebar.jsx +173 -0
  176. package/src/react/components/sidebars/CircleSidebar.jsx +134 -0
  177. package/src/react/components/sidebars/FrameSidebar.jsx +263 -0
  178. package/src/react/components/sidebars/IconSidebar.jsx +279 -0
  179. package/src/react/components/sidebars/ImageSidebar.jsx +69 -0
  180. package/src/react/components/sidebars/LineSidebar.jsx +109 -0
  181. package/src/react/components/sidebars/PaintbrushSidebar.jsx +119 -0
  182. package/src/react/components/sidebars/RectangleSidebar.jsx +165 -0
  183. package/src/react/components/sidebars/ShapeSidebar.jsx +130 -0
  184. package/src/react/components/sidebars/TextSidebar.jsx +174 -0
  185. package/src/react/hooks/inertStores.js +58 -0
  186. package/src/react/hooks/useSketchEngine.js +76 -0
  187. package/src/react/hooks/useTranslation.js +92 -0
  188. package/src/react/index.js +41 -0
  189. package/src/react/store/useSketchStore.js +234 -0
  190. package/src/react/store/useUIStore.js +196 -0
  191. package/src/react/styles/arrowSidebar.css +263 -0
  192. package/src/react/styles/canvasStroke.css +47 -0
  193. package/src/react/styles/canvasTools.css +769 -0
  194. package/src/react/styles/circleSidebar.css +206 -0
  195. package/src/react/styles/fonts/fonts.css +29 -0
  196. package/src/react/styles/fonts/lixCode.ttf +0 -0
  197. package/src/react/styles/fonts/lixDefault.ttf +0 -0
  198. package/src/react/styles/fonts/lixDocs.ttf +0 -0
  199. package/src/react/styles/fonts/lixFancy.ttf +0 -0
  200. package/src/react/styles/fonts/lixFont.woff2 +0 -0
  201. package/src/react/styles/lineSidebar.css +207 -0
  202. package/src/react/styles/paintbrushSidebar.css +247 -0
  203. package/src/react/styles/squareSidebar.css +206 -0
  204. package/src/react/styles/writeText.css +149 -0
  205. package/src/react/styles.css +175 -0
  206. package/src/react/toolsEnum.js +20 -0
  207. package/src/react/utils/toast.js +18 -0
  208. package/src/shapes/Frame.js +7 -1
  209. package/src/shapes/Line.js +11 -5
  210. package/src/tools/arrowTool.js +30 -4
  211. package/src/tools/circleTool.js +33 -9
  212. package/src/tools/codeTool.js +5 -3
  213. package/src/tools/frameTool.js +47 -7
  214. package/src/tools/freehandTool.js +12 -4
  215. package/src/tools/iconTool.js +7 -1
  216. package/src/tools/imageTool.js +56 -22
  217. package/src/tools/lineTool.js +27 -3
  218. package/src/tools/rectangleTool.js +45 -8
  219. package/src/tools/textTool.js +23 -6
  220. package/src/utils/allowedImageTypes.js +53 -0
@@ -4,13 +4,15 @@ import { pushCreateAction, pushDeleteAction, pushOptionsChangeAction, pushTransf
4
4
  import { calculateSnap, clearSnapGuides } from '../core/SnapGuides.js';
5
5
 
6
6
 
7
+ function getThemeStroke() { if (typeof document === "undefined") return "#fff"; return document.body && document.body.classList.contains("theme-dark") ? "#fff" : "#1a1a2e"; }
8
+
7
9
  let arrowStartX, arrowStartY;
8
10
  let currentArrow = null;
9
11
  let isResizing = false;
10
12
  let isDragging = false;
11
13
  let activeAnchor = null;
12
14
  let isDrawingArrow = false;
13
- let arrowStrokeColor = "#fff";
15
+ let arrowStrokeColor = null;
14
16
  let arrowStrokeThickness = 2;
15
17
  let arrowOutlineStyle = "solid";
16
18
  let arrowCurved = "straight";
@@ -68,7 +70,7 @@ const handleMouseDown = (e) => {
68
70
  if (isArrowToolActive) {
69
71
  isDrawingArrow = true;
70
72
  currentArrow = new Arrow({ x, y }, { x, y }, {
71
- stroke: arrowStrokeColor,
73
+ stroke: arrowStrokeColor ?? getThemeStroke(),
72
74
  strokeWidth: arrowStrokeThickness,
73
75
  arrowOutlineStyle: arrowOutlineStyle,
74
76
  arrowHeadStyle: arrowHeadStyle,
@@ -226,6 +228,27 @@ const handleMouseMove = (e) => {
226
228
  startX = x;
227
229
  startY = y;
228
230
 
231
+ // Issue #24 bug #4: track containing frame during drag of an
232
+ // existing arrow so mouseUp re-parents correctly. Without this
233
+ // hoveredFrameArrow stays null and the arrow is detached the
234
+ // moment the user nudges it inside its own parent frame.
235
+ let newHover = null;
236
+ for (const f of shapes) {
237
+ if (f.shapeName !== 'frame') continue;
238
+ if (f === currentShape) continue;
239
+ if (typeof f.isShapeInFrame === 'function' && f.isShapeInFrame(currentShape)) {
240
+ newHover = f;
241
+ break;
242
+ }
243
+ }
244
+ if (hoveredFrameArrow && hoveredFrameArrow !== newHover && typeof hoveredFrameArrow.removeHighlight === 'function') {
245
+ hoveredFrameArrow.removeHighlight();
246
+ }
247
+ if (newHover && newHover !== hoveredFrameArrow && typeof newHover.highlightFrame === 'function') {
248
+ newHover.highlightFrame();
249
+ }
250
+ hoveredFrameArrow = newHover;
251
+
229
252
  // Snap guides
230
253
  if (window.__sketchStoreApi && window.__sketchStoreApi.getState().snapToObjects) {
231
254
  const snap = calculateSnap(currentShape, e.shiftKey, e.clientX, e.clientY);
@@ -319,10 +342,13 @@ if (isDrawingArrow && currentArrow) {
319
342
  }
320
343
 
321
344
  if (isDragging && dragOldPosArrow && currentShape) {
345
+ // Issue #34 bug #2: containment transfer happens later in this block,
346
+ // so currentShape.parentFrame is still the OLD frame here. hoveredFrameArrow
347
+ // tracks the actual destination during mousemove — use that as the truth.
322
348
  const newPos = {
323
349
  startPoint: { x: currentShape.startPoint.x, y: currentShape.startPoint.y },
324
350
  endPoint: { x: currentShape.endPoint.x, y: currentShape.endPoint.y },
325
- parentFrame: currentShape.parentFrame // Add this line
351
+ parentFrame: hoveredFrameArrow || null,
326
352
  };
327
353
  const oldPos = {
328
354
  ...dragOldPosArrow,
@@ -578,4 +604,4 @@ export {
578
604
  handleMouseUp as handleMouseUpArrow,
579
605
  cleanupAttachments,
580
606
  updateAttachedArrows
581
- };
607
+ };
@@ -4,6 +4,8 @@ import { pushCreateAction, pushDeleteAction, pushOptionsChangeAction, pushTransf
4
4
  import { cleanupAttachments } from './arrowTool.js';
5
5
  import { calculateSnap, clearSnapGuides } from '../core/SnapGuides.js';
6
6
 
7
+ function getThemeStroke() { if (typeof document === "undefined") return "#fff"; return document.body && document.body.classList.contains("theme-dark") ? "#fff" : "#1a1a2e"; }
8
+
7
9
  let isDrawingCircle = false;
8
10
  let isDraggingShapeCircle = false;
9
11
  let isResizingShapeCircle = false;
@@ -16,7 +18,7 @@ const rc = rough.svg(svg);
16
18
  let startX, startY;
17
19
 
18
20
 
19
- let circleStrokecolor = "#fff";
21
+ let circleStrokecolor = null;
20
22
  let circleBackgroundColor = "transparent";
21
23
  let circleFillStyleValue = "none";
22
24
  let circleStrokeThicknes = 2;
@@ -83,7 +85,7 @@ const handleMouseDown = (e) => {
83
85
  disableAllSideBars();
84
86
  }
85
87
  let initialOptions = {
86
- stroke: circleStrokecolor,
88
+ stroke: circleStrokecolor ?? getThemeStroke(),
87
89
  fill: circleBackgroundColor,
88
90
  fillStyle: circleFillStyleValue,
89
91
  strokeWidth: circleStrokeThicknes,
@@ -261,6 +263,25 @@ const handleMouseMove = (e) => {
261
263
  } else {
262
264
  clearSnapGuides();
263
265
  }
266
+
267
+ // Issue #24 bug #4: track containing frame during drag (see same
268
+ // block in rectangleTool for the rationale).
269
+ let newHover = null;
270
+ for (const f of shapes) {
271
+ if (f.shapeName !== 'frame') continue;
272
+ if (f === currentShape) continue;
273
+ if (typeof f.isShapeInFrame === 'function' && f.isShapeInFrame(currentShape)) {
274
+ newHover = f;
275
+ break;
276
+ }
277
+ }
278
+ if (hoveredFrameCircle && hoveredFrameCircle !== newHover && typeof hoveredFrameCircle.removeHighlight === 'function') {
279
+ hoveredFrameCircle.removeHighlight();
280
+ }
281
+ if (newHover && newHover !== hoveredFrameCircle && typeof newHover.highlightFrame === 'function') {
282
+ newHover.highlightFrame();
283
+ }
284
+ hoveredFrameCircle = newHover;
264
285
  }
265
286
 
266
287
  else if(isResizingShapeCircle && currentShape && currentShape.isSelected)
@@ -374,13 +395,16 @@ const handleMouseUp = (e) => {
374
395
  }
375
396
 
376
397
  if((isDraggingShapeCircle || isResizingShapeCircle || isRotatingShapeCircle) && dragOldPosCircle && currentShape) {
377
- const newPos = {
378
- x: currentShape.x,
379
- y: currentShape.y,
380
- rx: currentShape.rx,
381
- ry: currentShape.ry,
398
+ // Issue #34 bug #2: see rectangleTool — record the hovered frame
399
+ // as the destination, not the stale `parentFrame` (containment
400
+ // transfer happens later in this handler).
401
+ const newPos = {
402
+ x: currentShape.x,
403
+ y: currentShape.y,
404
+ rx: currentShape.rx,
405
+ ry: currentShape.ry,
382
406
  rotation: currentShape.rotation,
383
- parentFrame: currentShape.parentFrame
407
+ parentFrame: isDraggingShapeCircle ? (hoveredFrameCircle || null) : currentShape.parentFrame,
384
408
  };
385
409
  const oldPos = {
386
410
  ...dragOldPosCircle,
@@ -616,4 +640,4 @@ export {
616
640
  handleMouseDown as handleMouseDownCircle,
617
641
  handleMouseMove as handleMouseMoveCircle,
618
642
  handleMouseUp as handleMouseUpCircle,
619
- }
643
+ }
@@ -11,9 +11,11 @@ import {
11
11
  } from '../core/UndoRedo.js';
12
12
  import { cleanupAttachments, updateAttachedArrows } from './arrowTool.js';
13
13
 
14
+ function getThemeStroke() { if (typeof document === "undefined") return "#fff"; return document.body && document.body.classList.contains("theme-dark") ? "#fff" : "#1a1a2e"; }
15
+
14
16
  let codeTextSize = "25px";
15
17
  let codeTextFont = "lixCode";
16
- let codeTextColor = "#fff";
18
+ let codeTextColor = null;
17
19
  let codeTextAlign = "left";
18
20
  let codeLanguage = "auto";
19
21
 
@@ -118,7 +120,7 @@ function addCodeBlock(event) {
118
120
 
119
121
  codeElement.setAttribute("x", 0);
120
122
  codeElement.setAttribute("y", 0);
121
- codeElement.setAttribute("fill", codeTextColor);
123
+ codeElement.setAttribute("fill", codeTextColor ?? getThemeStroke());
122
124
  codeElement.setAttribute("font-size", codeTextSize);
123
125
  codeElement.setAttribute("font-family", codeTextFont);
124
126
  codeElement.setAttribute("text-anchor", textAlignElement);
@@ -131,7 +133,7 @@ function addCodeBlock(event) {
131
133
  gElement.setAttribute("data-y", y);
132
134
  codeElement.setAttribute("data-initial-size", codeTextSize);
133
135
  codeElement.setAttribute("data-initial-font", codeTextFont);
134
- codeElement.setAttribute("data-initial-color", codeTextColor);
136
+ codeElement.setAttribute("data-initial-color", codeTextColor ?? getThemeStroke());
135
137
  codeElement.setAttribute("data-initial-align", codeTextAlign);
136
138
  codeElement.setAttribute("data-language", codeLanguage);
137
139
  codeElement.setAttribute("data-type", "code");
@@ -61,9 +61,21 @@ const handleMouseDown = (e) => {
61
61
  }
62
62
  }
63
63
  }
64
- // If a contained shape was clicked, don't select the frame — let the
65
- // EventDispatcher fall through to shape-specific handlers
66
- if (clickedContainedShape) return;
64
+ // If a contained shape was clicked, don't select the frame —
65
+ // and crucially, clear `currentShape` so the EventDispatcher's
66
+ // `if (handled && prevShape && !currentShape) handled=false` gate
67
+ // triggers and the dispatcher falls through to shape-specific
68
+ // handlers. Without this clear, the dispatcher returns with
69
+ // `handled=true` and the child shape is never selected (Issue
70
+ // #22, bug #2).
71
+ if (clickedContainedShape) {
72
+ if (currentShape && currentShape.shapeName === 'frame'
73
+ && typeof currentShape.removeSelection === 'function') {
74
+ currentShape.removeSelection();
75
+ }
76
+ currentShape = null;
77
+ return;
78
+ }
67
79
 
68
80
  if (currentShape && currentShape !== clickedFrame) {
69
81
  currentShape.removeSelection();
@@ -216,12 +228,40 @@ Frame.prototype.isNearAnchor = function(x, y) {
216
228
  // Delete functionality
217
229
  function deleteCurrentFrame() {
218
230
  if (currentShape && currentShape.shapeName === 'frame') {
219
- const idx = shapes.indexOf(currentShape);
231
+ const frame = currentShape;
232
+ const isDiagramFrame = !!frame._diagramType;
233
+
234
+ // Issue #24 bug #7: regular frame → orphan children + snapshot
235
+ // (mirrors deleteSelectedShapes / EngineShortcuts).
236
+ let childSnapshot = null;
237
+ if (!isDiagramFrame) {
238
+ childSnapshot = [...(frame.containedShapes || [])];
239
+ for (const child of childSnapshot) {
240
+ if (!child || child === frame) continue;
241
+ const el = child.group || child.element;
242
+ if (el) {
243
+ if (frame.clipGroup && el.parentNode === frame.clipGroup) frame.clipGroup.removeChild(el);
244
+ if (window.svg && el.parentNode !== window.svg) window.svg.appendChild(el);
245
+ }
246
+ child.parentFrame = null;
247
+ delete child.isBeingMovedByFrame;
248
+ }
249
+ frame.containedShapes = [];
250
+ }
251
+
252
+ const idx = shapes.indexOf(frame);
220
253
  if (idx !== -1) shapes.splice(idx, 1);
221
- if (currentShape.group.parentNode) {
222
- currentShape.group.parentNode.removeChild(currentShape.group);
254
+
255
+ if (!isDiagramFrame) {
256
+ if (frame.group && frame.group.parentNode) frame.group.parentNode.removeChild(frame.group);
257
+ if (frame.clipGroup && frame.clipGroup.parentNode) frame.clipGroup.parentNode.removeChild(frame.clipGroup);
258
+ if (frame.clipPath && frame.clipPath.parentNode) frame.clipPath.parentNode.removeChild(frame.clipPath);
259
+ } else if (frame.group && frame.group.parentNode) {
260
+ frame.group.parentNode.removeChild(frame.group);
223
261
  }
224
- pushDeleteAction(currentShape);
262
+
263
+ if (childSnapshot) pushDeleteAction(frame, { childSnapshot });
264
+ else pushDeleteAction(frame);
225
265
  currentShape = null;
226
266
  }
227
267
  }
@@ -4,6 +4,8 @@ import { pushCreateAction, pushDeleteAction, pushOptionsChangeAction, pushTransf
4
4
  import { updateAttachedArrows as updateArrowsForShape, cleanupAttachments } from './arrowTool.js';
5
5
  import { calculateSnap, clearSnapGuides } from '../core/SnapGuides.js';
6
6
 
7
+ function getThemeStroke() { if (typeof document === "undefined") return "#fff"; return document.body && document.body.classList.contains("theme-dark") ? "#fff" : "#1a1a2e"; }
8
+
7
9
  // Expose updateArrowsForShape globally so FreehandStroke.updateAttachedArrows() can call it
8
10
  // (FreehandStroke.js is a separate module and cannot import freehandTool's local bindings)
9
11
  window.__updateArrowsForShape = updateArrowsForShape;
@@ -13,7 +15,7 @@ const strokeThicknesses = document.querySelectorAll(".strokeThickness span");
13
15
  const strokeStyles = document.querySelectorAll(".strokeStyleSpan");
14
16
  const strokeTapers = document.querySelectorAll(".strokeTaperSpan");
15
17
  const strokeRoughnesses = document.querySelectorAll(".strokeRoughnessSpan");
16
- let strokeColor = "#fff";
18
+ let strokeColor = null;
17
19
  let strokeThickness = 2;
18
20
  let strokeStyleValue = "solid";
19
21
  let strokeThinning = 0;
@@ -140,7 +142,7 @@ function handleMouseDown(e) {
140
142
  lastTime = Date.now();
141
143
 
142
144
  currentStroke = new FreehandStroke(points, {
143
- stroke: strokeColor,
145
+ stroke: strokeColor ?? getThemeStroke(),
144
146
  strokeWidth: strokeThickness
145
147
  });
146
148
 
@@ -411,9 +413,15 @@ function handleMouseUp(e) {
411
413
  ...dragOldPosStroke,
412
414
  parentFrame: draggedShapeInitialFrameStroke
413
415
  };
416
+ // Issue #34 bug #2: for drag, hoveredFrameStroke is the actual
417
+ // destination — currentShape.parentFrame at this point is still
418
+ // the OLD frame. Resize / rotate don't track hover, fall back to
419
+ // the shape's own parent.
414
420
  const newPosForUndo = {
415
421
  ...newPos,
416
- parentFrame: currentShape.parentFrame
422
+ parentFrame: isDraggingStroke
423
+ ? (hoveredFrameStroke || null)
424
+ : currentShape.parentFrame,
417
425
  };
418
426
 
419
427
  const frameChanged = oldPos.parentFrame !== newPosForUndo.parentFrame;
@@ -659,4 +667,4 @@ export
659
667
  handleMouseDown as handleFreehandMouseDown,
660
668
  handleMouseMove as handleFreehandMouseMove,
661
669
  handleMouseUp as handleFreehandMouseUp,
662
- }
670
+ }
@@ -1008,9 +1008,15 @@ function stopInteracting() {
1008
1008
  ...oldPos,
1009
1009
  parentFrame: draggedShapeInitialFrameIcon
1010
1010
  };
1011
+ // Issue #34 bug #2: for drag, hoveredFrameIcon tracks the actual
1012
+ // destination — currentShape.parentFrame is still the OLD frame.
1013
+ // Resize / rotate don't track hover, so fall back to whatever the
1014
+ // shape itself reports (parent doesn't change for those gestures).
1011
1015
  const newPosWithFrame = {
1012
1016
  ...newPos,
1013
- parentFrame: iconShape ? iconShape.parentFrame : null
1017
+ parentFrame: isDragging
1018
+ ? (hoveredFrameIcon || null)
1019
+ : (iconShape ? iconShape.parentFrame : null),
1014
1020
  };
1015
1021
 
1016
1022
  const stateChanged = newPos.x !== oldPos.x || newPos.y !== oldPos.y ||
@@ -3,6 +3,7 @@
3
3
  import { pushCreateAction, pushDeleteAction, pushTransformAction, pushFrameAttachmentAction } from '../core/UndoRedo.js';
4
4
  import { updateAttachedArrows as updateArrowsForShape, cleanupAttachments } from './arrowTool.js';
5
5
  import { compressImage } from '../utils/imageCompressor.js';
6
+ import { isAllowedImage, IMAGE_ACCEPT_ATTR } from '../utils/allowedImageTypes.js';
6
7
 
7
8
 
8
9
  let isDraggingImage = false;
@@ -133,7 +134,7 @@ document.getElementById("importImage")?.addEventListener('click', () => {
133
134
  // Create a file input element
134
135
  const fileInput = document.createElement('input');
135
136
  fileInput.type = 'file';
136
- fileInput.accept = 'image/*'; // Accept all image types
137
+ fileInput.accept = IMAGE_ACCEPT_ATTR; // Static images only — see allowedImageTypes.js
137
138
  fileInput.style.display = 'none'; // Hide the input element
138
139
 
139
140
  // Add the input to the document temporarily
@@ -194,13 +195,16 @@ const loadHardcodedImage = (imagePath) => {
194
195
  img.src = imagePath;
195
196
  };
196
197
 
197
- const handleImageUpload = (file) => {
198
+ const handleImageUpload = async (file) => {
198
199
  if (!file || !isImageToolActive) return;
199
200
 
200
- // Validate file type
201
- if (!file.type.startsWith('image/')) {
202
- console.error('Selected file is not an image');
203
- alert('Please select a valid image file.');
201
+ // Validate file type against the canonical allowlist (avif, jpeg, jpg,
202
+ // png, bmp, svg, webp). Animated GIF, HEIC, TIFF, video, audio, and
203
+ // arbitrary files are rejected here.
204
+ if (!isAllowedImage(file)) {
205
+ console.error('Rejected file type:', file.type, file.name);
206
+ alert('Unsupported file type. Allowed: AVIF, JPEG, PNG, BMP, SVG, WebP.');
207
+ isImageToolActive = false;
204
208
  return;
205
209
  }
206
210
 
@@ -222,26 +226,50 @@ const handleImageUpload = (file) => {
222
226
 
223
227
  console.log('Processing image file:', file.name, 'Size:', file.size, 'Type:', file.type);
224
228
 
225
- const reader = new FileReader();
226
-
227
- // Store file size for room limit tracking
228
- window.__pendingImageFileSize = file.size;
229
+ // Read optionally compress → place. We compress client-side BEFORE
230
+ // placement so that consumers without an upload pipeline (VS Code
231
+ // extension, offline npm usage) still embed a compressed data URL
232
+ // rather than the original. SVGs are passed through unchanged because
233
+ // rasterizing them would destroy their vector fidelity.
234
+ try {
235
+ const rawDataUrl = await readFileAsDataUrl(file);
236
+ const isSvg = (file.type || '').toLowerCase() === 'image/svg+xml'
237
+ || (file.name || '').toLowerCase().endsWith('.svg');
238
+
239
+ let placedDataUrl = rawDataUrl;
240
+ let placedSize = file.size;
241
+ if (!isSvg) {
242
+ try {
243
+ const compressed = await compressImage(rawDataUrl);
244
+ if (compressed?.dataUrl) {
245
+ placedDataUrl = compressed.dataUrl;
246
+ placedSize = compressed.compressedSize || placedSize;
247
+ }
248
+ } catch (err) {
249
+ console.warn('[ImageTool] Pre-placement compression failed, using raw:', err);
250
+ }
251
+ }
229
252
 
230
- reader.onload = (e) => {
231
- imageToPlace = e.target.result;
253
+ window.__pendingImageFileSize = placedSize;
254
+ imageToPlace = placedDataUrl;
232
255
  isDraggingImage = true;
233
- console.log('Image loaded and ready to place');
234
- };
235
-
236
- reader.onerror = (error) => {
237
- console.error('Error reading file:', error);
256
+ console.log('Image loaded and ready to place', { size: placedSize, isSvg });
257
+ } catch (err) {
258
+ console.error('Error reading file:', err);
238
259
  alert('Error reading the image file. Please try again.');
239
260
  isImageToolActive = false;
240
- };
241
-
242
- reader.readAsDataURL(file);
261
+ }
243
262
  };
244
263
 
264
+ function readFileAsDataUrl(file) {
265
+ return new Promise((resolve, reject) => {
266
+ const reader = new FileReader();
267
+ reader.onload = (e) => resolve(e.target.result);
268
+ reader.onerror = (err) => reject(err);
269
+ reader.readAsDataURL(file);
270
+ });
271
+ }
272
+
245
273
  // Add coordinate conversion function like in other tools
246
274
  function getSVGCoordsFromMouse(e) {
247
275
  const viewBox = svg.viewBox.baseVal;
@@ -1089,9 +1117,15 @@ function stopInteracting() {
1089
1117
  ...oldPos,
1090
1118
  parentFrame: draggedShapeInitialFrameImage
1091
1119
  };
1120
+ // Issue #34 bug #2: for drag, hoveredFrameImage tracks the actual
1121
+ // destination — imageShape.parentFrame is still the OLD frame here.
1122
+ // Resize / rotate don't move between frames, so fall back to the
1123
+ // shape's own parent for those.
1092
1124
  const newPosWithFrame = {
1093
1125
  ...newPos,
1094
- parentFrame: imageShape ? imageShape.parentFrame : null
1126
+ parentFrame: isDragging
1127
+ ? (hoveredFrameImage || null)
1128
+ : (imageShape ? imageShape.parentFrame : null),
1095
1129
  };
1096
1130
 
1097
1131
  // Only push transform action if something actually changed
@@ -1286,7 +1320,7 @@ window.openImageFilePicker = function() {
1286
1320
  isImageToolActive = true;
1287
1321
  const fileInput = document.createElement('input');
1288
1322
  fileInput.type = 'file';
1289
- fileInput.accept = 'image/*';
1323
+ fileInput.accept = IMAGE_ACCEPT_ATTR;
1290
1324
  fileInput.style.display = 'none';
1291
1325
  document.body.appendChild(fileInput);
1292
1326
 
@@ -4,12 +4,14 @@ import { pushCreateAction, pushDeleteAction, pushOptionsChangeAction, pushTransf
4
4
  import { updateAttachedArrows as updateArrowsForShape, cleanupAttachments } from './arrowTool.js';
5
5
  import { calculateSnap, clearSnapGuides } from '../core/SnapGuides.js';
6
6
 
7
+ function getThemeStroke() { if (typeof document === "undefined") return "#fff"; return document.body && document.body.classList.contains("theme-dark") ? "#fff" : "#1a1a2e"; }
8
+
7
9
  let isDrawingLine = false;
8
10
  let currentLine = null;
9
11
  let lineStartX = 0;
10
12
  let lineStartY = 0;
11
13
  let currentLineGroup = null;
12
- let lineColor = "#fff";
14
+ let lineColor = null;
13
15
  let lineStrokeWidth = 3;
14
16
  let lineStrokeStyle = "solid";
15
17
  let lineEdgeType = 1;
@@ -71,7 +73,7 @@ const handleMouseDown = (e) => {
71
73
  { x, y },
72
74
  { x, y },
73
75
  {
74
- stroke: lineColor,
76
+ stroke: lineColor ?? getThemeStroke(),
75
77
  strokeWidth: lineStrokeWidth,
76
78
  roughness: lineSktetchRate,
77
79
  bowing: lineEdgeType,
@@ -238,6 +240,26 @@ const handleMouseMove = (e) => {
238
240
  startX = x;
239
241
  startY = y;
240
242
 
243
+ // Issue #24 bug #4: track containing frame during existing-line
244
+ // drag so mouseUp re-parents from current geometry, not from a
245
+ // stale hoveredFrameLine.
246
+ let newHover = null;
247
+ for (const f of shapes) {
248
+ if (f.shapeName !== 'frame') continue;
249
+ if (f === currentShape) continue;
250
+ if (typeof f.isShapeInFrame === 'function' && f.isShapeInFrame(currentShape)) {
251
+ newHover = f;
252
+ break;
253
+ }
254
+ }
255
+ if (hoveredFrameLine && hoveredFrameLine !== newHover && typeof hoveredFrameLine.removeHighlight === 'function') {
256
+ hoveredFrameLine.removeHighlight();
257
+ }
258
+ if (newHover && newHover !== hoveredFrameLine && typeof newHover.highlightFrame === 'function') {
259
+ newHover.highlightFrame();
260
+ }
261
+ hoveredFrameLine = newHover;
262
+
241
263
  // Snap guides
242
264
  if (window.__sketchStoreApi && window.__sketchStoreApi.getState().snapToObjects) {
243
265
  const snap = calculateSnap(currentShape, e.shiftKey, e.clientX, e.clientY);
@@ -299,11 +321,13 @@ const handleMouseUp = (e) => {
299
321
  }
300
322
 
301
323
  if (isDraggingLine && dragOldPosLine && currentShape) {
324
+ // Issue #34 bug #2: use hoveredFrameLine (actual destination) instead
325
+ // of the stale parentFrame — containment transfer hasn't happened yet.
302
326
  const newPos = {
303
327
  startPoint: { x: currentShape.startPoint.x, y: currentShape.startPoint.y },
304
328
  endPoint: { x: currentShape.endPoint.x, y: currentShape.endPoint.y },
305
329
  controlPoint: currentShape.controlPoint ? { x: currentShape.controlPoint.x, y: currentShape.controlPoint.y } : null,
306
- parentFrame: currentShape.parentFrame
330
+ parentFrame: hoveredFrameLine || null,
307
331
  };
308
332
  const oldPos = {
309
333
  ...dragOldPosLine,
@@ -16,7 +16,16 @@ let startRotationMouseAngleSquare = 0;
16
16
  let startShapeRotationSquare = 0;
17
17
  const rc = rough.svg(svg);
18
18
  let startX, startY;
19
- let squareStrokecolor = "#fff";
19
+ // Issue #38 follow-up: null sentinel means "use theme default at draw
20
+ // time". The legacy `"#fff"` literal was invisible on the light canvas;
21
+ // reading the body's `theme-dark` class lets the same code produce a
22
+ // readable stroke in either mode. As soon as the user picks a colour
23
+ // from the sidebar this var holds that value verbatim.
24
+ function getThemeStroke() {
25
+ if (typeof document === 'undefined') return '#fff';
26
+ return document.body && document.body.classList.contains('theme-dark') ? '#fff' : '#1a1a2e';
27
+ }
28
+ let squareStrokecolor = null;
20
29
  let squareBackgroundColor = "transparent";
21
30
  let squareFillStyleValue = "none";
22
31
  let squareStrokeThicknes = 2;
@@ -76,7 +85,7 @@ const handleMouseDownRect = (e) => {
76
85
  }
77
86
 
78
87
  let initialOptions = {
79
- stroke: squareStrokecolor,
88
+ stroke: squareStrokecolor ?? getThemeStroke(),
80
89
  fill: squareBackgroundColor,
81
90
  fillStyle: squareFillStyleValue,
82
91
  strokeWidth: squareStrokeThicknes,
@@ -241,6 +250,28 @@ const handleMouseMoveRect = (e) => {
241
250
  } else {
242
251
  clearSnapGuides();
243
252
  }
253
+
254
+ // Issue #24 bug #4: track the actual containing frame at every
255
+ // move so the mouseUp logic re-parents correctly. Without this
256
+ // hoveredFrame stays at null for existing-shape drags and the
257
+ // shape is detached the moment the user nudges it by 1 px inside
258
+ // its own parent frame.
259
+ let newHover = null;
260
+ for (const f of shapes) {
261
+ if (f.shapeName !== 'frame') continue;
262
+ if (f === currentShape) continue;
263
+ if (typeof f.isShapeInFrame === 'function' && f.isShapeInFrame(currentShape)) {
264
+ newHover = f;
265
+ break;
266
+ }
267
+ }
268
+ if (hoveredFrame && hoveredFrame !== newHover && typeof hoveredFrame.removeHighlight === 'function') {
269
+ hoveredFrame.removeHighlight();
270
+ }
271
+ if (newHover && newHover !== hoveredFrame && typeof newHover.highlightFrame === 'function') {
272
+ newHover.highlightFrame();
273
+ }
274
+ hoveredFrame = newHover;
244
275
  } else if (isResizingShapeSquare && currentShape && currentShape.isSelected && resizingAnchorIndexSquare !== null) {
245
276
  currentShape.updatePosition(resizingAnchorIndexSquare, mouseX, mouseY);
246
277
  currentShape._skipAnchors = true;
@@ -341,13 +372,19 @@ const handleMouseUpRect = (e) => {
341
372
  }
342
373
 
343
374
  if ((isDraggingShapeSquare || isResizingShapeSquare || isRotatingShapeSquare) && dragOldPosSquare && currentShape) {
344
- const newPos = {
345
- x: currentShape.x,
346
- y: currentShape.y,
347
- width: currentShape.width,
348
- height: currentShape.height,
375
+ // Issue #34 bug #2: for drag operations the actual frame transfer
376
+ // happens AFTER this mouseUp block — so `currentShape.parentFrame`
377
+ // at this point still points at the OLD frame. Use the hovered
378
+ // frame (tracked during mousemove) as the authoritative new
379
+ // destination instead, so the undo action records a real
380
+ // attachment delta and Ctrl+Z re-attaches the shape correctly.
381
+ const newPos = {
382
+ x: currentShape.x,
383
+ y: currentShape.y,
384
+ width: currentShape.width,
385
+ height: currentShape.height,
349
386
  rotation: currentShape.rotation,
350
- parentFrame: currentShape.parentFrame
387
+ parentFrame: isDraggingShapeSquare ? (hoveredFrame || null) : currentShape.parentFrame,
351
388
  };
352
389
  const oldPos = {
353
390
  ...dragOldPosSquare,