@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,263 @@
1
+ "use client"
2
+
3
+ import useSketchStore, { TOOLS } from '../../store/useSketchStore'
4
+ import useUIStore from '../../store/useUIStore'
5
+ import ShapeSidebar, { ToolbarButton, Divider, LayerControls } from './ShapeSidebar'
6
+ import { useState, useCallback, useEffect } from 'react'
7
+ import { useTranslation } from '../../hooks/useTranslation'
8
+ import { compressImage } from '../../../utils/imageCompressor.js'
9
+ import { IMAGE_ACCEPT_ATTR, isAllowedImage } from '../../../utils/allowedImageTypes.js'
10
+
11
+ const FILL_STYLES = [
12
+ { id: 'transparent', label: 'None', icon: 'bx-x' },
13
+ { id: 'solid', label: 'Solid', icon: 'bxs-square' },
14
+ { id: 'grid', label: 'Grid', icon: 'bx-grid-alt' },
15
+ ]
16
+
17
+ const FILL_COLORS = [
18
+ '#1e1e28',
19
+ '#13171C',
20
+ '#1a1a2e',
21
+ '#0d1117',
22
+ '#2d2d3a',
23
+ '#ffffff',
24
+ ]
25
+
26
+ export default function FrameSidebar() {
27
+ const { t } = useTranslation()
28
+ const activeTool = useSketchStore((s) => s.activeTool)
29
+ const selectedShapeSidebar = useSketchStore((s) => s.selectedShapeSidebar)
30
+ const toggleAIModal = useUIStore((s) => s.toggleAIModal)
31
+ const [frameName, setFrameName] = useState('Frame 1')
32
+ const [isGraph, setIsGraph] = useState(false)
33
+ const [fillStyle, setFillStyle] = useState('transparent')
34
+ const [fillColor, setFillColor] = useState('#1e1e28')
35
+
36
+ // Sync state from the actual selected frame when sidebar opens
37
+ useEffect(() => {
38
+ if (selectedShapeSidebar === 'frame' || activeTool === TOOLS.FRAME) {
39
+ const shape = typeof window !== 'undefined' ? window.currentShape : null
40
+ if (shape && shape.shapeName === 'frame') {
41
+ if (shape.frameName) setFrameName(shape.frameName)
42
+ setIsGraph(shape._frameType === 'graph')
43
+ setFillStyle(shape.fillStyle || 'transparent')
44
+ setFillColor(shape.fillColor || '#1e1e28')
45
+ } else {
46
+ setIsGraph(false)
47
+ setFillStyle('transparent')
48
+ setFillColor('#1e1e28')
49
+ }
50
+ }
51
+ }, [selectedShapeSidebar, activeTool])
52
+
53
+ const updateName = useCallback((e) => {
54
+ const name = e.target.value
55
+ setFrameName(name)
56
+ const shape = window.currentShape
57
+ if (shape && shape.shapeName === 'frame') {
58
+ shape.frameName = name
59
+ shape.draw()
60
+ }
61
+ }, [])
62
+
63
+ const updateFillStyle = useCallback((style) => {
64
+ setFillStyle(style)
65
+ const shape = window.currentShape
66
+ if (shape && shape.shapeName === 'frame') {
67
+ shape.fillStyle = style
68
+ shape.draw()
69
+ }
70
+ }, [])
71
+
72
+ const updateFillColor = useCallback((color) => {
73
+ setFillColor(color)
74
+ const shape = window.currentShape
75
+ if (shape && shape.shapeName === 'frame') {
76
+ shape.fillColor = color
77
+ shape.draw()
78
+ }
79
+ }, [])
80
+
81
+ const resizeToFit = useCallback(() => {
82
+ const shape = window.currentShape
83
+ if (shape && shape.shapeName === 'frame' && typeof shape.resizeToFitContents === 'function') {
84
+ shape.resizeToFitContents()
85
+ }
86
+ }, [])
87
+
88
+ const handleAIEdit = useCallback(() => {
89
+ const shape = window.currentShape
90
+ if (shape && shape.shapeName === 'frame') {
91
+ window.__aiEditTargetFrame = shape
92
+ toggleAIModal()
93
+ }
94
+ }, [toggleAIModal])
95
+
96
+ return (
97
+ <ShapeSidebar visible={activeTool === TOOLS.FRAME || selectedShapeSidebar === 'frame'}>
98
+ <ToolbarButton icon="bxs-rename" tooltip="Frame name">
99
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.name')}</p>
100
+ <input
101
+ type="text"
102
+ value={frameName}
103
+ onChange={updateName}
104
+ className="w-32 px-2.5 py-1.5 bg-white/[0.05] border border-white/[0.1] rounded-lg text-white text-xs outline-none focus:border-[#5B57D1]/50 transition-all duration-150 font-[lixFont]"
105
+ spellCheck={false}
106
+ />
107
+ </ToolbarButton>
108
+
109
+ <Divider />
110
+
111
+ {/* Fill style */}
112
+ <ToolbarButton icon="bxs-palette" tooltip="Fill style">
113
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.background')}</p>
114
+ <div className="flex gap-1 mb-2.5">
115
+ {FILL_STYLES.map((s) => (
116
+ <button
117
+ key={s.id}
118
+ onClick={() => updateFillStyle(s.id)}
119
+ title={s.label}
120
+ className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 ${
121
+ fillStyle === s.id
122
+ ? 'bg-white/[0.12] text-white'
123
+ : 'text-text-muted hover:text-white hover:bg-white/[0.06]'
124
+ }`}
125
+ >
126
+ <i className={`bx ${s.icon} text-sm`} />
127
+ {s.label}
128
+ </button>
129
+ ))}
130
+ </div>
131
+
132
+ {/* Color picker — only show for solid/grid */}
133
+ {fillStyle !== 'transparent' && (
134
+ <>
135
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-1.5">{t('sidebar.sectionHeader.color')}</p>
136
+ <div className="flex items-center gap-1.5">
137
+ {FILL_COLORS.map((c) => (
138
+ <button
139
+ key={c}
140
+ onClick={() => updateFillColor(c)}
141
+ title={c}
142
+ className={`w-6 h-6 rounded-md border-2 transition-all duration-100 ${
143
+ fillColor === c ? 'border-accent-blue scale-110' : 'border-white/10 hover:border-white/30'
144
+ }`}
145
+ style={{ backgroundColor: c }}
146
+ />
147
+ ))}
148
+ <label className="w-6 h-6 rounded-md border-2 border-white/10 hover:border-white/30 cursor-pointer overflow-hidden relative transition-all duration-100" title="Custom color">
149
+ <input
150
+ type="color"
151
+ value={fillColor}
152
+ onChange={(e) => updateFillColor(e.target.value)}
153
+ className="absolute inset-0 opacity-0 cursor-pointer"
154
+ />
155
+ <i className="bx bx-palette text-xs text-text-muted absolute inset-0 flex items-center justify-center" />
156
+ </label>
157
+ </div>
158
+ </>
159
+ )}
160
+ </ToolbarButton>
161
+
162
+ <Divider />
163
+
164
+ <ToolbarButton icon="bx-image-alt" tooltip="Background image">
165
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.backgroundImage')}</p>
166
+ <div className="flex gap-1.5 mb-2">
167
+ <button
168
+ onClick={() => {
169
+ const input = document.createElement('input')
170
+ input.type = 'file'
171
+ input.accept = IMAGE_ACCEPT_ATTR
172
+ input.onchange = async (e) => {
173
+ const file = e.target.files?.[0]
174
+ if (!file) return
175
+ if (!isAllowedImage(file)) {
176
+ console.warn('[FrameSidebar] Rejected file type:', file.type)
177
+ return
178
+ }
179
+ const reader = new FileReader()
180
+ reader.onload = async (ev) => {
181
+ const shape = window.currentShape
182
+ if (!shape?.shapeName === 'frame' || !shape.setImageFromURL) return
183
+ // Compress heavily before setting
184
+ try {
185
+ const compressed = await compressImage(ev.target.result, { maxWidth: 1280, quality: 0.5 })
186
+ shape.setImageFromURL(compressed.dataUrl, shape._frameImageFit || 'cover')
187
+ } catch {
188
+ shape.setImageFromURL(ev.target.result, shape._frameImageFit || 'cover')
189
+ }
190
+ }
191
+ reader.readAsDataURL(file)
192
+ }
193
+ input.click()
194
+ }}
195
+ className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-text-secondary text-xs hover:bg-white/[0.06] hover:text-white transition-all duration-100"
196
+ >
197
+ <i className="bx bx-upload text-sm" />
198
+ Set Image
199
+ </button>
200
+ <button
201
+ onClick={() => {
202
+ const shape = window.currentShape
203
+ if (shape?.shapeName === 'frame' && shape.setImageFromURL) {
204
+ shape.setImageFromURL(null)
205
+ shape.draw()
206
+ }
207
+ }}
208
+ className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-text-secondary text-xs hover:bg-white/[0.06] hover:text-red-400 transition-all duration-100"
209
+ >
210
+ <i className="bx bx-x text-sm" />
211
+ Remove
212
+ </button>
213
+ </div>
214
+ {/* Fit controls — only show when frame has an image */}
215
+ {(() => {
216
+ const shape = typeof window !== 'undefined' ? window.currentShape : null
217
+ if (!shape?.shapeName === 'frame' || !shape?._frameImageURL) return null
218
+ return (
219
+ <>
220
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-1.5">{t('sidebar.sectionHeader.fit')}</p>
221
+ <div className="flex gap-1">
222
+ {[
223
+ { id: 'cover', label: 'Cover' },
224
+ { id: 'contain', label: 'Contain' },
225
+ { id: 'fill', label: 'Stretch' },
226
+ ].map((f) => (
227
+ <button
228
+ key={f.id}
229
+ onClick={() => {
230
+ const s = window.currentShape
231
+ if (s?.shapeName === 'frame' && s.setImageFromURL && s._frameImageURL) {
232
+ s.setImageFromURL(s._frameImageURL, f.id)
233
+ }
234
+ }}
235
+ className={`px-2 py-1 rounded-md text-[10px] transition-all ${
236
+ shape._frameImageFit === f.id
237
+ ? 'bg-white/[0.12] text-white'
238
+ : 'text-text-muted hover:text-white hover:bg-white/[0.06]'
239
+ }`}
240
+ >{f.label}</button>
241
+ ))}
242
+ </div>
243
+ </>
244
+ )
245
+ })()}
246
+ </ToolbarButton>
247
+
248
+ <Divider />
249
+
250
+ <ToolbarButton icon="bxs-expand" tooltip="Actions">
251
+ <button onClick={resizeToFit} className="flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-text-secondary text-xs hover:bg-white/[0.06] hover:text-white transition-all duration-100">
252
+ <i className="bx bxs-expand text-sm" />
253
+ Resize to Fit
254
+ </button>
255
+ </ToolbarButton>
256
+
257
+ {/* AI Edit / Graph Edit removed while AI is coming-soon. Restore the
258
+ button + handleAIEdit wiring when the assistant ships. */}
259
+ <Divider />
260
+ <LayerControls />
261
+ </ShapeSidebar>
262
+ )
263
+ }
@@ -0,0 +1,279 @@
1
+ "use client"
2
+
3
+ import useSketchStore, { TOOLS } from '../../store/useSketchStore'
4
+ import { useState, useEffect, useCallback, useRef } from 'react'
5
+ import { useTranslation } from '../../hooks/useTranslation'
6
+
7
+ // Origin of the icons API. When the package runs same-origin with
8
+ // sketch.elixpo this stays empty (relative path). External hosts (e.g.
9
+ // blogs.elixpo) point at the public sketch.elixpo deploy via
10
+ // window.__lixsketchIconsOrigin = 'https://sketch.elixpo.com'
11
+ // The endpoints are CORS-enabled so any origin can fetch.
12
+ const DEFAULT_ICONS_ORIGIN = 'https://sketch.elixpo.com';
13
+ function getIconsBaseUrl() {
14
+ if (typeof window === 'undefined') return '';
15
+ if (window.__lixsketchIconsOrigin) return window.__lixsketchIconsOrigin;
16
+ // Same-origin if the host is sketch.elixpo itself; otherwise default to prod.
17
+ if (typeof location !== 'undefined' && /sketch\.elixpo/.test(location.host)) return '';
18
+ return DEFAULT_ICONS_ORIGIN;
19
+ }
20
+
21
+ const iconResultCache = new Map()
22
+
23
+ const CATEGORIES = [
24
+ { value: null, label: 'All', icon: 'bxs-grid-alt' },
25
+ { value: 'tech', label: 'Tech', icon: 'bxs-chip' },
26
+ { value: 'devops', label: 'DevOps', icon: 'bxs-server' },
27
+ { value: 'design', label: 'Design', icon: 'bxs-palette' },
28
+ { value: 'social media', label: 'Social', icon: 'bxs-share-alt' },
29
+ { value: 'navigation', label: 'Nav', icon: 'bxs-navigation' },
30
+ { value: 'business', label: 'Business', icon: 'bxs-briefcase' },
31
+ { value: 'media', label: 'Media', icon: 'bxs-videos' },
32
+ ]
33
+
34
+ // Normalize an SVG string so it renders fully within a fixed box.
35
+ // Ensures the inner <svg> has width/height="100%" and a viewBox.
36
+ function normalizeSvg(raw) {
37
+ if (!raw) return raw
38
+ // Parse to a temp element so we can inspect attributes
39
+ const tmp = document.createElement('div')
40
+ tmp.innerHTML = raw
41
+ const svgEl = tmp.querySelector('svg')
42
+ if (!svgEl) return raw
43
+
44
+ // Ensure viewBox exists — derive from width/height if missing
45
+ if (!svgEl.getAttribute('viewBox')) {
46
+ const w = parseFloat(svgEl.getAttribute('width')) || 24
47
+ const h = parseFloat(svgEl.getAttribute('height')) || 24
48
+ svgEl.setAttribute('viewBox', `0 0 ${w} ${h}`)
49
+ }
50
+ // Force the SVG to fill its container
51
+ svgEl.setAttribute('width', '100%')
52
+ svgEl.setAttribute('height', '100%')
53
+ return tmp.innerHTML
54
+ }
55
+
56
+ function IconCell({ icon, onClick }) {
57
+ const name = icon.filename?.replace('.svg', '').replace(/_/g, ' ') || ''
58
+ const normalizedSvg = typeof document !== 'undefined' && icon.svg ? normalizeSvg(icon.svg) : icon.svg
59
+ return (
60
+ <button
61
+ onClick={onClick}
62
+ title={name}
63
+ style={{ width: '44px', height: '44px', overflow: 'hidden', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: '8px', cursor: 'pointer', background: 'transparent', border: 'none', padding: 0 }}
64
+ className="hover:bg-surface-hover transition-colors duration-100"
65
+ >
66
+ {normalizedSvg ? (
67
+ <div
68
+ style={{ width: '24px', height: '24px', overflow: 'visible', flexShrink: 0, pointerEvents: 'none', filter: 'brightness(0) invert(1)' }}
69
+ dangerouslySetInnerHTML={{ __html: normalizedSvg }}
70
+ />
71
+ ) : (
72
+ <img
73
+ src={`/icons/${encodeURIComponent(icon.filename)}`}
74
+ alt=""
75
+ style={{ width: '24px', height: '24px', pointerEvents: 'none', filter: 'invert(1)' }}
76
+ loading="lazy"
77
+ />
78
+ )}
79
+ </button>
80
+ )
81
+ }
82
+
83
+ export default function IconSidebar() {
84
+ const { t } = useTranslation()
85
+ const activeTool = useSketchStore((s) => s.activeTool)
86
+ const setActiveTool = useSketchStore((s) => s.setActiveTool)
87
+ const visible = activeTool === TOOLS.ICON
88
+ const [query, setQuery] = useState('')
89
+ const [category, setCategory] = useState(null)
90
+ const [icons, setIcons] = useState([])
91
+ const [loading, setLoading] = useState(false)
92
+ const debounceRef = useRef(null)
93
+
94
+ // Close on Escape
95
+ useEffect(() => {
96
+ if (!visible) return
97
+ const handler = (e) => {
98
+ if (e.key === 'Escape') {
99
+ e.stopPropagation()
100
+ setActiveTool(TOOLS.SELECT)
101
+ }
102
+ }
103
+ document.addEventListener('keydown', handler, true)
104
+ return () => document.removeEventListener('keydown', handler, true)
105
+ }, [visible, setActiveTool])
106
+
107
+ // Close when clicking on the canvas (outside the sidebar)
108
+ useEffect(() => {
109
+ if (!visible) return
110
+ const svgEl = document.getElementById('freehand-canvas')
111
+ if (!svgEl) return
112
+ const handleCanvasClick = () => setActiveTool(TOOLS.SELECT)
113
+ svgEl.addEventListener('mousedown', handleCanvasClick)
114
+ return () => svgEl.removeEventListener('mousedown', handleCanvasClick)
115
+ }, [visible, setActiveTool])
116
+
117
+ const fetchIcons = useCallback(async (searchQuery, cat) => {
118
+ const params = new URLSearchParams()
119
+ if (searchQuery) params.set('q', searchQuery)
120
+ if (cat) params.set('category', cat)
121
+ params.set('inline', '1')
122
+ const cacheKey = params.toString()
123
+
124
+ // Return cached results instantly if available
125
+ if (iconResultCache.has(cacheKey)) {
126
+ setIcons(iconResultCache.get(cacheKey))
127
+ return
128
+ }
129
+
130
+ setLoading(true)
131
+ try {
132
+ const res = await fetch(`${getIconsBaseUrl()}/api/icons/search?${cacheKey}`)
133
+ if (res.ok) {
134
+ const data = await res.json()
135
+ const results = data.results || []
136
+ iconResultCache.set(cacheKey, results)
137
+ setIcons(results)
138
+ }
139
+ } catch (err) {
140
+ console.error('Icon fetch failed:', err)
141
+ }
142
+ setLoading(false)
143
+ }, [])
144
+
145
+ // Fetch icons when visibility, query, or category changes (debounced for query typing)
146
+ useEffect(() => {
147
+ if (!visible) return
148
+ if (debounceRef.current) clearTimeout(debounceRef.current)
149
+ debounceRef.current = setTimeout(() => {
150
+ fetchIcons(query, category)
151
+ }, query ? 300 : 0)
152
+ return () => clearTimeout(debounceRef.current)
153
+ }, [query, visible, category, fetchIcons])
154
+
155
+ // Preload all categories in background on first sidebar open
156
+ const hasPreloaded = useRef(false)
157
+ useEffect(() => {
158
+ if (!visible || hasPreloaded.current) return
159
+ hasPreloaded.current = true
160
+ // Fire-and-forget: preload each category so switching is instant
161
+ CATEGORIES.forEach((cat) => {
162
+ const params = new URLSearchParams()
163
+ if (cat.value) params.set('category', cat.value)
164
+ params.set('inline', '1')
165
+ const key = params.toString()
166
+ if (!iconResultCache.has(key)) {
167
+ fetch(`${getIconsBaseUrl()}/api/icons/search?${key}`)
168
+ .then((r) => r.ok ? r.json() : null)
169
+ .then((data) => { if (data?.results) iconResultCache.set(key, data.results) })
170
+ .catch(() => {})
171
+ }
172
+ })
173
+ }, [visible])
174
+
175
+ const handleIconClick = useCallback((icon) => {
176
+ if (typeof window === 'undefined') return
177
+ const place = (svgContent) => {
178
+ if (window.prepareIconPlacement) {
179
+ window.prepareIconPlacement(svgContent)
180
+ } else {
181
+ window.iconToPlace = svgContent
182
+ }
183
+ }
184
+ if (icon.svg) {
185
+ place(icon.svg)
186
+ } else {
187
+ fetch(`${getIconsBaseUrl()}/api/icons/serve?name=${encodeURIComponent(icon.filename)}`)
188
+ .then((r) => r.text())
189
+ .then(place)
190
+ .catch(() => {})
191
+ }
192
+ }, [])
193
+
194
+ return (
195
+ <div
196
+ className={`absolute top-[60px] right-2 bottom-[56px] w-[300px] bg-surface border border-border-light rounded-2xl z-[999] font-[lixFont] flex flex-col transition-transform duration-200 ${
197
+ visible ? 'translate-x-0' : 'translate-x-full'
198
+ }`}
199
+ >
200
+ {/* Header */}
201
+ <div className="px-3.5 pt-3.5 pb-2 shrink-0">
202
+ <div className="flex items-center justify-between mb-2.5">
203
+ <h3 className="text-text-primary text-sm font-medium">Icons</h3>
204
+ <button
205
+ onClick={() => setActiveTool(TOOLS.SELECT)}
206
+ className="w-6 h-6 flex items-center justify-center rounded-md text-text-muted hover:text-text-primary hover:bg-surface-hover transition-colors duration-100"
207
+ title="Close (Esc)"
208
+ >
209
+ <i className="bx bx-x text-lg" />
210
+ </button>
211
+ </div>
212
+
213
+ {/* Search */}
214
+ <div className="flex items-center gap-2 bg-surface-hover border border-border-light rounded-lg px-2.5 py-2">
215
+ <i className="bx bxs-search text-text-muted text-sm" />
216
+ <input
217
+ id="iconSearchInput"
218
+ type="text"
219
+ value={query}
220
+ onChange={(e) => setQuery(e.target.value)}
221
+ placeholder="Search icons..."
222
+ className="flex-1 bg-transparent text-text-primary text-sm outline-none placeholder:text-text-dim"
223
+ spellCheck={false}
224
+ />
225
+ {query && (
226
+ <button onClick={() => setQuery('')} className="text-text-dim hover:text-text-secondary">
227
+ <i className="bx bxs-x-circle text-sm" />
228
+ </button>
229
+ )}
230
+ </div>
231
+ </div>
232
+
233
+ {/* Categories */}
234
+ <div className="flex flex-wrap gap-1 px-3.5 pb-2.5 shrink-0">
235
+ {CATEGORIES.map((cat) => (
236
+ <button
237
+ key={cat.value || 'all'}
238
+ onClick={() => setCategory(cat.value)}
239
+ className={`flex items-center gap-1 px-2 py-1 rounded-md text-xs whitespace-nowrap transition-colors duration-100 ${
240
+ category === cat.value
241
+ ? 'bg-accent-blue/20 text-accent-blue-hover'
242
+ : 'text-text-muted hover:bg-surface-hover hover:text-text-primary'
243
+ }`}
244
+ >
245
+ <i className={`bx ${cat.icon} text-xs`} />
246
+ {cat.label}
247
+ </button>
248
+ ))}
249
+ </div>
250
+
251
+ {/* Divider */}
252
+ <div className="h-px bg-surface-hover mx-3.5 shrink-0" />
253
+
254
+ {/* Icon grid — scrollable */}
255
+ <div className="flex-1 overflow-y-auto no-scrollbar px-3 py-2.5" id="iconsContainer">
256
+ {loading ? (
257
+ <div className="flex items-center justify-center py-12 text-text-muted text-sm">
258
+ <i className="bx bxs-hourglass bx-spin text-lg mr-2" />
259
+ Loading...
260
+ </div>
261
+ ) : icons.length === 0 ? (
262
+ <div className="flex items-center justify-center py-12 text-text-muted text-sm">
263
+ No icons found
264
+ </div>
265
+ ) : (
266
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(6, minmax(0, 1fr))', gap: '2px' }}>
267
+ {icons.map((icon, i) => (
268
+ <IconCell
269
+ key={icon.filename || i}
270
+ icon={icon}
271
+ onClick={() => handleIconClick(icon)}
272
+ />
273
+ ))}
274
+ </div>
275
+ )}
276
+ </div>
277
+ </div>
278
+ )
279
+ }
@@ -0,0 +1,69 @@
1
+ "use client"
2
+
3
+ import useSketchStore from '../../store/useSketchStore'
4
+ import useUIStore from '../../store/useUIStore'
5
+ import ShapeSidebar, { Divider, LayerControls } from './ShapeSidebar'
6
+ import { useCallback } from 'react'
7
+ import { useTranslation } from '../../hooks/useTranslation'
8
+
9
+ export default function ImageSidebar() {
10
+ const { t } = useTranslation()
11
+ const selectedShapeSidebar = useSketchStore((s) => s.selectedShapeSidebar)
12
+ const toggleImageGenerateModal = useUIStore((s) => s.toggleImageGenerateModal)
13
+
14
+ const handleEditWithAI = useCallback(() => {
15
+ const shape = window.currentShape
16
+ if (!shape || shape.shapeName !== 'image') return
17
+
18
+ // Get the image URL from the element
19
+ const el = shape.element
20
+ const href = el?.getAttribute('href') || el?.getAttributeNS('http://www.w3.org/1999/xlink', 'href') || ''
21
+ const w = parseFloat(el?.getAttribute('width')) || 512
22
+ const h = parseFloat(el?.getAttribute('height')) || 512
23
+
24
+ // Store reference for the modal to pick up
25
+ window.__editImageRef = { imageUrl: href, width: w, height: h, shape }
26
+ toggleImageGenerateModal()
27
+ }, [toggleImageGenerateModal])
28
+
29
+ const handleReplace = useCallback(() => {
30
+ if (window.openImageFilePicker) {
31
+ // Store current shape so the new image replaces it
32
+ window.__replaceImageShape = window.currentShape
33
+ window.openImageFilePicker()
34
+ }
35
+ }, [])
36
+
37
+ return (
38
+ <ShapeSidebar visible={selectedShapeSidebar === 'image'}>
39
+ {/* Edit with AI */}
40
+ <button
41
+ onClick={handleEditWithAI}
42
+ title="Edit with AI"
43
+ className="h-9 flex items-center gap-1.5 px-3 rounded-lg text-text-muted hover:text-purple-400 hover:bg-purple-500/10 transition-all duration-100"
44
+ >
45
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none">
46
+ <path d="M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8L12 2z" />
47
+ </svg>
48
+ <span className="text-xs">AI Edit</span>
49
+ </button>
50
+
51
+ <Divider />
52
+
53
+ {/* Replace image */}
54
+ <button
55
+ onClick={handleReplace}
56
+ title="Replace image"
57
+ className="h-9 flex items-center gap-1.5 px-3 rounded-lg text-text-muted hover:text-white hover:bg-white/[0.06] transition-all duration-100"
58
+ >
59
+ <i className="bx bx-upload text-base" />
60
+ <span className="text-xs">Replace</span>
61
+ </button>
62
+
63
+ <Divider />
64
+
65
+ {/* Layer controls */}
66
+ <LayerControls />
67
+ </ShapeSidebar>
68
+ )
69
+ }