@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,347 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useRef, useState } from 'react';
4
+ import { compressImage } from '../utils/imageCompressor.js';
5
+ let _saveScene = null;
6
+ let _loadScene = null;
7
+ async function ensureSceneSerializer() {
8
+ if (_saveScene && _loadScene) return;
9
+ const m = await import('../core/SceneSerializer.js');
10
+ _saveScene = m.saveScene;
11
+ _loadScene = m.loadScene;
12
+ }
13
+
14
+ import useSketchStore, { TOOLS } from './store/useSketchStore.js';
15
+ import useUIStore from './store/useUIStore.js';
16
+ import SVGCanvas from './components/canvas/SVGCanvas.jsx';
17
+ import Toolbar from './components/Toolbar.jsx';
18
+ import Footer from './components/Footer.jsx';
19
+ import AppMenu from './components/AppMenu.jsx';
20
+ import RectangleSidebar from './components/sidebars/RectangleSidebar.jsx';
21
+ import CircleSidebar from './components/sidebars/CircleSidebar.jsx';
22
+ import LineSidebar from './components/sidebars/LineSidebar.jsx';
23
+ import ArrowSidebar from './components/sidebars/ArrowSidebar.jsx';
24
+ import PaintbrushSidebar from './components/sidebars/PaintbrushSidebar.jsx';
25
+ import TextSidebar from './components/sidebars/TextSidebar.jsx';
26
+ import FrameSidebar from './components/sidebars/FrameSidebar.jsx';
27
+ import IconSidebar from './components/sidebars/IconSidebar.jsx';
28
+ import ImageSidebar from './components/sidebars/ImageSidebar.jsx';
29
+ import MultiSelectActions from './components/canvas/MultiSelectActions.jsx';
30
+ import ContextMenu from './components/canvas/ContextMenu.jsx';
31
+ import FindBar from './components/canvas/FindBar.jsx';
32
+ import ImageSourcePicker from './components/canvas/ImageSourcePicker.jsx';
33
+ import CanvasLoadingOverlay from './components/canvas/CanvasLoadingOverlay.jsx';
34
+ import ShortcutsModal from './components/modals/ShortcutsModal.jsx';
35
+ import CommandPalette from './components/modals/CommandPalette.jsx';
36
+ import ExportImageModal from './components/modals/ExportImageModal.jsx';
37
+ import HelpModal from './components/modals/HelpModal.jsx';
38
+ import LixScriptModal from './components/modals/LixScriptModal.jsx';
39
+ import GraphModal from './components/modals/GraphModal.jsx';
40
+
41
+ const SAVE_DEBOUNCE_MS = 1500;
42
+
43
+ /**
44
+ * Self-contained canvas component. Mounts the SketchEngine on an internal
45
+ * <svg>, renders the full toolbar + per-shape sidebars + offline modals,
46
+ * and surfaces scene changes + image uploads to the host.
47
+ *
48
+ * Offline-first: no cloud sync, AI, or auth code paths reach this build.
49
+ * The host owns persistence and image upload via the two callbacks.
50
+ */
51
+ export default function LixSketchCanvas({
52
+ initialScene = null,
53
+ onSceneChange = null,
54
+ onUploadImage = null,
55
+ onExit = null,
56
+ className = '',
57
+ style = null,
58
+ }) {
59
+ const wrapperRef = useRef(null);
60
+ const lastSceneJsonRef = useRef('');
61
+ const debounceRef = useRef(null);
62
+ const [bootstrapped, setBootstrapped] = useState(false);
63
+
64
+ // Re-route engine image uploads through the host before any user action.
65
+ useEffect(() => {
66
+ installImageUploadBridge(onUploadImage);
67
+ }, [onUploadImage]);
68
+
69
+ // Eager-load lixFont / lixCode so the textarea overlay used while a user
70
+ // types into a text shape uses the same font as the final rendered text.
71
+ // Without this, the first text edit shows in a system font and snaps to
72
+ // lixFont only after the SVG re-renders.
73
+ useEffect(() => {
74
+ if (typeof document === 'undefined' || !document.fonts?.load) return;
75
+ Promise.allSettled([
76
+ document.fonts.load('1em lixFont'),
77
+ document.fonts.load('1em lixCode'),
78
+ ]).catch(() => {});
79
+ }, []);
80
+
81
+ // Apply initialScene once the engine reports ready (via window.__sketchEngine).
82
+ useEffect(() => {
83
+ if (!initialScene || bootstrapped) return;
84
+ let cancelled = false;
85
+ async function tryLoad() {
86
+ if (cancelled) return;
87
+ const serializer = window.__sceneSerializer;
88
+ if (!serializer) {
89
+ setTimeout(tryLoad, 200);
90
+ return;
91
+ }
92
+ try {
93
+ await ensureSceneSerializer();
94
+ if (cancelled) return;
95
+ const data = typeof initialScene === 'string' ? JSON.parse(initialScene) : initialScene;
96
+ if (data && data.format === 'lixsketch') {
97
+ _loadScene(data);
98
+ lastSceneJsonRef.current = JSON.stringify(data);
99
+ }
100
+ } catch (err) {
101
+ console.warn('[LixSketchCanvas] initialScene load failed:', err);
102
+ }
103
+ setBootstrapped(true);
104
+ }
105
+ tryLoad();
106
+ return () => { cancelled = true; };
107
+ }, [initialScene, bootstrapped]);
108
+
109
+ // Ctrl+S → flush + show toast. Same UX as the standalone product but
110
+ // with no cloud round-trip — onSceneChange (passed by the host) is the
111
+ // real persistence layer.
112
+ useEffect(() => {
113
+ function handleKey(e) {
114
+ if (!(e.ctrlKey || e.metaKey)) return;
115
+ const key = (e.key || '').toLowerCase();
116
+ if (key !== 's' || e.shiftKey) return;
117
+ e.preventDefault();
118
+ (async () => {
119
+ try {
120
+ await ensureSceneSerializer();
121
+ const scene = _saveScene('Untitled');
122
+ const json = JSON.stringify(scene);
123
+ lastSceneJsonRef.current = json;
124
+ if (onSceneChange) {
125
+ const metadata = {
126
+ shapeCount: Array.isArray(scene.shapes) ? scene.shapes.length : 0,
127
+ viewport: scene.viewport || null,
128
+ zoom: scene.zoom || 1,
129
+ sizeBytes: json.length,
130
+ savedAt: Date.now(),
131
+ };
132
+ onSceneChange(scene, metadata);
133
+ }
134
+ // Show the in-canvas save toast (rendered below).
135
+ const toast = document.getElementById('lixsketch-save-toast');
136
+ if (toast) {
137
+ toast.classList.remove('hidden');
138
+ clearTimeout(toast._hideTimer);
139
+ toast._hideTimer = setTimeout(() => toast.classList.add('hidden'), 1800);
140
+ }
141
+ useUIStore.getState().setSaveStatus?.('cloud');
142
+ } catch (err) {
143
+ console.warn('[LixSketchCanvas] Ctrl+S save failed:', err);
144
+ }
145
+ })();
146
+ }
147
+ window.addEventListener('keydown', handleKey);
148
+ return () => window.removeEventListener('keydown', handleKey);
149
+ }, [onSceneChange]);
150
+
151
+ // Scene-change autosave — MutationObserver + mouseup, debounced.
152
+ useEffect(() => {
153
+ if (!onSceneChange) return;
154
+ let svg = null;
155
+ let observer = null;
156
+
157
+ const flush = async () => {
158
+ try {
159
+ await ensureSceneSerializer();
160
+ const scene = _saveScene('Untitled');
161
+ const json = JSON.stringify(scene);
162
+ if (json === lastSceneJsonRef.current) return;
163
+ lastSceneJsonRef.current = json;
164
+ const metadata = {
165
+ shapeCount: Array.isArray(scene.shapes) ? scene.shapes.length : 0,
166
+ viewport: scene.viewport || null,
167
+ zoom: scene.zoom || 1,
168
+ sizeBytes: json.length,
169
+ savedAt: Date.now(),
170
+ };
171
+ onSceneChange(scene, metadata);
172
+ } catch (err) {
173
+ console.warn('[LixSketchCanvas] save failed:', err);
174
+ }
175
+ };
176
+ const debounced = () => {
177
+ clearTimeout(debounceRef.current);
178
+ debounceRef.current = setTimeout(flush, SAVE_DEBOUNCE_MS);
179
+ };
180
+
181
+ function attach() {
182
+ svg = window.svg;
183
+ if (!svg) {
184
+ setTimeout(attach, 200);
185
+ return;
186
+ }
187
+ observer = new MutationObserver(debounced);
188
+ observer.observe(svg, { childList: true, subtree: true, attributes: true });
189
+ svg.addEventListener('mouseup', debounced);
190
+ }
191
+ attach();
192
+ window.addEventListener('beforeunload', flush);
193
+
194
+ return () => {
195
+ if (observer) observer.disconnect();
196
+ if (svg) svg.removeEventListener('mouseup', debounced);
197
+ window.removeEventListener('beforeunload', flush);
198
+ clearTimeout(debounceRef.current);
199
+ };
200
+ }, [onSceneChange]);
201
+
202
+ return (
203
+ <div
204
+ ref={wrapperRef}
205
+ className={`lixsketch-canvas-root canvas-mode ${className}`}
206
+ style={style}
207
+ >
208
+ <SVGCanvas />
209
+ <Toolbar />
210
+ <Footer />
211
+ <AppMenu />
212
+ <RectangleSidebar />
213
+ <CircleSidebar />
214
+ <LineSidebar />
215
+ <ArrowSidebar />
216
+ <PaintbrushSidebar />
217
+ <TextSidebar />
218
+ <FrameSidebar />
219
+ <IconSidebar />
220
+ <ImageSidebar />
221
+ <MultiSelectActions />
222
+ <ShortcutsModal />
223
+ <CommandPalette />
224
+ <ExportImageModal />
225
+ <HelpModal />
226
+ <LixScriptModal />
227
+ <GraphModal />
228
+ <ContextMenu />
229
+ <FindBar />
230
+ <ImageSourcePicker />
231
+ <CanvasLoadingOverlay />
232
+
233
+ {/* Floating header — menu trigger + help / shortcuts. Hosts can
234
+ hide this strip by setting CSS `.lixsketch-floating-header { display: none }`
235
+ if they render their own chrome (blogs.elixpo's CanvasSubpage does). */}
236
+ <div className="lixsketch-floating-header absolute top-2 right-2 z-[1000] flex items-center gap-1.5 font-[lixFont]">
237
+ <button
238
+ type="button"
239
+ title="LixScript — write shapes in DSL"
240
+ onClick={() => useUIStore.getState().toggleAIModal?.()}
241
+ className="w-9 h-9 flex items-center justify-center rounded-lg bg-surface border border-border-light text-text-muted hover:text-text-primary hover:bg-surface-hover transition-colors"
242
+ >
243
+ <i className="bx bx-code-alt text-base" />
244
+ </button>
245
+ <button
246
+ type="button"
247
+ title="Graph — plot equations"
248
+ onClick={() => useUIStore.getState().toggleGraphModal?.()}
249
+ className="w-9 h-9 flex items-center justify-center rounded-lg bg-surface border border-border-light text-text-muted hover:text-text-primary hover:bg-surface-hover transition-colors"
250
+ >
251
+ <i className="bx bx-line-chart text-base" />
252
+ </button>
253
+ <button
254
+ type="button"
255
+ title="Help (?)"
256
+ onClick={() => useUIStore.getState().toggleHelpModal?.()}
257
+ className="w-9 h-9 flex items-center justify-center rounded-lg bg-surface border border-border-light text-text-muted hover:text-text-primary hover:bg-surface-hover transition-colors"
258
+ >
259
+ <i className="bx bx-help-circle text-base" />
260
+ </button>
261
+ <button
262
+ type="button"
263
+ title="Shortcuts (Ctrl+/)"
264
+ onClick={() => useUIStore.getState().toggleShortcutsModal?.()}
265
+ className="w-9 h-9 flex items-center justify-center rounded-lg bg-surface border border-border-light text-text-muted hover:text-text-primary hover:bg-surface-hover transition-colors"
266
+ >
267
+ <i className="bx bx-command text-base" />
268
+ </button>
269
+ <button
270
+ type="button"
271
+ title="Menu"
272
+ onClick={() => useUIStore.getState().toggleMenu?.()}
273
+ className="w-9 h-9 flex items-center justify-center rounded-lg bg-surface border border-border-light text-text-muted hover:text-text-primary hover:bg-surface-hover transition-colors"
274
+ >
275
+ <i className="bx bx-menu text-base" />
276
+ </button>
277
+ </div>
278
+
279
+ {/* Save toast — same look as the standalone product. Hidden by
280
+ default; Ctrl+S flips the `hidden` class on/off. */}
281
+ <div
282
+ id="lixsketch-save-toast"
283
+ className="hidden fixed bottom-20 left-1/2 -translate-x-1/2 z-[9999] px-4 py-2 rounded-xl bg-surface/85 backdrop-blur-md border border-border-light text-text-secondary text-xs font-[lixFont] pointer-events-none"
284
+ >
285
+ <i className="bx bx-check text-green-400 mr-1.5" />
286
+ Saved
287
+ </div>
288
+ </div>
289
+ );
290
+ }
291
+
292
+ // ── Image upload bridge ───────────────────────────────────────────────
293
+ function installImageUploadBridge(onUploadImage) {
294
+ if (typeof window === 'undefined') return;
295
+ if (!onUploadImage) {
296
+ // Offline mode (no host upload pipeline): leave the engine's
297
+ // compressed-data-URL placement as the final state.
298
+ window.uploadImageToCloudinary = async () => {};
299
+ return;
300
+ }
301
+
302
+ window.uploadImageToCloudinary = async function bridgedUpload(imageShape) {
303
+ const href = imageShape?.element?.getAttribute('href') || '';
304
+ if (!href.startsWith('data:')) return;
305
+
306
+ imageShape.uploadStatus = 'uploading';
307
+ imageShape.uploadAbortController = new AbortController();
308
+ const signal = imageShape.uploadAbortController.signal;
309
+ imageShape.showUploadIndicator?.();
310
+
311
+ try {
312
+ let payloadDataUrl = href;
313
+ try {
314
+ const compressed = await compressImage(href);
315
+ if (compressed?.dataUrl) payloadDataUrl = compressed.dataUrl;
316
+ } catch (err) {
317
+ console.warn('[LixSketchCanvas] compression failed, sending raw:', err);
318
+ }
319
+ if (signal.aborted) return;
320
+
321
+ const result = await onUploadImage(payloadDataUrl);
322
+ if (signal.aborted) return;
323
+ if (!result?.url) throw new Error(result?.error || 'Upload failed');
324
+
325
+ imageShape.element.setAttribute('href', result.url);
326
+ imageShape.element.setAttribute('data-href', result.url);
327
+ if (result.publicId) imageShape.element.setAttribute('data-cloudinary-id', result.publicId);
328
+ if (typeof result.sizeBytes === 'number') {
329
+ const oldSize = imageShape.element.__fileSize || 0;
330
+ imageShape.element.__fileSize = result.sizeBytes;
331
+ window.__roomImageBytesUsed = Math.max(
332
+ 0,
333
+ (window.__roomImageBytesUsed || 0) - oldSize + result.sizeBytes
334
+ );
335
+ }
336
+ imageShape.uploadStatus = 'done';
337
+ } catch (err) {
338
+ if (!signal.aborted) {
339
+ console.warn('[LixSketchCanvas] upload failed:', err);
340
+ imageShape.uploadStatus = 'failed';
341
+ }
342
+ } finally {
343
+ imageShape.removeUploadIndicator?.();
344
+ imageShape.uploadAbortController = null;
345
+ }
346
+ };
347
+ }