@elixpo/lixsketch 5.4.7 → 5.5.1

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 +5349 -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 +482 -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,287 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect, useRef, useCallback } from 'react'
4
+ import useUIStore from '../../store/useUIStore'
5
+
6
+ const SCALES = [1, 2, 3]
7
+
8
+ function getCleanSVG() {
9
+ const svgEl = window.svg
10
+ if (!svgEl) return null
11
+ const clone = svgEl.cloneNode(true)
12
+ clone.querySelectorAll(
13
+ '[data-selection], .selection-handle, .resize-handle, .rotation-handle, .anchor, .rotate-anchor'
14
+ ).forEach((el) => el.remove())
15
+ return clone
16
+ }
17
+
18
+ function renderToCanvas(clone, scale, bgColor) {
19
+ return new Promise((resolve) => {
20
+ const svgData = new XMLSerializer().serializeToString(clone)
21
+ const vb = window.svg.viewBox.baseVal
22
+ const canvas = document.createElement('canvas')
23
+ canvas.width = vb.width * scale
24
+ canvas.height = vb.height * scale
25
+ const ctx = canvas.getContext('2d')
26
+ ctx.scale(scale, scale)
27
+
28
+ const img = new Image()
29
+ const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' })
30
+ const url = URL.createObjectURL(blob)
31
+
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 = () => {
42
+ URL.revokeObjectURL(url)
43
+ resolve(null)
44
+ }
45
+ img.src = url
46
+ })
47
+ }
48
+
49
+ export default function ExportImageModal() {
50
+ const open = useUIStore((s) => s.exportImageModalOpen)
51
+ const toggleModal = useUIStore((s) => s.toggleExportImageModal)
52
+
53
+ const [scale, setScale] = useState(2)
54
+ const [bgMode, setBgMode] = useState('dark') // 'dark' | 'light' | 'none'
55
+ const [previewUrl, setPreviewUrl] = useState(null)
56
+ const previewRef = useRef(null)
57
+
58
+ const getBgColor = useCallback(() => {
59
+ if (bgMode === 'dark') return '#121212'
60
+ if (bgMode === 'light') return '#ffffff'
61
+ return null // transparent
62
+ }, [bgMode])
63
+
64
+ // Generate preview whenever settings change
65
+ useEffect(() => {
66
+ if (!open) return
67
+ let cancelled = false
68
+
69
+ const generate = async () => {
70
+ const clone = getCleanSVG()
71
+ if (!clone) return
72
+ // Preview at 1x for speed
73
+ const canvas = await renderToCanvas(clone, 1, getBgColor())
74
+ if (cancelled || !canvas) return
75
+ setPreviewUrl(canvas.toDataURL('image/png'))
76
+ }
77
+ generate()
78
+
79
+ return () => { cancelled = true }
80
+ }, [open, bgMode, getBgColor])
81
+
82
+ if (!open) return null
83
+
84
+ const handleExportPNG = async () => {
85
+ const clone = getCleanSVG()
86
+ if (!clone) return
87
+ const canvas = await renderToCanvas(clone, scale, getBgColor())
88
+ if (!canvas) return
89
+
90
+ canvas.toBlob((blob) => {
91
+ const a = document.createElement('a')
92
+ a.href = URL.createObjectURL(blob)
93
+ a.download = `lixsketch-export-${scale}x.png`
94
+ document.body.appendChild(a)
95
+ a.click()
96
+ document.body.removeChild(a)
97
+ }, 'image/png')
98
+ toggleModal()
99
+ }
100
+
101
+ const handleExportSVG = () => {
102
+ const clone = getCleanSVG()
103
+ if (!clone) return
104
+ // Apply background as a rect if needed
105
+ const bg = getBgColor()
106
+ if (bg) {
107
+ const ns = 'http://www.w3.org/2000/svg'
108
+ const rect = document.createElementNS(ns, 'rect')
109
+ const vb = window.svg.viewBox.baseVal
110
+ rect.setAttribute('width', vb.width)
111
+ rect.setAttribute('height', vb.height)
112
+ rect.setAttribute('fill', bg)
113
+ clone.insertBefore(rect, clone.firstChild)
114
+ }
115
+ const svgData = new XMLSerializer().serializeToString(clone)
116
+ const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' })
117
+ const a = document.createElement('a')
118
+ a.href = URL.createObjectURL(blob)
119
+ a.download = 'lixsketch-export.svg'
120
+ document.body.appendChild(a)
121
+ a.click()
122
+ document.body.removeChild(a)
123
+ toggleModal()
124
+ }
125
+
126
+ const handleCopyPNG = async () => {
127
+ const clone = getCleanSVG()
128
+ if (!clone) return
129
+ const canvas = await renderToCanvas(clone, scale, getBgColor())
130
+ if (!canvas) return
131
+
132
+ canvas.toBlob((blob) => {
133
+ if (!blob) return
134
+ navigator.clipboard
135
+ .write([new ClipboardItem({ 'image/png': blob })])
136
+ .catch((err) => console.warn('Clipboard write failed:', err))
137
+ }, 'image/png')
138
+ toggleModal()
139
+ }
140
+
141
+ const handleCopySVG = () => {
142
+ const clone = getCleanSVG()
143
+ if (!clone) return
144
+ const svgData = new XMLSerializer().serializeToString(clone)
145
+ navigator.clipboard.writeText(svgData).catch((err) =>
146
+ console.warn('Clipboard write failed:', err)
147
+ )
148
+ toggleModal()
149
+ }
150
+
151
+ return (
152
+ <div
153
+ className="fixed inset-0 z-[9999] flex items-center justify-center font-[lixFont]"
154
+ onClick={toggleModal}
155
+ >
156
+ <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
157
+
158
+ <div
159
+ className="relative bg-surface-card border border-border-light rounded-2xl w-full max-w-[720px] mx-4 overflow-hidden"
160
+ onClick={(e) => e.stopPropagation()}
161
+ >
162
+ {/* Header */}
163
+ <div className="flex items-center justify-between px-6 pt-5 pb-3">
164
+ <h2 className="text-text-primary text-base font-medium">Export Image</h2>
165
+ <button
166
+ onClick={toggleModal}
167
+ className="w-7 h-7 flex items-center justify-center rounded-lg text-text-muted hover:text-text-primary hover:bg-surface-hover transition-all duration-200"
168
+ >
169
+ <i className="bx bx-x text-xl" />
170
+ </button>
171
+ </div>
172
+
173
+ <hr className="border-border-light mx-6" />
174
+
175
+ {/* Body - two columns */}
176
+ <div className="flex gap-0">
177
+ {/* Left - Preview */}
178
+ <div className="flex-1 p-6 flex items-center justify-center min-h-[300px]">
179
+ <div
180
+ ref={previewRef}
181
+ className="w-full max-h-[320px] rounded-xl overflow-hidden border border-border-light flex items-center justify-center"
182
+ style={{ background: bgMode === 'none' ? 'repeating-conic-gradient(#2a2a35 0% 25%, #1e1e28 0% 50%) 0 0 / 16px 16px' : getBgColor() }}
183
+ >
184
+ {previewUrl ? (
185
+ <img
186
+ src={previewUrl}
187
+ alt="Export preview"
188
+ className="w-full h-full object-contain max-h-[320px]"
189
+ />
190
+ ) : (
191
+ <span className="text-text-dim text-xs">Generating preview...</span>
192
+ )}
193
+ </div>
194
+ </div>
195
+
196
+ {/* Right - Controls */}
197
+ <div className="w-[240px] border-l border-border-light p-5 flex flex-col gap-5">
198
+ {/* Background */}
199
+ <div>
200
+ <p className="text-text-dim text-xs uppercase tracking-wider mb-2">Background</p>
201
+ <div className="flex items-center gap-1">
202
+ {[
203
+ { value: 'dark', label: 'Dark', icon: 'bxs-moon' },
204
+ { value: 'light', label: 'Light', icon: 'bxs-sun' },
205
+ { value: 'none', label: 'None', icon: 'bx-hide' },
206
+ ].map((opt) => (
207
+ <button
208
+ key={opt.value}
209
+ onClick={() => setBgMode(opt.value)}
210
+ className={`flex-1 flex items-center justify-center gap-1 py-1.5 rounded-lg text-xs transition-all duration-200 ${
211
+ bgMode === opt.value
212
+ ? 'bg-accent text-text-primary'
213
+ : 'text-text-muted hover:bg-surface-hover'
214
+ }`}
215
+ >
216
+ <i className={`bx ${opt.icon} text-xs`} />
217
+ {opt.label}
218
+ </button>
219
+ ))}
220
+ </div>
221
+ </div>
222
+
223
+ {/* Scale */}
224
+ <div>
225
+ <p className="text-text-dim text-xs uppercase tracking-wider mb-2">Scale</p>
226
+ <div className="flex items-center gap-1">
227
+ {SCALES.map((s) => (
228
+ <button
229
+ key={s}
230
+ onClick={() => setScale(s)}
231
+ className={`flex-1 py-1.5 rounded-lg text-xs transition-all duration-200 ${
232
+ scale === s
233
+ ? 'bg-accent text-text-primary'
234
+ : 'text-text-muted hover:bg-surface-hover'
235
+ }`}
236
+ >
237
+ {s}x
238
+ </button>
239
+ ))}
240
+ </div>
241
+ </div>
242
+
243
+ <hr className="border-border-light" />
244
+
245
+ {/* Export buttons */}
246
+ <div className="flex flex-col gap-2">
247
+ <button
248
+ onClick={handleExportPNG}
249
+ className="w-full flex items-center gap-2 px-3 py-2.5 rounded-lg bg-accent-blue hover:bg-accent-blue-hover text-text-primary text-xs transition-all duration-200"
250
+ >
251
+ <i className="bx bx-image text-sm" />
252
+ Export as PNG
253
+ </button>
254
+ <button
255
+ onClick={handleExportSVG}
256
+ className="w-full flex items-center gap-2 px-3 py-2.5 rounded-lg bg-surface hover:bg-surface-hover border border-border text-text-secondary text-xs transition-all duration-200"
257
+ >
258
+ <i className="bx bx-code-alt text-sm" />
259
+ Export as SVG
260
+ </button>
261
+ </div>
262
+
263
+ <hr className="border-border-light" />
264
+
265
+ {/* Clipboard buttons */}
266
+ <div className="flex flex-col gap-2">
267
+ <button
268
+ onClick={handleCopyPNG}
269
+ className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-text-secondary text-xs hover:bg-surface-hover border border-border transition-all duration-200"
270
+ >
271
+ <i className="bx bx-clipboard text-sm" />
272
+ Copy as PNG
273
+ </button>
274
+ <button
275
+ onClick={handleCopySVG}
276
+ className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-text-secondary text-xs hover:bg-surface-hover border border-border transition-all duration-200"
277
+ >
278
+ <i className="bx bx-copy text-sm" />
279
+ Copy as SVG
280
+ </button>
281
+ </div>
282
+ </div>
283
+ </div>
284
+ </div>
285
+ </div>
286
+ )
287
+ }
@@ -0,0 +1,174 @@
1
+ "use client"
2
+
3
+ import { useCallback, useEffect, useRef, useState } from 'react'
4
+ import useUIStore from '../../store/useUIStore'
5
+
6
+ const SAMPLE = `# One equation per line. Comments start with #.
7
+ # Hit Cmd/Ctrl+Enter to render onto the canvas.
8
+
9
+ y = x^2
10
+ y = sin(x) * 3
11
+ y = -x + 2
12
+ `
13
+
14
+ const DEFAULT_SETTINGS = { xMin: -10, xMax: 10, yMin: -10, yMax: 10, showGrid: true }
15
+
16
+ function parseEquations(text) {
17
+ return text
18
+ .split(/\r?\n/)
19
+ .map((s) => s.trim())
20
+ .filter((s) => s && !s.startsWith('#'))
21
+ }
22
+
23
+ /**
24
+ * Graph modal — plots client-side via the engine's GraphRenderer. No
25
+ * inference; the math is parsed by GraphMathParser.parseExpression.
26
+ * Inserts the rendered graph onto the canvas through the bridge
27
+ * SketchEngine.init() exposes as `window.__graphRenderer`.
28
+ */
29
+ export default function GraphModal() {
30
+ const open = useUIStore((s) => s.graphModalOpen)
31
+ const toggle = useUIStore((s) => s.toggleGraphModal)
32
+ const [source, setSource] = useState(SAMPLE)
33
+ const [settings, setSettings] = useState(DEFAULT_SETTINGS)
34
+ const [busy, setBusy] = useState(false)
35
+ const [status, setStatus] = useState(null)
36
+ const taRef = useRef(null)
37
+
38
+ useEffect(() => {
39
+ if (!open) return
40
+ requestAnimationFrame(() => taRef.current?.focus())
41
+ const onKey = (e) => {
42
+ if (e.key === 'Escape') toggle()
43
+ }
44
+ document.addEventListener('keydown', onKey)
45
+ return () => document.removeEventListener('keydown', onKey)
46
+ }, [open, toggle])
47
+
48
+ const render = useCallback(async () => {
49
+ if (busy) return
50
+ setBusy(true)
51
+ setStatus(null)
52
+ try {
53
+ const equations = parseEquations(source)
54
+ if (equations.length === 0) {
55
+ setStatus({ tone: 'error', message: 'Add at least one equation.' })
56
+ return
57
+ }
58
+ // Validate via the engine's parser before handing to the renderer.
59
+ const { parseExpression } = await import('../../../core/GraphMathParser.js')
60
+ const bad = equations.find((eq) => !parseExpression(eq))
61
+ if (bad) {
62
+ setStatus({ tone: 'error', message: `Could not parse: ${bad}` })
63
+ return
64
+ }
65
+ if (typeof window.__graphRenderer === 'function') {
66
+ window.__graphRenderer(equations, settings)
67
+ setStatus({ tone: 'success', message: `Plotted ${equations.length} equation(s).` })
68
+ setTimeout(() => { toggle() }, 700)
69
+ } else {
70
+ setStatus({ tone: 'error', message: 'Graph engine not ready yet — wait a moment and retry.' })
71
+ }
72
+ } catch (err) {
73
+ console.warn('[GraphModal] render failed:', err)
74
+ setStatus({ tone: 'error', message: err?.message || 'Render failed.' })
75
+ } finally {
76
+ setBusy(false)
77
+ }
78
+ }, [source, settings, busy, toggle])
79
+
80
+ const handleKeyDown = useCallback((e) => {
81
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
82
+ e.preventDefault()
83
+ render()
84
+ }
85
+ }, [render])
86
+
87
+ const setRange = (key) => (e) => {
88
+ const v = parseFloat(e.target.value)
89
+ if (Number.isFinite(v)) setSettings((s) => ({ ...s, [key]: v }))
90
+ }
91
+
92
+ if (!open) return null
93
+
94
+ return (
95
+ <div
96
+ className="fixed inset-0 z-[1100] flex items-center justify-center bg-black/60 backdrop-blur-sm font-[lixFont]"
97
+ onClick={toggle}
98
+ role="dialog"
99
+ aria-modal="true"
100
+ >
101
+ <div
102
+ onClick={(e) => e.stopPropagation()}
103
+ className="relative w-[560px] max-w-[94vw] bg-surface border border-border-light rounded-2xl p-5 shadow-2xl flex flex-col gap-3"
104
+ >
105
+ <button
106
+ onClick={toggle}
107
+ className="absolute top-3 right-3 w-7 h-7 flex items-center justify-center rounded-md text-text-muted hover:text-text-primary hover:bg-surface-hover transition-colors"
108
+ title="Close (Esc)"
109
+ >
110
+ <i className="bx bx-x text-lg" />
111
+ </button>
112
+
113
+ <div className="flex items-center gap-2">
114
+ <div className="w-8 h-8 rounded-lg bg-accent-blue/10 border border-accent-blue/30 flex items-center justify-center">
115
+ <i className="bx bx-line-chart text-accent-blue" />
116
+ </div>
117
+ <div className="flex-1">
118
+ <h2 className="text-text-primary text-sm font-medium">Graph</h2>
119
+ <p className="text-text-dim text-[11px]">One equation per line — plotted on the canvas. No AI involved.</p>
120
+ </div>
121
+ </div>
122
+
123
+ <textarea
124
+ ref={taRef}
125
+ value={source}
126
+ onChange={(e) => setSource(e.target.value)}
127
+ onKeyDown={handleKeyDown}
128
+ spellCheck={false}
129
+ className="w-full h-[200px] resize-none bg-surface-card border border-border-light rounded-lg px-3 py-2 text-[12px] text-text-primary font-[lixCode] outline-none focus:border-accent-blue/50"
130
+ />
131
+
132
+ <div className="grid grid-cols-4 gap-2 text-[11px]">
133
+ <label className="flex flex-col gap-0.5">
134
+ <span className="text-text-dim">xMin</span>
135
+ <input type="number" value={settings.xMin} onChange={setRange('xMin')}
136
+ className="bg-surface-card border border-border-light rounded px-2 py-1 text-text-primary outline-none focus:border-accent-blue/50" />
137
+ </label>
138
+ <label className="flex flex-col gap-0.5">
139
+ <span className="text-text-dim">xMax</span>
140
+ <input type="number" value={settings.xMax} onChange={setRange('xMax')}
141
+ className="bg-surface-card border border-border-light rounded px-2 py-1 text-text-primary outline-none focus:border-accent-blue/50" />
142
+ </label>
143
+ <label className="flex flex-col gap-0.5">
144
+ <span className="text-text-dim">yMin</span>
145
+ <input type="number" value={settings.yMin} onChange={setRange('yMin')}
146
+ className="bg-surface-card border border-border-light rounded px-2 py-1 text-text-primary outline-none focus:border-accent-blue/50" />
147
+ </label>
148
+ <label className="flex flex-col gap-0.5">
149
+ <span className="text-text-dim">yMax</span>
150
+ <input type="number" value={settings.yMax} onChange={setRange('yMax')}
151
+ className="bg-surface-card border border-border-light rounded px-2 py-1 text-text-primary outline-none focus:border-accent-blue/50" />
152
+ </label>
153
+ </div>
154
+
155
+ <div className="flex items-center justify-between">
156
+ {status ? (
157
+ <span className={`text-[11px] ${status.tone === 'error' ? 'text-red-400' : 'text-green-400'}`}>
158
+ {status.message}
159
+ </span>
160
+ ) : (
161
+ <span className="text-text-dim text-[11px]">Cmd/Ctrl+Enter to render · Esc to close</span>
162
+ )}
163
+ <button
164
+ onClick={render}
165
+ disabled={busy}
166
+ className="px-3 py-1.5 rounded-lg bg-accent-blue text-text-primary text-[12px] font-medium hover:bg-accent-blue-hover transition-colors disabled:opacity-50 disabled:pointer-events-none"
167
+ >
168
+ {busy ? 'Plotting…' : 'Plot'}
169
+ </button>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ )
174
+ }
@@ -0,0 +1,208 @@
1
+ "use client"
2
+
3
+ import { useState } from 'react'
4
+ import useUIStore from '../../store/useUIStore'
5
+
6
+ const TOOL_SHORTCUTS = [
7
+ { keys: 'H', action: 'Pan' },
8
+ { keys: 'V / 1', action: 'Selection' },
9
+ { keys: 'R / 2', action: 'Rectangle' },
10
+ { keys: '3', action: 'Diamond' },
11
+ { keys: 'O / 4', action: 'Circle' },
12
+ { keys: 'A / 5', action: 'Arrow' },
13
+ { keys: 'L / 6', action: 'Line' },
14
+ { keys: 'P / 7', action: 'Freehand' },
15
+ { keys: 'T / 8', action: 'Text' },
16
+ { keys: '9', action: 'Image' },
17
+ { keys: 'E / 0', action: 'Eraser' },
18
+ { keys: 'F', action: 'Frame' },
19
+ { keys: 'K', action: 'Laser' },
20
+ ]
21
+
22
+ const ACTION_SHORTCUTS = [
23
+ { keys: 'Ctrl+A', action: 'Select All' },
24
+ { keys: 'Ctrl+G', action: 'Group' },
25
+ { keys: 'Ctrl+Shift+G', action: 'Ungroup' },
26
+ { keys: 'Ctrl+D', action: 'Duplicate' },
27
+ { keys: 'Ctrl+S', action: 'Quick Save (canvas + doc)' },
28
+ { keys: 'Ctrl+C', action: 'Copy' },
29
+ { keys: 'Ctrl+V', action: 'Paste' },
30
+ { keys: 'Ctrl+Z', action: 'Undo' },
31
+ { keys: 'Ctrl+Shift+Z', action: 'Redo' },
32
+ { keys: 'Esc', action: 'Deselect' },
33
+ { keys: 'Del / Backspace', action: 'Delete' },
34
+ { keys: 'Space (hold)', action: 'Pan' },
35
+ { keys: 'Shift (hold)', action: 'Straight Draw' },
36
+ ]
37
+
38
+ const VIEW_SHORTCUTS = [
39
+ { keys: 'Ctrl++', action: 'Zoom In' },
40
+ { keys: 'Ctrl+-', action: 'Zoom Out' },
41
+ { keys: 'Ctrl+0', action: 'Reset Zoom' },
42
+ { keys: "Ctrl+'", action: 'Toggle Grid' },
43
+ { keys: 'Ctrl+/', action: 'Command Palette' },
44
+ ]
45
+
46
+ // ── Doc editor shortcuts ──────────────────────────────────────────
47
+ // Markdown shortcuts auto-convert when typed at the start of a line.
48
+ const DOC_BLOCK_SHORTCUTS = [
49
+ { keys: '# Space', action: 'Heading 1' },
50
+ { keys: '## Space', action: 'Heading 2' },
51
+ { keys: '### Space', action: 'Heading 3' },
52
+ { keys: '> Space', action: 'Quote' },
53
+ { keys: '- Space', action: 'Bulleted list' },
54
+ { keys: '1. Space', action: 'Numbered list' },
55
+ { keys: '[] Space', action: 'Checklist' },
56
+ { keys: '``` Space', action: 'Code block' },
57
+ { keys: '--- Enter', action: 'Divider' },
58
+ { keys: '/', action: 'Block menu' },
59
+ ]
60
+
61
+ const DOC_INLINE_SHORTCUTS = [
62
+ { keys: 'Ctrl+B', action: 'Bold' },
63
+ { keys: 'Ctrl+I', action: 'Italic' },
64
+ { keys: 'Ctrl+U', action: 'Underline' },
65
+ { keys: 'Ctrl+Shift+S', action: 'Strikethrough' },
66
+ { keys: 'Ctrl+E', action: 'Inline code' },
67
+ { keys: 'Tab', action: 'Indent block' },
68
+ { keys: 'Shift+Tab', action: 'Outdent block' },
69
+ { keys: 'Ctrl+Z', action: 'Undo' },
70
+ { keys: 'Ctrl+Shift+Z', action: 'Redo' },
71
+ ]
72
+
73
+ const TABS = [
74
+ { id: 'canvas', label: 'Canvas', icon: 'bx-pen' },
75
+ { id: 'docs', label: 'Document', icon: 'bxs-notepad' },
76
+ ]
77
+
78
+ function ShortcutRow({ keys, action }) {
79
+ return (
80
+ <div className="flex items-center justify-between py-1.5">
81
+ <span className="text-text-secondary text-xs">{action}</span>
82
+ <div className="flex items-center gap-1">
83
+ {keys.split('+').map((key, i) => (
84
+ <span key={i}>
85
+ {i > 0 && <span className="text-text-dim text-xs mx-0.5">+</span>}
86
+ <kbd className="px-1.5 py-0.5 bg-surface-dark rounded text-text-muted text-xs border border-border font-[lixFont]">
87
+ {key.trim()}
88
+ </kbd>
89
+ </span>
90
+ ))}
91
+ </div>
92
+ </div>
93
+ )
94
+ }
95
+
96
+ function ShortcutSection({ title, shortcuts }) {
97
+ return (
98
+ <div>
99
+ <h3 className="text-text-dim text-xs uppercase tracking-wider mb-2">{title}</h3>
100
+ {shortcuts.map((s) => (
101
+ <ShortcutRow key={s.action} keys={s.keys} action={s.action} />
102
+ ))}
103
+ </div>
104
+ )
105
+ }
106
+
107
+ export default function HelpModal() {
108
+ const open = useUIStore((s) => s.helpModalOpen)
109
+ const toggleHelpModal = useUIStore((s) => s.toggleHelpModal)
110
+ const [activeTab, setActiveTab] = useState('canvas')
111
+
112
+ if (!open) return null
113
+
114
+ return (
115
+ <div
116
+ className="fixed inset-0 z-[9999] flex items-center justify-center font-[lixFont]"
117
+ onClick={toggleHelpModal}
118
+ >
119
+ <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
120
+
121
+ <div
122
+ className="relative bg-surface-card border border-border-light rounded-2xl w-full max-w-[800px] mx-4 max-h-[85vh] flex flex-col"
123
+ onClick={(e) => e.stopPropagation()}
124
+ >
125
+ {/* Header */}
126
+ <div className="flex items-center justify-between px-6 pt-5 pb-3">
127
+ <h2 className="text-text-primary text-base font-medium">Shortcuts</h2>
128
+ <button
129
+ onClick={toggleHelpModal}
130
+ className="w-7 h-7 flex items-center justify-center rounded-lg text-text-muted hover:text-text-primary hover:bg-surface-hover transition-all duration-200"
131
+ >
132
+ <i className="bx bx-x text-xl" />
133
+ </button>
134
+ </div>
135
+
136
+ {/* Tabs */}
137
+ <div className="flex items-center gap-1 px-6 pb-3">
138
+ {TABS.map((tab) => (
139
+ <button
140
+ key={tab.id}
141
+ onClick={() => setActiveTab(tab.id)}
142
+ className={`px-3 py-1.5 rounded-lg text-xs flex items-center gap-1.5 transition-all duration-200 ${
143
+ activeTab === tab.id
144
+ ? 'bg-surface-hover text-text-primary'
145
+ : 'text-text-muted hover:text-text-secondary hover:bg-surface-hover/50'
146
+ }`}
147
+ >
148
+ <i className={`bx ${tab.icon} text-sm`} />
149
+ {tab.label}
150
+ </button>
151
+ ))}
152
+ </div>
153
+
154
+ <hr className="border-border-light mx-6" />
155
+
156
+ {/* Content - scrollable */}
157
+ <div className="flex-1 overflow-y-auto no-scrollbar px-6 py-4">
158
+ {activeTab === 'canvas' && (
159
+ <div className="grid grid-cols-2 gap-6">
160
+ <ShortcutSection title="Tools" shortcuts={TOOL_SHORTCUTS} />
161
+ <div className="flex flex-col gap-4">
162
+ <ShortcutSection title="Actions" shortcuts={ACTION_SHORTCUTS} />
163
+ <ShortcutSection title="View" shortcuts={VIEW_SHORTCUTS} />
164
+ </div>
165
+ </div>
166
+ )}
167
+
168
+ {activeTab === 'docs' && (
169
+ <div className="grid grid-cols-2 gap-6">
170
+ <ShortcutSection title="Block markdown" shortcuts={DOC_BLOCK_SHORTCUTS} />
171
+ <ShortcutSection title="Formatting" shortcuts={DOC_INLINE_SHORTCUTS} />
172
+ </div>
173
+ )}
174
+ </div>
175
+
176
+ {/* Footer links */}
177
+ <hr className="border-border-light mx-6" />
178
+ <div className="flex items-center gap-3 px-6 py-4 flex-wrap">
179
+ <a
180
+ href="/docs"
181
+ className="flex items-center gap-2 px-3 py-2 rounded-lg bg-accent-blue/10 hover:bg-accent-blue/20 border border-accent-blue/20 text-accent-blue text-xs transition-all duration-200"
182
+ >
183
+ <i className="bx bx-book-open text-sm" />
184
+ Documentation
185
+ </a>
186
+ <a
187
+ href="https://github.com/elixpo/sketch.elixpo"
188
+ target="_blank"
189
+ rel="noopener noreferrer"
190
+ className="flex items-center gap-2 px-3 py-2 rounded-lg bg-surface hover:bg-surface-hover border border-border text-text-secondary text-xs transition-all duration-200"
191
+ >
192
+ <i className="bx bxl-github text-sm" />
193
+ View Repository
194
+ </a>
195
+ <a
196
+ href="https://github.com/elixpo/sketch.elixpo/issues"
197
+ target="_blank"
198
+ rel="noopener noreferrer"
199
+ className="flex items-center gap-2 px-3 py-2 rounded-lg bg-surface hover:bg-surface-hover border border-border text-text-secondary text-xs transition-all duration-200"
200
+ >
201
+ <i className="bx bx-bug text-sm" />
202
+ Report An Issue
203
+ </a>
204
+ </div>
205
+ </div>
206
+ </div>
207
+ )
208
+ }