@elixpo/lixsketch 5.4.7 → 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 (218) 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/GraphEngine.js +64 -41
  149. package/src/core/MermaidFlowchartRenderer.js +153 -88
  150. package/src/core/MermaidSequenceRenderer.js +168 -93
  151. package/src/core/SceneSerializer.js +86 -2
  152. package/src/core/Selection.js +119 -19
  153. package/src/core/UndoRedo.js +82 -15
  154. package/src/core/ZoomPan.js +15 -5
  155. package/src/index.js +14 -1
  156. package/src/react/LixSketchCanvas.jsx +347 -0
  157. package/src/react/components/AppMenu.jsx +475 -0
  158. package/src/react/components/Footer.jsx +85 -0
  159. package/src/react/components/Toolbar.jsx +140 -0
  160. package/src/react/components/canvas/CanvasLoadingOverlay.jsx +76 -0
  161. package/src/react/components/canvas/ContextMenu.jsx +466 -0
  162. package/src/react/components/canvas/FindBar.jsx +177 -0
  163. package/src/react/components/canvas/ImageSourcePicker.jsx +133 -0
  164. package/src/react/components/canvas/MultiSelectActions.jsx +120 -0
  165. package/src/react/components/canvas/SVGCanvas.jsx +145 -0
  166. package/src/react/components/canvas/SaveStatusIndicator.jsx +29 -0
  167. package/src/react/components/modals/CanvasPropertiesModal.jsx +358 -0
  168. package/src/react/components/modals/CommandPalette.jsx +307 -0
  169. package/src/react/components/modals/ExportImageModal.jsx +287 -0
  170. package/src/react/components/modals/GraphModal.jsx +174 -0
  171. package/src/react/components/modals/HelpModal.jsx +208 -0
  172. package/src/react/components/modals/LixScriptModal.jsx +130 -0
  173. package/src/react/components/modals/ShortcutsModal.jsx +125 -0
  174. package/src/react/components/sidebars/ArrowSidebar.jsx +173 -0
  175. package/src/react/components/sidebars/CircleSidebar.jsx +134 -0
  176. package/src/react/components/sidebars/FrameSidebar.jsx +263 -0
  177. package/src/react/components/sidebars/IconSidebar.jsx +279 -0
  178. package/src/react/components/sidebars/ImageSidebar.jsx +69 -0
  179. package/src/react/components/sidebars/LineSidebar.jsx +109 -0
  180. package/src/react/components/sidebars/PaintbrushSidebar.jsx +119 -0
  181. package/src/react/components/sidebars/RectangleSidebar.jsx +165 -0
  182. package/src/react/components/sidebars/ShapeSidebar.jsx +130 -0
  183. package/src/react/components/sidebars/TextSidebar.jsx +174 -0
  184. package/src/react/hooks/inertStores.js +58 -0
  185. package/src/react/hooks/useSketchEngine.js +76 -0
  186. package/src/react/hooks/useTranslation.js +92 -0
  187. package/src/react/index.js +41 -0
  188. package/src/react/store/useSketchStore.js +234 -0
  189. package/src/react/store/useUIStore.js +196 -0
  190. package/src/react/styles/arrowSidebar.css +263 -0
  191. package/src/react/styles/canvasStroke.css +47 -0
  192. package/src/react/styles/canvasTools.css +769 -0
  193. package/src/react/styles/circleSidebar.css +206 -0
  194. package/src/react/styles/fonts/fonts.css +29 -0
  195. package/src/react/styles/fonts/lixCode.ttf +0 -0
  196. package/src/react/styles/fonts/lixDefault.ttf +0 -0
  197. package/src/react/styles/fonts/lixDocs.ttf +0 -0
  198. package/src/react/styles/fonts/lixFancy.ttf +0 -0
  199. package/src/react/styles/fonts/lixFont.woff2 +0 -0
  200. package/src/react/styles/lineSidebar.css +207 -0
  201. package/src/react/styles/paintbrushSidebar.css +247 -0
  202. package/src/react/styles/squareSidebar.css +206 -0
  203. package/src/react/styles/writeText.css +149 -0
  204. package/src/react/styles.css +175 -0
  205. package/src/react/toolsEnum.js +20 -0
  206. package/src/react/utils/toast.js +18 -0
  207. package/src/shapes/Frame.js +7 -1
  208. package/src/shapes/Line.js +11 -5
  209. package/src/tools/arrowTool.js +30 -4
  210. package/src/tools/circleTool.js +33 -9
  211. package/src/tools/codeTool.js +5 -3
  212. package/src/tools/frameTool.js +47 -7
  213. package/src/tools/freehandTool.js +12 -4
  214. package/src/tools/iconTool.js +7 -1
  215. package/src/tools/imageTool.js +7 -1
  216. package/src/tools/lineTool.js +27 -3
  217. package/src/tools/rectangleTool.js +45 -8
  218. package/src/tools/textTool.js +23 -6
@@ -0,0 +1,140 @@
1
+ "use client"
2
+
3
+ import useSketchStore, { TOOLS } from '../store/useSketchStore'
4
+ import useUIStore from '../store/useUIStore'
5
+
6
+ // Items are built lazily inside the component body so esbuild's lazy ESM
7
+ // wrapper can't evaluate them before the store module finishes init.
8
+ function getViewModeItems() {
9
+ return [
10
+ { tool: TOOLS.PAN, icon: 'bxs-hand', title: 'Pan (H)', key: 'H' },
11
+ ]
12
+ }
13
+
14
+ function getToolItems() {
15
+ return [
16
+ { tool: TOOLS.PAN, icon: 'bxs-hand', title: 'Pan (H)', key: 'H' },
17
+ { tool: TOOLS.SELECT, icon: 'bxs-pointer', title: 'Select (V)', key: 'V' },
18
+ 'spacer',
19
+ { tool: TOOLS.RECTANGLE, icon: 'bx-square', title: 'Rectangle (R)', key: 'R' },
20
+ { tool: TOOLS.CIRCLE, icon: 'bx-circle', title: 'Circle (O)', key: 'O' },
21
+ { tool: TOOLS.LINE, icon: 'bx-minus', title: 'Line (L)', key: 'L' },
22
+ { tool: TOOLS.ARROW, icon: 'bx-right-arrow-alt', title: 'Arrow (A)', rotate: true, key: 'A' },
23
+ { tool: TOOLS.TEXT, icon: 'bx-text', title: 'Text (T)', key: 'T' },
24
+ { tool: TOOLS.FREEHAND, icon: 'bx-pen', title: 'Freehand (P)', key: 'P' },
25
+ { tool: TOOLS.IMAGE, icon: 'bx-image-alt', title: 'Image (9)', key: '9' },
26
+ { tool: TOOLS.ICON, icon: 'bx-wink-smile', title: 'Icon (I)', key: 'I' },
27
+ 'spacer',
28
+ { tool: TOOLS.FRAME, icon: 'bx-crop', title: 'Frame (F)', key: 'F' },
29
+ { tool: TOOLS.LASER, icon: 'bxs-magic-wand', title: 'Laser (K)', key: 'K' },
30
+ { tool: TOOLS.ERASER, icon: 'bxs-eraser', title: 'Eraser (E)', key: 'E' },
31
+ // AI tool entry removed while the assistant is coming-soon.
32
+ // Restore `{ tool: 'ai', icon: null, title: 'AI', isAI: true }` here
33
+ // when the modal becomes a real feature again.
34
+ ]
35
+ }
36
+
37
+ export default function Toolbar() {
38
+ const activeTool = useSketchStore((s) => s.activeTool)
39
+ const setActiveTool = useSketchStore((s) => s.setActiveTool)
40
+ const viewMode = useSketchStore((s) => s.viewMode)
41
+ const toolLock = useSketchStore((s) => s.toolLock)
42
+ const toggleToolLock = useSketchStore((s) => s.toggleToolLock)
43
+ const toggleAIModal = useUIStore((s) => s.toggleAIModal)
44
+
45
+ const items = viewMode ? getViewModeItems() : getToolItems()
46
+
47
+ return (
48
+ <>
49
+ <div className={`absolute left-2.5 top-1/2 -translate-y-1/2 w-[46px] rounded-xl bg-surface z-[1000] flex flex-col items-center py-1.5 gap-0.5 font-[lixFont] max-h-[calc(100vh-120px)] overflow-y-auto no-scrollbar`}>
50
+ {/* Tool lock button at the top */}
51
+ {!viewMode && (
52
+ <>
53
+ <button
54
+ title="Tool Lock (Q)"
55
+ onClick={toggleToolLock}
56
+ className={`relative w-[33px] h-[30px] flex items-center justify-center rounded-lg transition-all duration-200 ${
57
+ toolLock
58
+ ? 'bg-accent-blue/20 text-accent-blue'
59
+ : 'text-text-dim hover:text-text-muted hover:bg-surface-hover'
60
+ }`}
61
+ >
62
+ <i className={`bx ${toolLock ? 'bxs-lock-alt' : 'bx-lock-alt'} text-lg`} />
63
+ <span className="absolute bottom-0.5 right-[-1px] text-[10px] leading-none opacity-50">Q</span>
64
+ </button>
65
+ <div className="w-6 h-px bg-border-light my-0.5" />
66
+ </>
67
+ )}
68
+
69
+ {items.map((item, idx) => {
70
+ if (item === 'spacer') {
71
+ return (
72
+ <div
73
+ key={`spacer-${idx}`}
74
+ className="w-6 h-px bg-border-light my-0.5"
75
+ />
76
+ )
77
+ }
78
+
79
+ const isActive = activeTool === item.tool
80
+
81
+ if (item.isAI) {
82
+ return (
83
+ <button
84
+ key="ai"
85
+ title={item.title}
86
+ onClick={toggleAIModal}
87
+ className="w-[33px] h-[31px] flex items-center justify-center rounded-lg text-text-muted hover:text-accent hover:bg-surface-hover transition-all duration-200"
88
+ >
89
+ <svg
90
+ width="20"
91
+ height="20"
92
+ viewBox="0 0 24 24"
93
+ fill="none"
94
+ stroke="currentColor"
95
+ strokeWidth="2"
96
+ strokeLinecap="round"
97
+ strokeLinejoin="round"
98
+ >
99
+ <path d="M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3z" />
100
+ <path d="M18 14l1 3 3 1-3 1-1 3-1-3-3-1 3-1 1-3z" />
101
+ </svg>
102
+ </button>
103
+ )
104
+ }
105
+
106
+ return (
107
+ <button
108
+ key={item.tool}
109
+ title={item.title}
110
+ onClick={() => setActiveTool(item.tool)}
111
+ className={`relative w-[33px] h-[31px] flex items-center justify-center rounded-lg transition-all duration-200 ${
112
+ isActive
113
+ ? 'bg-surface-active text-text-primary'
114
+ : 'text-text-muted hover:text-text-primary hover:bg-surface-hover'
115
+ }`}
116
+ >
117
+ <i
118
+ className={`bx ${item.icon} text-xl`}
119
+ style={item.rotate ? { transform: 'rotate(-45deg)' } : undefined}
120
+ />
121
+ {item.key && (
122
+ <span className={`absolute bottom-0.5 right-[-1px] text-[10px] leading-none ${isActive ? 'opacity-60' : 'opacity-35'}`}>
123
+ {item.key}
124
+ </span>
125
+ )}
126
+ </button>
127
+ )
128
+ })}
129
+ </div>
130
+ {viewMode && (
131
+ <div className="absolute top-16 left-2.5 w-[46px] z-[1000] flex justify-center font-[lixFont]">
132
+ <span className="text-text-dim text-[9px] text-center leading-tight">
133
+ View<br/>Mode<br/>
134
+ <kbd className="text-[8px] text-text-muted">Esc</kbd>
135
+ </span>
136
+ </div>
137
+ )}
138
+ </>
139
+ )
140
+ }
@@ -0,0 +1,76 @@
1
+ "use client"
2
+
3
+ import useUIStore from '../../store/useUIStore'
4
+
5
+ export default function CanvasLoadingOverlay() {
6
+ const loading = useUIStore((s) => s.canvasLoading)
7
+ const message = useUIStore((s) => s.canvasLoadingMessage)
8
+
9
+ if (!loading) return null
10
+
11
+ return (
12
+ <div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/80 backdrop-blur-sm font-[lixFont] pointer-events-all">
13
+ <div className="flex flex-col items-center gap-5">
14
+ <style>{`
15
+ @keyframes cl-glob {
16
+ 0%, 100% { transform: translate(0, 0) scale(1); opacity: 0.7; }
17
+ 25% { transform: translate(10px, -6px) scale(1.12); opacity: 0.9; }
18
+ 50% { transform: translate(-5px, 8px) scale(0.9); opacity: 0.6; }
19
+ 75% { transform: translate(-8px, -4px) scale(1.08); opacity: 0.85; }
20
+ }
21
+ @keyframes cl-glob-2 {
22
+ 0%, 100% { transform: translate(0, 0) scale(1); opacity: 0.6; }
23
+ 25% { transform: translate(-8px, 5px) scale(1.1); opacity: 0.85; }
24
+ 50% { transform: translate(6px, -8px) scale(0.95); opacity: 0.7; }
25
+ 75% { transform: translate(5px, 6px) scale(1.12); opacity: 0.9; }
26
+ }
27
+ @keyframes cl-float {
28
+ 0%, 100% { transform: translateY(0); }
29
+ 50% { transform: translateY(-3px); }
30
+ }
31
+ `}</style>
32
+
33
+ <div className="relative w-20 h-20">
34
+ <div className="absolute rounded-full" style={{
35
+ width: 38, height: 38, top: 2, left: 4,
36
+ background: 'radial-gradient(circle, rgba(59,130,246,0.5) 0%, transparent 70%)',
37
+ filter: 'blur(10px)',
38
+ animation: 'cl-glob 3.5s ease-in-out infinite',
39
+ willChange: 'transform, opacity',
40
+ }} />
41
+ <div className="absolute rounded-full" style={{
42
+ width: 34, height: 34, top: 14, right: 2,
43
+ background: 'radial-gradient(circle, rgba(168,85,247,0.45) 0%, transparent 70%)',
44
+ filter: 'blur(10px)',
45
+ animation: 'cl-glob-2 4s ease-in-out infinite',
46
+ willChange: 'transform, opacity',
47
+ }} />
48
+ <div className="absolute rounded-full" style={{
49
+ width: 30, height: 30, bottom: 2, left: 12,
50
+ background: 'radial-gradient(circle, rgba(52,211,153,0.4) 0%, transparent 70%)',
51
+ filter: 'blur(10px)',
52
+ animation: 'cl-glob 4.2s ease-in-out infinite reverse',
53
+ willChange: 'transform, opacity',
54
+ }} />
55
+ <div className="absolute inset-0 flex items-center justify-center" style={{ animation: 'cl-float 2.5s ease-in-out infinite' }}>
56
+ <div className="w-9 h-9 rounded-lg bg-white/[0.06] backdrop-blur-sm border border-white/[0.08] flex items-center justify-center">
57
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="opacity-70">
58
+ <rect x="3" y="3" width="7" height="7" rx="1" />
59
+ <rect x="14" y="3" width="7" height="7" rx="1" />
60
+ <rect x="3" y="14" width="7" height="7" rx="1" />
61
+ <rect x="14" y="14" width="7" height="7" rx="1" />
62
+ </svg>
63
+ </div>
64
+ </div>
65
+ </div>
66
+
67
+ <div className="flex items-center gap-1.5">
68
+ <span className="w-1.5 h-1.5 rounded-full bg-blue-400 animate-bounce" style={{ animationDelay: '0ms' }} />
69
+ <span className="w-1.5 h-1.5 rounded-full bg-purple-400 animate-bounce" style={{ animationDelay: '150ms' }} />
70
+ <span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-bounce" style={{ animationDelay: '300ms' }} />
71
+ </div>
72
+ <p className="text-text-dim text-sm">{message}</p>
73
+ </div>
74
+ </div>
75
+ )
76
+ }
@@ -0,0 +1,466 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect, useCallback, useRef } from 'react'
4
+ import useSketchStore from '../../store/useSketchStore'
5
+ import useUIStore from '../../store/useUIStore'
6
+ import { showToast } from '../../utils/toast'
7
+
8
+ // ── Helpers ──────────────────────────────────────────────────
9
+
10
+ function getCleanSVG() {
11
+ const svgEl = window.svg
12
+ if (!svgEl) return null
13
+ const clone = svgEl.cloneNode(true)
14
+ clone.querySelectorAll(
15
+ '[data-selection], .selection-handle, .resize-handle, .rotation-handle, .anchor, .rotate-anchor, .resize-anchor, .rotation-anchor, .selection-outline'
16
+ ).forEach((el) => el.remove())
17
+ return clone
18
+ }
19
+
20
+ function renderToCanvas(clone, scale, bgColor) {
21
+ return new Promise((resolve) => {
22
+ const svgData = new XMLSerializer().serializeToString(clone)
23
+ const vb = window.svg.viewBox.baseVal
24
+ const canvas = document.createElement('canvas')
25
+ canvas.width = vb.width * scale
26
+ canvas.height = vb.height * scale
27
+ const ctx = canvas.getContext('2d')
28
+ ctx.scale(scale, scale)
29
+ const img = new Image()
30
+ const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' })
31
+ const url = URL.createObjectURL(blob)
32
+ img.onload = () => {
33
+ if (bgColor) {
34
+ ctx.fillStyle = bgColor
35
+ ctx.fillRect(0, 0, vb.width, vb.height)
36
+ }
37
+ ctx.drawImage(img, 0, 0, vb.width, vb.height)
38
+ URL.revokeObjectURL(url)
39
+ resolve(canvas)
40
+ }
41
+ img.onerror = () => { URL.revokeObjectURL(url); resolve(null) }
42
+ img.src = url
43
+ })
44
+ }
45
+
46
+ function getShapeAtPoint(x, y) {
47
+ const shapes = window.shapes
48
+ if (!shapes) return null
49
+ // Walk backwards (topmost first)
50
+ for (let i = shapes.length - 1; i >= 0; i--) {
51
+ const shape = shapes[i]
52
+ const el = shape.group || shape.element
53
+ if (!el) continue
54
+ const rect = el.getBoundingClientRect()
55
+ if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
56
+ return shape
57
+ }
58
+ }
59
+ return null
60
+ }
61
+
62
+ // ── Menu item components ─────────────────────────────────────
63
+
64
+ function MenuItem({ label, shortcut, onClick, disabled, danger }) {
65
+ return (
66
+ <button
67
+ onClick={onClick}
68
+ disabled={disabled}
69
+ className={`w-full flex items-center justify-between px-3 py-1.5 text-xs transition-colors duration-100 ${
70
+ disabled
71
+ ? 'text-text-dim cursor-default'
72
+ : danger
73
+ ? 'text-red-400 hover:bg-red-500/15 cursor-pointer'
74
+ : 'text-text-secondary hover:bg-surface-hover cursor-pointer'
75
+ }`}
76
+ >
77
+ <span>{label}</span>
78
+ {shortcut && <span className="text-text-dim text-[10px] ml-6">{shortcut}</span>}
79
+ </button>
80
+ )
81
+ }
82
+
83
+ function CheckMenuItem({ label, shortcut, checked, onClick }) {
84
+ return (
85
+ <button
86
+ onClick={onClick}
87
+ className="w-full flex items-center justify-between px-3 py-1.5 text-xs text-text-secondary hover:bg-surface-hover cursor-pointer transition-colors duration-100"
88
+ >
89
+ <span className="flex items-center gap-2">
90
+ {checked && <i className="bx bx-check text-sm text-accent-blue -ml-0.5" />}
91
+ {!checked && <span className="w-[14px]" />}
92
+ {label}
93
+ </span>
94
+ {shortcut && <span className="text-text-dim text-[10px] ml-6">{shortcut}</span>}
95
+ </button>
96
+ )
97
+ }
98
+
99
+ function Separator() {
100
+ return <hr className="border-border-light my-1" />
101
+ }
102
+
103
+ // ── Main component ───────────────────────────────────────────
104
+
105
+ export default function ContextMenu() {
106
+ const [visible, setVisible] = useState(false)
107
+ const [position, setPosition] = useState({ x: 0, y: 0 })
108
+ const [targetShape, setTargetShape] = useState(null)
109
+ const menuRef = useRef(null)
110
+
111
+ const gridEnabled = useSketchStore((s) => s.gridEnabled)
112
+ const snapToObjects = useSketchStore((s) => s.snapToObjects)
113
+ const zenMode = useSketchStore((s) => s.zenMode)
114
+ const viewMode = useSketchStore((s) => s.viewMode)
115
+
116
+ const close = useCallback(() => {
117
+ setVisible(false)
118
+ setTargetShape(null)
119
+ }, [])
120
+
121
+ // Context menu handler
122
+ useEffect(() => {
123
+ const handleContextMenu = (e) => {
124
+ const svg = document.getElementById('freehand-canvas')
125
+ if (!svg) return
126
+
127
+ // Only handle right-clicks on or within the SVG canvas
128
+ if (!svg.contains(e.target) && e.target !== svg) return
129
+
130
+ e.preventDefault()
131
+ const shape = getShapeAtPoint(e.clientX, e.clientY)
132
+
133
+ // Also check if there's a currentShape selected
134
+ const current = window.currentShape || null
135
+
136
+ setTargetShape(shape || current)
137
+ setPosition({ x: e.clientX, y: e.clientY })
138
+ setVisible(true)
139
+ }
140
+
141
+ const handleClick = () => close()
142
+ const handleKeyDown = (e) => { if (e.key === 'Escape') close() }
143
+
144
+ document.addEventListener('contextmenu', handleContextMenu)
145
+ document.addEventListener('click', handleClick)
146
+ document.addEventListener('keydown', handleKeyDown)
147
+ return () => {
148
+ document.removeEventListener('contextmenu', handleContextMenu)
149
+ document.removeEventListener('click', handleClick)
150
+ document.removeEventListener('keydown', handleKeyDown)
151
+ }
152
+ }, [close])
153
+
154
+ // Adjust position so menu doesn't overflow viewport
155
+ useEffect(() => {
156
+ if (!visible || !menuRef.current) return
157
+ const rect = menuRef.current.getBoundingClientRect()
158
+ const vw = window.innerWidth
159
+ const vh = window.innerHeight
160
+ let { x, y } = position
161
+ if (x + rect.width > vw - 8) x = vw - rect.width - 8
162
+ if (y + rect.height > vh - 8) y = vh - rect.height - 8
163
+ if (x !== position.x || y !== position.y) setPosition({ x, y })
164
+ }, [visible, position])
165
+
166
+ if (!visible) return null
167
+
168
+ // ── Action handlers ────────────────────────────────────────
169
+
170
+ const handlePaste = () => {
171
+ if (typeof window.pasteClipboard === 'function') window.pasteClipboard()
172
+ else document.dispatchEvent(new KeyboardEvent('keydown', { key: 'v', ctrlKey: true }))
173
+ close()
174
+ }
175
+
176
+ const handleCopy = () => {
177
+ if (typeof window.copySelected === 'function') window.copySelected()
178
+ else document.dispatchEvent(new KeyboardEvent('keydown', { key: 'c', ctrlKey: true }))
179
+ close()
180
+ }
181
+
182
+ const handleCut = () => {
183
+ handleCopy()
184
+ handleDeleteShape()
185
+ }
186
+
187
+ const handleSelectAll = () => {
188
+ useSketchStore.getState().setActiveTool('select')
189
+ if (window.multiSelection && window.shapes) {
190
+ window.multiSelection.clearSelection()
191
+ window.shapes.forEach((s) => window.multiSelection.addShape(s))
192
+ }
193
+ close()
194
+ }
195
+
196
+ const handleCopyPNG = async () => {
197
+ const clone = getCleanSVG()
198
+ if (!clone) { close(); return }
199
+ const canvas = await renderToCanvas(clone, 2, '#121212')
200
+ if (!canvas) { close(); return }
201
+ canvas.toBlob((blob) => {
202
+ if (blob) navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]).catch(() => {})
203
+ }, 'image/png')
204
+ close()
205
+ }
206
+
207
+ const handleCopySVG = () => {
208
+ const clone = getCleanSVG()
209
+ if (!clone) { close(); return }
210
+ const svgData = new XMLSerializer().serializeToString(clone)
211
+ navigator.clipboard.writeText(svgData).catch(() => {})
212
+ close()
213
+ }
214
+
215
+ const handleToggleGrid = () => {
216
+ useSketchStore.getState().toggleGrid()
217
+ close()
218
+ }
219
+
220
+ const handleToggleSnap = () => {
221
+ useSketchStore.getState().toggleSnapToObjects()
222
+ close()
223
+ }
224
+
225
+ const handleZenMode = () => {
226
+ useSketchStore.getState().toggleZenMode()
227
+ close()
228
+ }
229
+
230
+ const handleViewMode = () => {
231
+ useSketchStore.getState().toggleViewMode()
232
+ close()
233
+ }
234
+
235
+ const handleDeleteShape = () => {
236
+ // Multi-selection delete
237
+ if (window.multiSelection?.selectedShapes?.size > 0) {
238
+ if (typeof window.deleteSelectedShapes === 'function') window.deleteSelectedShapes()
239
+ close()
240
+ return
241
+ }
242
+ const shape = targetShape || window.currentShape
243
+ if (!shape) { close(); return }
244
+ const shapes = window.shapes
245
+
246
+ // Issue #24 bug #7: frame → orphan children + snapshot for undo
247
+ // (mirrors deleteSelectedShapes / EngineShortcuts).
248
+ const isFrame = shape.shapeName === 'frame'
249
+ const isDiagramFrame = isFrame && !!shape._diagramType
250
+ let childSnapshot = null
251
+ if (isFrame && !isDiagramFrame) {
252
+ childSnapshot = [...(shape.containedShapes || [])]
253
+ for (const child of childSnapshot) {
254
+ if (!child || child === shape) continue
255
+ const el = child.group || child.element
256
+ if (el) {
257
+ if (shape.clipGroup && el.parentNode === shape.clipGroup) shape.clipGroup.removeChild(el)
258
+ if (window.svg && el.parentNode !== window.svg) window.svg.appendChild(el)
259
+ }
260
+ child.parentFrame = null
261
+ delete child.isBeingMovedByFrame
262
+ }
263
+ shape.containedShapes = []
264
+ }
265
+
266
+ if (shapes) {
267
+ const idx = shapes.indexOf(shape)
268
+ if (idx !== -1) shapes.splice(idx, 1)
269
+ }
270
+ if (typeof window.cleanupAttachments === 'function') window.cleanupAttachments(shape)
271
+ if (shape.parentFrame && typeof shape.parentFrame.removeShapeFromFrame === 'function') {
272
+ shape.parentFrame.removeShapeFromFrame(shape)
273
+ }
274
+
275
+ if (isFrame && !isDiagramFrame) {
276
+ if (shape.group && shape.group.parentNode) shape.group.parentNode.removeChild(shape.group)
277
+ if (shape.clipGroup && shape.clipGroup.parentNode) shape.clipGroup.parentNode.removeChild(shape.clipGroup)
278
+ if (shape.clipPath && shape.clipPath.parentNode) shape.clipPath.parentNode.removeChild(shape.clipPath)
279
+ } else {
280
+ const el = shape.group || shape.element
281
+ if (el && el.parentNode) el.parentNode.removeChild(el)
282
+ }
283
+
284
+ if (typeof window.pushDeleteAction === 'function') {
285
+ if (childSnapshot) window.pushDeleteAction(shape, { childSnapshot })
286
+ else window.pushDeleteAction(shape)
287
+ }
288
+ window.currentShape = null
289
+ if (typeof window.disableAllSideBars === 'function') window.disableAllSideBars()
290
+ close()
291
+ }
292
+
293
+ const handleDuplicate = () => {
294
+ // Copy then paste
295
+ if (typeof window.copySelected === 'function') window.copySelected()
296
+ setTimeout(() => {
297
+ if (typeof window.pasteClipboard === 'function') window.pasteClipboard()
298
+ }, 50)
299
+ close()
300
+ }
301
+
302
+ // Layer order
303
+ const handleLayerAction = (action) => {
304
+ const shape = targetShape || window.currentShape
305
+ if (!shape || !window.__layerOrder) { close(); return }
306
+ window.__layerOrder[action](shape)
307
+ close()
308
+ }
309
+
310
+ // Flip (transform via scale)
311
+ const handleFlip = (axis) => {
312
+ const shape = targetShape || window.currentShape
313
+ if (!shape) { close(); return }
314
+ const el = shape.group || shape.element
315
+ if (!el) { close(); return }
316
+ const current = el.getAttribute('transform') || ''
317
+ const bbox = el.getBBox()
318
+ const cx = bbox.x + bbox.width / 2
319
+ const cy = bbox.y + bbox.height / 2
320
+ const scaleStr = axis === 'h'
321
+ ? `translate(${cx * 2}, 0) scale(-1, 1)`
322
+ : `translate(0, ${cy * 2}) scale(1, -1)`
323
+ el.setAttribute('transform', current + ' ' + scaleStr)
324
+ close()
325
+ }
326
+
327
+ // Wrap selection in frame
328
+ const handleWrapInFrame = () => {
329
+ // Simulate the frame wrap if engine supports it
330
+ close()
331
+ }
332
+
333
+ // ── Group / ungroup helpers ─────────────────────────────────
334
+ // Mirror the Ctrl+G / Ctrl+Shift+G behavior so the right-click menu
335
+ // is a discoverable alternative.
336
+ const handleGroup = () => {
337
+ const ms = window.multiSelection
338
+ const sel = ms?.selectedShapes
339
+ const targets = sel && sel.size > 1
340
+ ? Array.from(sel)
341
+ : (window.currentShape ? [window.currentShape] : [])
342
+ if (targets.length > 1) {
343
+ const newId = `g-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`
344
+ for (const s of targets) s.groupId = newId
345
+ if (typeof ms?.updateControls === 'function') ms.updateControls()
346
+ showToast(`Grouped ${targets.length} shapes`, { tone: 'success' })
347
+ }
348
+ close()
349
+ }
350
+
351
+ const handleUngroup = () => {
352
+ const ms = window.multiSelection
353
+ const sel = ms?.selectedShapes
354
+ const targets = sel && sel.size > 0
355
+ ? Array.from(sel)
356
+ : (window.currentShape ? [window.currentShape] : [])
357
+ const groupIds = new Set(targets.map(s => s.groupId).filter(Boolean))
358
+ if (groupIds.size > 0 && Array.isArray(window.shapes)) {
359
+ let cleared = 0
360
+ for (const s of window.shapes) {
361
+ if (s.groupId && groupIds.has(s.groupId)) { s.groupId = null; cleared++ }
362
+ }
363
+ if (typeof ms?.updateControls === 'function') ms.updateControls()
364
+ showToast(`Ungrouped ${cleared} shapes`, { tone: 'success' })
365
+ }
366
+ close()
367
+ }
368
+
369
+ // Compute selection state for the menu rendering below.
370
+ const selectionShapes = (() => {
371
+ const sel = window.multiSelection?.selectedShapes
372
+ if (sel && sel.size > 0) return Array.from(sel)
373
+ if (targetShape) return [targetShape]
374
+ return []
375
+ })()
376
+ const canGroup = selectionShapes.length > 1
377
+ const groupIdsInSelection = new Set(selectionShapes.map(s => s.groupId).filter(Boolean))
378
+ const canUngroup = groupIdsInSelection.size > 0
379
+ const fullyGrouped = selectionShapes.length > 1 &&
380
+ selectionShapes.every(s => s.groupId) &&
381
+ groupIdsInSelection.size === 1
382
+
383
+ const handleCanvasProperties = () => {
384
+ // Toggle shape sidebar / properties via Alt+/
385
+ close()
386
+ }
387
+
388
+ // ── Render ─────────────────────────────────────────────────
389
+
390
+ const isShape = !!targetShape
391
+
392
+ return (
393
+ <div
394
+ ref={menuRef}
395
+ className="fixed z-9999 min-w-[220px] py-1.5 bg-surface/90 backdrop-blur-xl rounded-xl border border-border-light shadow-2xl font-[lixFont] select-none"
396
+ style={{ left: position.x, top: position.y }}
397
+ onClick={(e) => e.stopPropagation()}
398
+ >
399
+ {isShape ? (
400
+ /* ── Shape context menu ── */
401
+ <>
402
+ <MenuItem label="Cut" shortcut="Ctrl+X" onClick={handleCut} />
403
+ <MenuItem label="Copy" shortcut="Ctrl+C" onClick={handleCopy} />
404
+ <MenuItem label="Paste" shortcut="Ctrl+V" onClick={handlePaste} />
405
+
406
+ <Separator />
407
+ {/* Frame wrap doesn't make sense when the selection is already
408
+ grouped — the group itself is the implicit container. */}
409
+ {!fullyGrouped && (
410
+ <MenuItem label="Wrap selection in frame" onClick={handleWrapInFrame} />
411
+ )}
412
+ {canGroup && !fullyGrouped && (
413
+ <MenuItem label="Group" shortcut="Ctrl+G" onClick={handleGroup} />
414
+ )}
415
+ {canUngroup && (
416
+ <MenuItem label="Ungroup" shortcut="Ctrl+Shift+G" onClick={handleUngroup} />
417
+ )}
418
+
419
+ <Separator />
420
+ <MenuItem label="Copy to clipboard as PNG" shortcut="Shift+Alt+C" onClick={handleCopyPNG} />
421
+ <MenuItem label="Copy to clipboard as SVG" onClick={handleCopySVG} />
422
+
423
+ <Separator />
424
+ <MenuItem label="Send backward" shortcut="Ctrl+[" onClick={() => handleLayerAction('sendBackward')} />
425
+ <MenuItem label="Bring forward" shortcut="Ctrl+]" onClick={() => handleLayerAction('bringForward')} />
426
+ <MenuItem label="Send to back" shortcut="Ctrl+Shift+[" onClick={() => handleLayerAction('sendToBack')} />
427
+ <MenuItem label="Bring to front" shortcut="Ctrl+Shift+]" onClick={() => handleLayerAction('bringToFront')} />
428
+
429
+ <Separator />
430
+ <MenuItem label="Flip horizontal" shortcut="Shift+H" onClick={() => handleFlip('h')} />
431
+ <MenuItem label="Flip vertical" shortcut="Shift+V" onClick={() => handleFlip('v')} />
432
+
433
+ <Separator />
434
+ <MenuItem label="Duplicate" shortcut="Ctrl+D" onClick={handleDuplicate} />
435
+ <MenuItem label="Delete" danger onClick={handleDeleteShape} />
436
+ </>
437
+ ) : (
438
+ /* ── Canvas context menu ── */
439
+ <>
440
+ <MenuItem label="Paste" shortcut="Ctrl+V" onClick={handlePaste} />
441
+ <MenuItem label="Copy to clipboard as PNG" shortcut="Shift+Alt+C" onClick={handleCopyPNG} />
442
+ <MenuItem label="Copy to clipboard as SVG" onClick={handleCopySVG} />
443
+
444
+ <Separator />
445
+ <MenuItem label="Select all" shortcut="Ctrl+A" onClick={handleSelectAll} />
446
+
447
+ <Separator />
448
+ <CheckMenuItem label="Toggle grid" shortcut="Ctrl+'" checked={gridEnabled} onClick={handleToggleGrid} />
449
+ <CheckMenuItem label="Snap to objects" shortcut="Alt+S" checked={snapToObjects} onClick={handleToggleSnap} />
450
+
451
+ <Separator />
452
+ <MenuItem label="Find on canvas" shortcut="Ctrl+F" onClick={() => { useUIStore.getState().toggleFindBar(); close() }} />
453
+ <MenuItem label="Zen mode" shortcut="Alt+Z" onClick={handleZenMode} />
454
+ <MenuItem label="View mode" shortcut="Alt+R" onClick={handleViewMode} />
455
+
456
+ <Separator />
457
+ <MenuItem label="Reset canvas" danger onClick={() => {
458
+ const serializer = window.__sceneSerializer
459
+ if (serializer?.resetCanvas) serializer.resetCanvas()
460
+ close()
461
+ }} />
462
+ </>
463
+ )}
464
+ </div>
465
+ )
466
+ }