@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,177 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect, useRef, useCallback } from 'react'
4
+ import useUIStore from '../../store/useUIStore'
5
+
6
+ export default function FindBar() {
7
+ const open = useUIStore((s) => s.findBarOpen)
8
+ const closeFindBar = useUIStore((s) => s.closeFindBar)
9
+ const [query, setQuery] = useState('')
10
+ const [results, setResults] = useState([])
11
+ const [activeIndex, setActiveIndex] = useState(-1)
12
+ const inputRef = useRef(null)
13
+ const highlightRef = useRef(null)
14
+
15
+ useEffect(() => {
16
+ if (open) {
17
+ setQuery('')
18
+ setResults([])
19
+ setActiveIndex(-1)
20
+ setTimeout(() => inputRef.current?.focus(), 50)
21
+ } else {
22
+ // Clean up highlight when closing
23
+ removeHighlight()
24
+ }
25
+ }, [open])
26
+
27
+ const removeHighlight = useCallback(() => {
28
+ if (highlightRef.current && highlightRef.current.parentNode) {
29
+ highlightRef.current.parentNode.removeChild(highlightRef.current)
30
+ highlightRef.current = null
31
+ }
32
+ }, [])
33
+
34
+ const highlightShape = useCallback((result) => {
35
+ removeHighlight()
36
+ if (!result || !window.svg) return
37
+
38
+ const shape = result.shape
39
+ const x = shape.x || 0
40
+ const y = shape.y || 0
41
+ const w = shape.width || 100
42
+ const h = shape.height || 30
43
+
44
+ // Create highlight rect
45
+ const ns = 'http://www.w3.org/2000/svg'
46
+ const rect = document.createElementNS(ns, 'rect')
47
+ rect.setAttribute('x', x - 4)
48
+ rect.setAttribute('y', y - 4)
49
+ rect.setAttribute('width', w + 8)
50
+ rect.setAttribute('height', h + 8)
51
+ rect.setAttribute('fill', 'rgba(91, 87, 209, 0.15)')
52
+ rect.setAttribute('stroke', '#5B57D1')
53
+ rect.setAttribute('stroke-width', '2')
54
+ rect.setAttribute('stroke-dasharray', '4,2')
55
+ rect.setAttribute('rx', '4')
56
+ rect.setAttribute('pointer-events', 'none')
57
+ rect.classList.add('find-highlight')
58
+ window.svg.appendChild(rect)
59
+ highlightRef.current = rect
60
+
61
+ // Pan to the shape
62
+ const svgEl = window.svg
63
+ const vb = svgEl.viewBox.baseVal
64
+ const centerX = x + w / 2
65
+ const centerY = y + h / 2
66
+ const newX = centerX - vb.width / 2
67
+ const newY = centerY - vb.height / 2
68
+ svgEl.setAttribute('viewBox', `${newX} ${newY} ${vb.width} ${vb.height}`)
69
+ if (window.currentViewBox) {
70
+ window.currentViewBox.x = newX
71
+ window.currentViewBox.y = newY
72
+ }
73
+ }, [removeHighlight])
74
+
75
+ const doSearch = useCallback((q) => {
76
+ const finder = window.__sceneSerializer?.findText
77
+ if (!finder || !q.trim()) {
78
+ setResults([])
79
+ setActiveIndex(-1)
80
+ removeHighlight()
81
+ return
82
+ }
83
+ const found = finder(q)
84
+ setResults(found)
85
+ if (found.length > 0) {
86
+ setActiveIndex(0)
87
+ highlightShape(found[0])
88
+ } else {
89
+ setActiveIndex(-1)
90
+ removeHighlight()
91
+ }
92
+ }, [highlightShape, removeHighlight])
93
+
94
+ const goToResult = useCallback((idx) => {
95
+ if (idx < 0 || idx >= results.length) return
96
+ setActiveIndex(idx)
97
+ highlightShape(results[idx])
98
+ }, [results, highlightShape])
99
+
100
+ const handleKeyDown = (e) => {
101
+ if (e.key === 'Escape') {
102
+ closeFindBar()
103
+ return
104
+ }
105
+ if (e.key === 'Enter') {
106
+ e.preventDefault()
107
+ if (results.length === 0) {
108
+ doSearch(query)
109
+ } else {
110
+ // Go to next result
111
+ const next = (activeIndex + 1) % results.length
112
+ goToResult(next)
113
+ }
114
+ return
115
+ }
116
+ }
117
+
118
+ const handleChange = (e) => {
119
+ const q = e.target.value
120
+ setQuery(q)
121
+ doSearch(q)
122
+ }
123
+
124
+ if (!open) return null
125
+
126
+ return (
127
+ <div className="fixed top-14 right-4 z-[1000] font-[lixFont]">
128
+ <div className="flex items-center gap-2 bg-surface-card/95 backdrop-blur-lg border border-border-light rounded-xl px-3 py-2 shadow-xl min-w-[300px]">
129
+ <i className="bx bx-search text-text-muted text-sm" />
130
+ <input
131
+ ref={inputRef}
132
+ type="text"
133
+ value={query}
134
+ onChange={handleChange}
135
+ onKeyDown={handleKeyDown}
136
+ placeholder="Find text on canvas..."
137
+ className="flex-1 bg-transparent text-text-primary text-xs outline-none placeholder:text-text-dim font-[lixFont]"
138
+ />
139
+
140
+ {results.length > 0 && (
141
+ <span className="text-text-dim text-xs whitespace-nowrap">
142
+ {activeIndex + 1}/{results.length}
143
+ </span>
144
+ )}
145
+ {query && results.length === 0 && (
146
+ <span className="text-red-400/70 text-xs whitespace-nowrap">
147
+ No results
148
+ </span>
149
+ )}
150
+
151
+ {results.length > 1 && (
152
+ <div className="flex items-center gap-0.5">
153
+ <button
154
+ onClick={() => goToResult((activeIndex - 1 + results.length) % results.length)}
155
+ className="w-5 h-5 flex items-center justify-center rounded text-text-muted hover:text-white hover:bg-white/10 transition-colors"
156
+ >
157
+ <i className="bx bx-chevron-up text-sm" />
158
+ </button>
159
+ <button
160
+ onClick={() => goToResult((activeIndex + 1) % results.length)}
161
+ className="w-5 h-5 flex items-center justify-center rounded text-text-muted hover:text-white hover:bg-white/10 transition-colors"
162
+ >
163
+ <i className="bx bx-chevron-down text-sm" />
164
+ </button>
165
+ </div>
166
+ )}
167
+
168
+ <button
169
+ onClick={closeFindBar}
170
+ className="w-5 h-5 flex items-center justify-center rounded text-text-muted hover:text-white hover:bg-white/10 transition-colors"
171
+ >
172
+ <i className="bx bx-x text-sm" />
173
+ </button>
174
+ </div>
175
+ </div>
176
+ )
177
+ }
@@ -0,0 +1,133 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect, useCallback, useRef } from 'react'
4
+ import useUIStore from '../../store/useUIStore'
5
+ import useSketchStore from '../../store/useSketchStore'
6
+
7
+ export default function ImageSourcePicker() {
8
+ const ref = useRef(null)
9
+ const toggleImageGenerateModal = useUIStore((s) => s.toggleImageGenerateModal)
10
+ const activeTool = useSketchStore((s) => s.activeTool)
11
+ const setActiveTool = useSketchStore((s) => s.setActiveTool)
12
+
13
+ // Derive visibility directly from Zustand — no intermediate state
14
+ const visible = activeTool === 'image'
15
+
16
+ const handleGenerateAI = useCallback(() => {
17
+ setActiveTool('select')
18
+ toggleImageGenerateModal()
19
+ }, [setActiveTool, toggleImageGenerateModal])
20
+
21
+ const handleUpload = useCallback(() => {
22
+ setActiveTool('select')
23
+ if (window.openImageFilePicker) {
24
+ window.openImageFilePicker()
25
+ }
26
+ }, [setActiveTool])
27
+
28
+ // Keep the global bridge for engine calls
29
+ useEffect(() => {
30
+ window.__showImageSourcePicker = () => {
31
+ // The picker now shows automatically via Zustand activeTool,
32
+ // but keep this bridge so the engine call doesn't error
33
+ }
34
+ return () => {
35
+ delete window.__showImageSourcePicker
36
+ }
37
+ }, [])
38
+
39
+ // Close on outside click & keybinds
40
+ useEffect(() => {
41
+ if (!visible) return
42
+
43
+ const handleClick = (e) => {
44
+ if (ref.current && !ref.current.contains(e.target)) {
45
+ setActiveTool('select')
46
+ }
47
+ }
48
+
49
+ const handleKeyDown = (e) => {
50
+ if (e.key === 'Escape') {
51
+ setActiveTool('select')
52
+ }
53
+ if (e.key === 'g' || e.key === 'G') {
54
+ e.preventDefault()
55
+ e.stopPropagation()
56
+ handleGenerateAI()
57
+ }
58
+ if (e.key === 'u' || e.key === 'U') {
59
+ e.preventDefault()
60
+ e.stopPropagation()
61
+ handleUpload()
62
+ }
63
+ }
64
+
65
+ // Delay to avoid catching the click that opened it
66
+ const timer = setTimeout(() => {
67
+ document.addEventListener('mousedown', handleClick)
68
+ document.addEventListener('keydown', handleKeyDown, true)
69
+ }, 100)
70
+ return () => {
71
+ clearTimeout(timer)
72
+ document.removeEventListener('mousedown', handleClick)
73
+ document.removeEventListener('keydown', handleKeyDown, true)
74
+ }
75
+ }, [visible, setActiveTool, handleGenerateAI, handleUpload])
76
+
77
+ if (!visible) return null
78
+
79
+ // Compute position on each render when visible
80
+ let posX = 65
81
+ let posY = typeof window !== 'undefined' ? window.innerHeight / 2 - 50 : 300
82
+ if (typeof document !== 'undefined') {
83
+ const imageBtn = document.querySelector('button[title="Image (9)"]')
84
+ if (imageBtn) {
85
+ const rect = imageBtn.getBoundingClientRect()
86
+ posX = rect.right + 10
87
+ posY = rect.top - 10
88
+ }
89
+ }
90
+
91
+ return (
92
+ <div
93
+ ref={ref}
94
+ className="fixed z-[1100] font-[lixFont]"
95
+ style={{ left: posX, top: posY }}
96
+ >
97
+ <div className="bg-surface-card border border-border-light rounded-xl p-1.5 shadow-2xl shadow-black/40 flex flex-col gap-1 min-w-[200px]">
98
+ <button
99
+ onClick={handleGenerateAI}
100
+ className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-text-muted hover:text-accent-blue hover:bg-surface-hover transition-all group"
101
+ >
102
+ <div className="w-8 h-8 rounded-lg bg-accent-blue/10 flex items-center justify-center group-hover:bg-accent-blue/20 transition-all">
103
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-accent-blue">
104
+ <path d="M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3z" />
105
+ <path d="M18 14l1 3 3 1-3 1-1 3-1-3-3-1 3-1 1-3z" />
106
+ </svg>
107
+ </div>
108
+ <div className="flex-1 text-left">
109
+ <div className="text-sm font-medium">Generate with AI</div>
110
+ <div className="text-[10px] text-text-dim">10 generations &middot; 5 edits free</div>
111
+ </div>
112
+ <kbd className="text-[10px] text-text-dim bg-surface-dark px-1.5 py-0.5 rounded">G</kbd>
113
+ </button>
114
+
115
+ <div className="h-px bg-white/[0.06] mx-2" />
116
+
117
+ <button
118
+ onClick={handleUpload}
119
+ className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-text-muted hover:text-text-primary hover:bg-surface-hover transition-all group"
120
+ >
121
+ <div className="w-8 h-8 rounded-lg bg-surface-hover flex items-center justify-center group-hover:bg-surface-active transition-all">
122
+ <i className="bx bx-upload text-lg" />
123
+ </div>
124
+ <div className="flex-1 text-left">
125
+ <div className="text-sm font-medium">Upload from device</div>
126
+ <div className="text-[10px] text-text-dim">PNG, JPG, SVG, WebP</div>
127
+ </div>
128
+ <kbd className="text-[10px] text-text-dim bg-surface-dark px-1.5 py-0.5 rounded">U</kbd>
129
+ </button>
130
+ </div>
131
+ </div>
132
+ )
133
+ }
@@ -0,0 +1,120 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect } from 'react'
4
+ import { showToast } from '../../utils/toast'
5
+
6
+ export default function MultiSelectActions() {
7
+ const [count, setCount] = useState(0)
8
+ const [groupState, setGroupState] = useState('none') // 'none' | 'partial' | 'all'
9
+
10
+ useEffect(() => {
11
+ // Poll multi-selection state. Cheap — Set size + a single
12
+ // groupId pass over selectedShapes.
13
+ const interval = setInterval(() => {
14
+ const ms = window.multiSelection
15
+ const sel = ms?.selectedShapes
16
+ const n = sel ? sel.size : 0
17
+ setCount((prev) => (prev !== n ? n : prev))
18
+
19
+ if (n < 2) {
20
+ setGroupState('none')
21
+ return
22
+ }
23
+ let withId = 0
24
+ let groupIds = new Set()
25
+ sel.forEach((s) => {
26
+ if (s.groupId) {
27
+ withId++
28
+ groupIds.add(s.groupId)
29
+ }
30
+ })
31
+ // "all" = every shape has a groupId AND they all share the same one
32
+ if (withId === n && groupIds.size === 1) setGroupState('all')
33
+ else if (withId > 0) setGroupState('partial')
34
+ else setGroupState('none')
35
+ }, 200)
36
+ return () => clearInterval(interval)
37
+ }, [])
38
+
39
+ if (count < 2) return null
40
+
41
+ const handleFrame = () => {
42
+ if (window.frameSelectedShapes) {
43
+ window.frameSelectedShapes()
44
+ setCount(0)
45
+ }
46
+ }
47
+
48
+ const handleGroup = () => {
49
+ const ms = window.multiSelection
50
+ const sel = ms?.selectedShapes
51
+ if (!sel || sel.size < 2) return
52
+ const newId = `g-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`
53
+ sel.forEach((s) => { s.groupId = newId })
54
+ if (typeof ms.updateControls === 'function') ms.updateControls()
55
+ showToast(`Grouped ${sel.size} shapes`, { tone: 'success' })
56
+ }
57
+
58
+ const handleUngroup = () => {
59
+ const ms = window.multiSelection
60
+ const sel = ms?.selectedShapes
61
+ if (!sel || sel.size === 0) return
62
+ const groupIds = new Set()
63
+ sel.forEach((s) => { if (s.groupId) groupIds.add(s.groupId) })
64
+ if (groupIds.size === 0 || !Array.isArray(window.shapes)) return
65
+ let cleared = 0
66
+ for (const s of window.shapes) {
67
+ if (s.groupId && groupIds.has(s.groupId)) { s.groupId = null; cleared++ }
68
+ }
69
+ if (typeof ms.updateControls === 'function') ms.updateControls()
70
+ showToast(`Ungrouped ${cleared} shapes`, { tone: 'success' })
71
+ }
72
+
73
+ const grouped = groupState === 'all'
74
+
75
+ return (
76
+ <div className="absolute top-14 left-1/2 -translate-x-1/2 z-[1000] font-[lixFont]">
77
+ <div className="flex items-center gap-2 bg-surface/90 backdrop-blur-lg border border-border-light rounded-xl px-3 py-1.5 shadow-lg">
78
+ <span className="text-text-muted text-xs">
79
+ {count} selected{grouped && <span className="ml-1 text-accent-blue">· grouped</span>}
80
+ </span>
81
+ <div className="w-px h-4 bg-border-light" />
82
+
83
+ {/* Frame is only useful when shapes aren't already grouped — a
84
+ grouped set is conceptually a "frameless frame" already. */}
85
+ {!grouped && (
86
+ <button
87
+ onClick={handleFrame}
88
+ className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs text-accent-blue hover:bg-accent-blue/10 transition-all duration-150"
89
+ title="Wrap selection in a frame"
90
+ >
91
+ <i className="bx bx-crop text-sm" />
92
+ Frame it
93
+ </button>
94
+ )}
95
+
96
+ {/* Group / Ungroup toggle. Group is shown when nothing in the
97
+ selection is grouped yet; Ungroup once they're a single group. */}
98
+ {grouped ? (
99
+ <button
100
+ onClick={handleUngroup}
101
+ className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs text-yellow-400 hover:bg-yellow-500/10 transition-all duration-150"
102
+ title="Ungroup (Ctrl+Shift+G)"
103
+ >
104
+ <i className="bx bx-unlink text-sm" />
105
+ Ungroup
106
+ </button>
107
+ ) : (
108
+ <button
109
+ onClick={handleGroup}
110
+ className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs text-text-secondary hover:bg-surface-hover transition-all duration-150"
111
+ title="Group (Ctrl+G)"
112
+ >
113
+ <i className="bx bx-link text-sm" />
114
+ Group
115
+ </button>
116
+ )}
117
+ </div>
118
+ </div>
119
+ )
120
+ }
@@ -0,0 +1,145 @@
1
+ "use client"
2
+
3
+ import { useRef, useState, useEffect } from 'react'
4
+ import useSketchStore, { TOOLS } from '../../store/useSketchStore'
5
+ import useSketchEngine from '../../hooks/useSketchEngine'
6
+
7
+ const GRID_SIZE = 20
8
+
9
+ export default function SVGCanvas() {
10
+ const [svgReady, setSvgReady] = useState(false)
11
+ const svgRef = useRef(null)
12
+ const canvasBackground = useSketchStore((s) => s.canvasBackground)
13
+ const gridEnabled = useSketchStore((s) => s.gridEnabled)
14
+ const getCursor = useSketchStore((s) => s.getCursor)
15
+ const cursor = getCursor()
16
+
17
+ // viewBox is owned imperatively by the lixsketch engine (zoom/pan call
18
+ // svg.setAttribute('viewBox', …) directly). React must NOT re-render
19
+ // the viewBox prop or it'll snap back over engine writes — which is
20
+ // what was causing zoom deformity and the pan-release jump in split
21
+ // mode. The initial value is set in the effect below; React never
22
+ // rewrites it after that.
23
+ useEffect(() => {
24
+ const applyImperative = (w, h) => {
25
+ const zoom = window.currentZoom || 1
26
+ const cv = window.currentViewBox || { x: 0, y: 0 }
27
+ const vbW = w / zoom
28
+ const vbH = h / zoom
29
+ const x = cv.x || 0
30
+ const y = cv.y || 0
31
+ window.currentViewBox = { x, y, width: vbW, height: vbH }
32
+ const el = svgRef.current
33
+ if (el) el.setAttribute('viewBox', `${x} ${y} ${vbW} ${vbH}`)
34
+ }
35
+
36
+ const sync = () => {
37
+ const el = svgRef.current
38
+ if (!el) return
39
+ const rect = el.getBoundingClientRect()
40
+ const w = Math.max(1, Math.round(rect.width))
41
+ const h = Math.max(1, Math.round(rect.height))
42
+ applyImperative(w, h)
43
+ }
44
+
45
+ sync()
46
+ setSvgReady(true)
47
+
48
+ window.addEventListener('resize', sync)
49
+
50
+ let ro
51
+ if (typeof ResizeObserver !== 'undefined' && svgRef.current) {
52
+ ro = new ResizeObserver(sync)
53
+ ro.observe(svgRef.current)
54
+ }
55
+
56
+ return () => {
57
+ window.removeEventListener('resize', sync)
58
+ if (ro) ro.disconnect()
59
+ }
60
+ }, [])
61
+
62
+ // Close icon sidebar when clicking on canvas without an icon ready to place
63
+ useEffect(() => {
64
+ const svg = svgRef.current
65
+ if (!svg) return
66
+
67
+ const handleCanvasClick = () => {
68
+ const activeTool = useSketchStore.getState().activeTool
69
+ // If icon tool is active but no icon is queued for placement, close the sidebar
70
+ if (activeTool === TOOLS.ICON && !window.isIconToolActive) {
71
+ useSketchStore.getState().setActiveTool(TOOLS.SELECT)
72
+ }
73
+ }
74
+
75
+ svg.addEventListener('pointerdown', handleCanvasClick)
76
+ return () => svg.removeEventListener('pointerdown', handleCanvasClick)
77
+ }, [svgReady])
78
+
79
+ // Initialize the imperative sketch engine on this SVG element
80
+ useSketchEngine(svgRef, svgReady)
81
+
82
+ // Expose grid state to engine
83
+ useEffect(() => {
84
+ window.__gridEnabled = gridEnabled
85
+ }, [gridEnabled])
86
+
87
+ return (
88
+ <svg
89
+ id="freehand-canvas"
90
+ ref={svgRef}
91
+ className="absolute inset-0 w-full h-full"
92
+ style={{
93
+ background: canvasBackground,
94
+ cursor,
95
+ userSelect: 'none',
96
+ WebkitUserSelect: 'none',
97
+ touchAction: 'none',
98
+ }}
99
+ preserveAspectRatio="none"
100
+ suppressHydrationWarning
101
+ >
102
+ {gridEnabled && (
103
+ <defs>
104
+ <pattern
105
+ id="grid-small"
106
+ width={GRID_SIZE}
107
+ height={GRID_SIZE}
108
+ patternUnits="userSpaceOnUse"
109
+ >
110
+ <path
111
+ d={`M ${GRID_SIZE} 0 L 0 0 0 ${GRID_SIZE}`}
112
+ fill="none"
113
+ stroke="rgba(255,255,255,0.06)"
114
+ strokeWidth="0.5"
115
+ />
116
+ </pattern>
117
+ <pattern
118
+ id="grid-large"
119
+ width={GRID_SIZE * 5}
120
+ height={GRID_SIZE * 5}
121
+ patternUnits="userSpaceOnUse"
122
+ >
123
+ <rect width={GRID_SIZE * 5} height={GRID_SIZE * 5} fill="url(#grid-small)" />
124
+ <path
125
+ d={`M ${GRID_SIZE * 5} 0 L 0 0 0 ${GRID_SIZE * 5}`}
126
+ fill="none"
127
+ stroke="rgba(255,255,255,0.12)"
128
+ strokeWidth="0.8"
129
+ />
130
+ </pattern>
131
+ </defs>
132
+ )}
133
+ {gridEnabled && (
134
+ <rect
135
+ x="-100000"
136
+ y="-100000"
137
+ width="200000"
138
+ height="200000"
139
+ fill="url(#grid-large)"
140
+ style={{ pointerEvents: 'none' }}
141
+ />
142
+ )}
143
+ </svg>
144
+ )
145
+ }
@@ -0,0 +1,29 @@
1
+ 'use client'
2
+
3
+ import useUIStore from '../../store/useUIStore'
4
+
5
+ const STATUS_CONFIG = {
6
+ cloud: { color: 'bg-green-400', label: 'Synced to cloud' },
7
+ local: { color: 'bg-yellow-400', label: 'Saved locally' },
8
+ failed: { color: 'bg-red-400', label: 'Sync failed' },
9
+ }
10
+
11
+ export default function SaveStatusIndicator() {
12
+ const saveStatus = useUIStore((s) => s.saveStatus)
13
+
14
+ if (saveStatus === 'idle') return null
15
+
16
+ const config = STATUS_CONFIG[saveStatus]
17
+ if (!config) return null
18
+
19
+ return (
20
+ <div className="fixed bottom-3 left-1/2 -translate-x-1/2 z-[999] flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-card/80 backdrop-blur-md border border-border-light font-[lixFont] pointer-events-none select-none">
21
+ <span
22
+ className={`w-2 h-2 rounded-full shrink-0 ${config.color}`}
23
+ />
24
+ <span className="text-text-dim text-[10px] tracking-wide">
25
+ {config.label}
26
+ </span>
27
+ </div>
28
+ )
29
+ }