@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,475 @@
1
+ "use client"
2
+
3
+ import { useState } from 'react'
4
+ import useUIStore from '../store/useUIStore'
5
+ import useSketchStore from '../store/useSketchStore'
6
+ import { useAuthStore } from '../hooks/inertStores'
7
+ import { triggerCloudSync } from '../hooks/inertStores'
8
+ import { triggerDocCloudSync, persistLayoutMode } from '../hooks/inertStores'
9
+ import { useTranslation } from '../hooks/useTranslation'
10
+
11
+ // Issue #38 bug #1: light-themed canvas background palette to pair with
12
+ // the new light default theme.
13
+ const CANVAS_BACKGROUNDS = [
14
+ { color: '#ffffff', label: 'menu.canvasBg.white' },
15
+ { color: '#faf9f5', label: 'menu.canvasBg.cream' },
16
+ { color: '#f5f3ed', label: 'menu.canvasBg.paper' },
17
+ { color: '#f0f5fb', label: 'menu.canvasBg.skyTint' },
18
+ { color: '#f0f5ef', label: 'menu.canvasBg.sageTint' },
19
+ ]
20
+
21
+ export default function AppMenu() {
22
+ const { t, language } = useTranslation()
23
+
24
+ const LINKS = [
25
+ { label: t('links.documentation'), icon: 'bx-book-open', href: '/docs' },
26
+ { label: t('links.github'), icon: 'bxl-github', href: 'https://github.com/elixpo/sketch.elixpo' },
27
+ { label: t('links.reportIssue'), icon: 'bx-bug', href: 'https://github.com/elixpo/sketch.elixpo/issues' },
28
+ ]
29
+
30
+ const PREFERENCE_ITEMS = [
31
+ { label: t('prefs.toolLock'), shortcut: 'Q', id: 'toolLock' },
32
+ { label: t('prefs.snapObjects'), shortcut: 'Alt+S', id: 'snapObjects' },
33
+ { label: t('prefs.toggleGrid'), shortcut: "Ctrl+'", id: 'toggleGrid' },
34
+ { label: t('prefs.zenMode'), shortcut: 'Alt+Z', id: 'zenMode' },
35
+ { label: t('prefs.viewMode'), shortcut: 'Alt+R', id: 'viewMode' },
36
+ { label: t('prefs.canvasShapeProps'), shortcut: 'Alt+/', id: 'properties' },
37
+ { label: t('prefs.arrowBinding'), id: 'arrowBinding', toggle: true },
38
+ { label: t('prefs.snapMidpoints'), id: 'snapMidpoints', toggle: true },
39
+ ]
40
+
41
+ const menuOpen = useUIStore((s) => s.menuOpen)
42
+ const closeMenu = useUIStore((s) => s.closeMenu)
43
+ const toggleSaveModal = useUIStore((s) => s.toggleSaveModal)
44
+ const toggleCommandPalette = useUIStore((s) => s.toggleCommandPalette)
45
+ const toggleHelpModal = useUIStore((s) => s.toggleHelpModal)
46
+ const toggleExportImageModal = useUIStore((s) => s.toggleExportImageModal)
47
+ const theme = useUIStore((s) => s.theme)
48
+ const setTheme = useUIStore((s) => s.setTheme)
49
+ const persistUIPrefs = useUIStore((s) => s.persistUIPrefs)
50
+ const canvasBackground = useSketchStore((s) => s.canvasBackground)
51
+ const setCanvasBackground = useSketchStore((s) => s.setCanvasBackground)
52
+ const clearShapes = useSketchStore((s) => s.clearShapes)
53
+ const clearHistory = useSketchStore((s) => s.clearHistory)
54
+ const gridEnabled = useSketchStore((s) => s.gridEnabled)
55
+ const toggleGrid = useSketchStore((s) => s.toggleGrid)
56
+
57
+ const viewMode = useSketchStore((s) => s.viewMode)
58
+ const zenMode = useSketchStore((s) => s.zenMode)
59
+ const toolLock = useSketchStore((s) => s.toolLock)
60
+ const snapToObjects = useSketchStore((s) => s.snapToObjects)
61
+ const toggleViewMode = useSketchStore((s) => s.toggleViewMode)
62
+ const toggleZenMode = useSketchStore((s) => s.toggleZenMode)
63
+ const toggleToolLock = useSketchStore((s) => s.toggleToolLock)
64
+ const toggleSnapToObjects = useSketchStore((s) => s.toggleSnapToObjects)
65
+
66
+ const layoutMode = useSketchStore((s) => s.layoutMode)
67
+ const setLayoutMode = useSketchStore((s) => s.setLayoutMode)
68
+ const handleSetLayout = (mode) => {
69
+ if (mode === layoutMode) return
70
+ setLayoutMode(mode)
71
+ persistLayoutMode(mode)
72
+ }
73
+
74
+ const isAuthenticated = useAuthStore((s) => s.isAuthenticated)
75
+ const authUser = useAuthStore((s) => s.user)
76
+ const login = useAuthStore((s) => s.login)
77
+ const logout = useAuthStore((s) => s.logout)
78
+
79
+ const [prefsOpen, setPrefsOpen] = useState(false)
80
+
81
+ // Menu is always accessible (via floating button in view/zen mode)
82
+
83
+ const handleOpen = () => {
84
+ const serializer = window.__sceneSerializer
85
+ if (serializer) {
86
+ serializer.upload().then((result) => {
87
+ if (result && result.success) closeMenu()
88
+ else if (result && result.error) {
89
+ console.warn('[Open] Invalid scene file:', result.error)
90
+ }
91
+ })
92
+ }
93
+ closeMenu()
94
+ }
95
+
96
+ return (
97
+ <>
98
+ {menuOpen && (
99
+ <div
100
+ className="fixed inset-0 z-[999]"
101
+ onClick={() => { closeMenu(); setPrefsOpen(false) }}
102
+ />
103
+ )}
104
+ <div
105
+ className={`absolute top-14 right-4 w-[230px] max-h-[calc(100vh-140px)] overflow-y-auto no-scrollbar bg-surface/75 backdrop-blur-lg rounded-2xl z-[1000] border border-border-light p-1.5 font-[lixFont] text-[13px] transition-all duration-200 ${
106
+ menuOpen
107
+ ? 'opacity-100 blur-0 pointer-events-auto'
108
+ : 'opacity-0 blur-[20px] pointer-events-none'
109
+ }`}
110
+ >
111
+ {/* Open */}
112
+ <button
113
+ onClick={handleOpen}
114
+ className="w-full flex items-center justify-between px-3 py-1.5 rounded-lg text-text-secondary text-[12.5px] hover:bg-surface-hover cursor-pointer transition-all duration-200"
115
+ >
116
+ <span className="flex items-center gap-2">
117
+ <i className="bx bx-folder-open text-sm" />
118
+ {t('menu.open')}
119
+ </span>
120
+ <span className="text-text-dim text-xs">Ctrl+O</span>
121
+ </button>
122
+
123
+ {/* Quick Save */}
124
+ <button
125
+ onClick={() => {
126
+ const serializer = window.__sceneSerializer
127
+ if (serializer) {
128
+ const workspaceName = useUIStore.getState().workspaceName || 'Untitled'
129
+ const sceneData = serializer.save(workspaceName)
130
+ const sessionId = window.__sessionID
131
+ const key = sessionId ? `lixsketch-autosave-${sessionId}` : 'lixsketch-autosave'
132
+ localStorage.setItem(key, JSON.stringify(sceneData))
133
+ useUIStore.getState().setSaveStatus('local')
134
+ triggerCloudSync()
135
+ }
136
+ closeMenu()
137
+ }}
138
+ className="w-full flex items-center justify-between px-3 py-1.5 rounded-lg text-text-secondary text-[12.5px] hover:bg-surface-hover cursor-pointer transition-all duration-200"
139
+ >
140
+ <span className="flex items-center gap-2">
141
+ <i className="bx bx-check-circle text-sm" />
142
+ {t('menu.quickSave')}
143
+ </span>
144
+ <span className="text-text-dim text-xs">Ctrl+S</span>
145
+ </button>
146
+
147
+ {/* Save & Share */}
148
+ <button
149
+ onClick={() => { toggleSaveModal(); closeMenu() }}
150
+ className="w-full flex items-center justify-between px-3 py-1.5 rounded-lg text-text-secondary text-[12.5px] hover:bg-surface-hover cursor-pointer transition-all duration-200"
151
+ >
152
+ <span className="flex items-center gap-2">
153
+ <i className="bx bx-save text-sm" />
154
+ {t('menu.saveShare')}
155
+ </span>
156
+ <span className="text-text-dim text-xs">Ctrl+Shift+S</span>
157
+ </button>
158
+
159
+ {/* Export Image */}
160
+ <button
161
+ onClick={() => { toggleExportImageModal(); closeMenu() }}
162
+ className="w-full flex items-center justify-between px-3 py-1.5 rounded-lg text-text-secondary text-[12.5px] hover:bg-surface-hover cursor-pointer transition-all duration-200"
163
+ >
164
+ <span className="flex items-center gap-2">
165
+ <i className="bx bx-image text-sm" />
166
+ {t('menu.exportImage')}
167
+ </span>
168
+ <span className="text-text-dim text-xs">Ctrl+Shift+E</span>
169
+ </button>
170
+
171
+ <hr className="border-border-light my-1" />
172
+
173
+ {/* Commands - highlighted */}
174
+ <button
175
+ onClick={() => { toggleCommandPalette(); closeMenu() }}
176
+ className="w-full flex items-center justify-between px-3 py-2 rounded-lg text-xs transition-all duration-200 text-accent-blue bg-accent-blue/10 hover:bg-accent-blue/20 cursor-pointer"
177
+ >
178
+ <span className="flex items-center gap-2">
179
+ <i className="bx bx-command text-sm" />
180
+ {t('menu.commands')}
181
+ </span>
182
+ <span className="text-text-dim text-xs">Ctrl+/</span>
183
+ </button>
184
+
185
+ {/* Find Text */}
186
+ <button
187
+ onClick={() => { useUIStore.getState().toggleFindBar(); closeMenu() }}
188
+ className="w-full flex items-center justify-between px-3 py-1.5 rounded-lg text-text-secondary text-[12.5px] hover:bg-surface-hover cursor-pointer transition-all duration-200"
189
+ >
190
+ <span className="flex items-center gap-2">
191
+ <i className="bx bx-search text-sm" />
192
+ {t('menu.findText')}
193
+ </span>
194
+ <span className="text-text-dim text-xs">Ctrl+F</span>
195
+ </button>
196
+
197
+ {/* Canvas Properties */}
198
+ <button
199
+ onClick={() => { useUIStore.getState().toggleCanvasProperties(); closeMenu() }}
200
+ className="w-full flex items-center justify-between px-3 py-1.5 rounded-lg text-text-secondary text-[12.5px] hover:bg-surface-hover cursor-pointer transition-all duration-200"
201
+ >
202
+ <span className="flex items-center gap-2">
203
+ <i className="bx bx-info-circle text-sm" />
204
+ {t('menu.canvasProperties')}
205
+ </span>
206
+ </button>
207
+
208
+ {/* Help */}
209
+ <button
210
+ onClick={() => { toggleHelpModal(); closeMenu() }}
211
+ className="w-full flex items-center justify-between px-3 py-1.5 rounded-lg text-text-secondary text-[12.5px] hover:bg-surface-hover cursor-pointer transition-all duration-200"
212
+ >
213
+ <span className="flex items-center gap-2">
214
+ <i className="bx bx-help-circle text-sm" />
215
+ {t('menu.help')}
216
+ </span>
217
+ </button>
218
+
219
+ <hr className="border-border-light my-1" />
220
+
221
+ {/* Document layout */}
222
+ <div className="px-3 py-1.5">
223
+ <p className="text-text-dim text-[10px] uppercase tracking-wider mb-1.5 flex items-center gap-1.5">
224
+ <i className="bx bx-file-blank text-[11px]" />
225
+ Document
226
+ </p>
227
+ <div className="flex items-center gap-1 bg-surface/60 border border-border-light rounded-lg p-0.5">
228
+ {[
229
+ { key: 'canvas', icon: 'bx-pen', label: 'Canvas' },
230
+ { key: 'split', icon: 'bx-layout', label: 'Split' },
231
+ { key: 'docs', icon: 'bxs-notepad', label: 'Docs' },
232
+ ].map((m) => {
233
+ const active = layoutMode === m.key
234
+ return (
235
+ <button
236
+ key={m.key}
237
+ onClick={() => handleSetLayout(m.key)}
238
+ title={m.label}
239
+ className={`flex-1 flex items-center justify-center gap-1 h-6 rounded-md text-[10.5px] transition-all duration-150 ${
240
+ active
241
+ ? 'bg-accent-blue text-text-primary'
242
+ : 'text-text-muted hover:text-text-primary hover:bg-surface-hover'
243
+ }`}
244
+ >
245
+ <i className={`bx ${m.icon} text-[11px]`} />
246
+ {m.label}
247
+ </button>
248
+ )
249
+ })}
250
+ </div>
251
+ </div>
252
+
253
+ {/* Sync doc now (Ctrl+S triggers both, but explicit action is useful from menu) */}
254
+ <button
255
+ onClick={() => {
256
+ triggerCloudSync()
257
+ triggerDocCloudSync()
258
+ closeMenu()
259
+ }}
260
+ className="w-full flex items-center justify-between px-3 py-1.5 rounded-lg text-text-secondary text-[12.5px] hover:bg-surface-hover cursor-pointer transition-all duration-200"
261
+ >
262
+ <span className="flex items-center gap-2">
263
+ <i className="bx bx-cloud-upload text-sm" />
264
+ Sync canvas + doc
265
+ </span>
266
+ <span className="text-text-dim text-xs">Ctrl+S</span>
267
+ </button>
268
+
269
+ <hr className="border-border-light my-1" />
270
+
271
+ {/* Preferences - inline expandable */}
272
+ <button
273
+ onClick={() => setPrefsOpen((p) => !p)}
274
+ className={`w-full flex items-center justify-between px-3 py-1.5 rounded-lg text-text-secondary text-[12.5px] hover:bg-surface-hover cursor-pointer transition-all duration-200 ${prefsOpen ? 'bg-surface-hover' : ''}`}
275
+ >
276
+ <span className="flex items-center gap-2">
277
+ <i className="bx bx-cog text-sm" />
278
+ {t('menu.preferences')}
279
+ </span>
280
+ <i className={`bx bx-chevron-down text-sm text-text-dim transition-transform duration-150 ${prefsOpen ? 'rotate-180' : ''}`} />
281
+ </button>
282
+
283
+ {prefsOpen && (
284
+ <div className="ml-2 border-l border-border-light pl-1">
285
+ {/* Language Switcher */}
286
+ <div className="w-full flex items-center justify-between px-3 py-1.5 rounded-lg text-text-secondary text-[11px] transition-all duration-200">
287
+ <span className="flex items-center gap-2">
288
+ {t('prefs.language')}
289
+ </span>
290
+ <select
291
+ className="bg-surface-hover text-text-primary text-[10px] rounded px-1 outline-none border border-border-light"
292
+ value={language}
293
+ onChange={(e) => persistUIPrefs({ language: e.target.value })}
294
+ >
295
+ <option value="en">English</option>
296
+ <option value="bg">Български</option>
297
+ <option value="de">Deutsch</option>
298
+ </select>
299
+ </div>
300
+
301
+ {PREFERENCE_ITEMS.map((item) => {
302
+ const isActive =
303
+ (item.id === 'toolLock' && toolLock) ||
304
+ (item.id === 'snapObjects' && snapToObjects) ||
305
+ (item.id === 'toggleGrid' && gridEnabled) ||
306
+ (item.id === 'zenMode' && zenMode) ||
307
+ (item.id === 'viewMode' && viewMode) ||
308
+ (item.toggle) // arrow binding, snap midpoints default on
309
+
310
+ const handleClick = () => {
311
+ if (item.id === 'toolLock') toggleToolLock()
312
+ else if (item.id === 'snapObjects') toggleSnapToObjects()
313
+ else if (item.id === 'toggleGrid') toggleGrid()
314
+ else if (item.id === 'zenMode') { toggleZenMode(); closeMenu() }
315
+ else if (item.id === 'viewMode') { toggleViewMode(); closeMenu() }
316
+ }
317
+
318
+ return (
319
+ <button
320
+ key={item.id}
321
+ onClick={handleClick}
322
+ className="w-full flex items-center justify-between px-3 py-1.5 rounded-lg text-text-secondary text-[11px] hover:bg-surface-hover cursor-pointer transition-all duration-200"
323
+ >
324
+ <span className="flex items-center gap-2">
325
+ {isActive && (
326
+ <i className="bx bx-check text-sm text-accent-blue" />
327
+ )}
328
+ {item.label}
329
+ </span>
330
+ {item.shortcut && (
331
+ <span className="text-text-dim text-[10px]">{item.shortcut}</span>
332
+ )}
333
+ </button>
334
+ )
335
+ })}
336
+ </div>
337
+ )}
338
+
339
+ {/* Grid toggle */}
340
+ <button
341
+ onClick={toggleGrid}
342
+ className="w-full flex items-center justify-between px-3 py-1.5 rounded-lg text-text-secondary text-[12.5px] hover:bg-surface-hover cursor-pointer transition-all duration-200"
343
+ >
344
+ <span className="flex items-center gap-2">
345
+ <i className="bx bx-grid-alt text-sm" />
346
+ {t('menu.showGrid')}
347
+ </span>
348
+ <div className={`w-7 h-4 rounded-full transition-all duration-150 relative ${gridEnabled ? 'bg-accent-blue' : 'bg-white/10'}`}>
349
+ <div className={`absolute top-0.5 w-3 h-3 rounded-full bg-white transition-all duration-150 ${gridEnabled ? 'left-3.5' : 'left-0.5'}`} />
350
+ </div>
351
+ </button>
352
+
353
+ {/* Reset The Canvas */}
354
+ <button
355
+ onClick={() => {
356
+ const serializer = window.__sceneSerializer
357
+ if (serializer?.resetCanvas) serializer.resetCanvas()
358
+ clearShapes(); clearHistory(); closeMenu()
359
+ }}
360
+ className="w-full flex items-center justify-between px-3 py-2 rounded-lg text-red-400 text-xs hover:bg-red-500/10 cursor-pointer transition-all duration-200"
361
+ >
362
+ <span className="flex items-center gap-2">
363
+ <i className="bx bx-reset text-sm" />
364
+ {t('menu.resetCanvas')}
365
+ </span>
366
+ </button>
367
+
368
+ <hr className="border-border-light my-1" />
369
+
370
+ {/* Links */}
371
+ {LINKS.map((link) => {
372
+ const isExternal = link.href.startsWith('http')
373
+ return (
374
+ <a
375
+ key={link.label}
376
+ href={link.href}
377
+ {...(isExternal ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
378
+ onClick={closeMenu}
379
+ className="w-full flex items-center gap-2 px-3 py-1.5 rounded-lg text-text-secondary text-[12.5px] hover:bg-surface-hover cursor-pointer transition-all duration-200"
380
+ >
381
+ <i className={`bx ${link.icon} text-sm`} />
382
+ {link.label}
383
+ </a>
384
+ )
385
+ })}
386
+
387
+ <hr className="border-border-light my-1" />
388
+
389
+ {/* Sign In / Sign Out */}
390
+ {isAuthenticated ? (
391
+ <>
392
+ <div className="px-3 py-2 flex items-center gap-2">
393
+ {authUser?.avatar ? (
394
+ <img src={authUser.avatar} alt="" className="w-5 h-5 rounded-full" />
395
+ ) : (
396
+ <i className="bx bx-user-circle text-sm text-accent-blue" />
397
+ )}
398
+ <span className="text-text-secondary text-xs truncate flex-1">{authUser?.displayName || authUser?.email}</span>
399
+ </div>
400
+ <button
401
+ onClick={() => { logout(); closeMenu() }}
402
+ className="w-full flex items-center justify-between px-3 py-2 rounded-lg text-xs transition-all duration-200 text-red-400 hover:bg-red-500/10 cursor-pointer"
403
+ >
404
+ <span className="flex items-center gap-2">
405
+ <i className="bx bx-log-out text-sm" />
406
+ {t('menu.signOut')}
407
+ </span>
408
+ </button>
409
+ </>
410
+ ) : (
411
+ <button
412
+ onClick={() => { login(); closeMenu() }}
413
+ className="w-full flex items-center justify-between px-3 py-2 rounded-lg text-xs transition-all duration-200 text-text-secondary hover:bg-surface-hover"
414
+ >
415
+ <span className="flex items-center gap-2">
416
+ <i className="bx bx-log-in text-sm" />
417
+ {t('menu.signIn')}
418
+ </span>
419
+ <span className="text-text-dim text-[10px] px-1.5 py-0.5 rounded bg-accent-blue/15 text-accent-blue">Elixpo</span>
420
+ </button>
421
+ )}
422
+
423
+ <hr className="border-border-light my-1" />
424
+
425
+ {/* Theme toggle */}
426
+ <div className="px-3 py-2">
427
+ <p className="text-text-dim text-xs uppercase tracking-wider mb-2">
428
+ {t('menu.theme')}
429
+ </p>
430
+ <div className="flex items-center gap-1">
431
+ {[
432
+ { value: 'light', icon: 'bxs-sun' },
433
+ { value: 'dark', icon: 'bxs-moon' },
434
+ { value: 'system', icon: 'bx-laptop' },
435
+ ].map((t) => (
436
+ <button
437
+ key={t.value}
438
+ onClick={() => setTheme(t.value)}
439
+ className={`flex-1 flex items-center justify-center py-1.5 rounded-lg text-xs cursor-pointer transition-all duration-200 ${
440
+ theme === t.value
441
+ ? 'bg-accent text-text-primary'
442
+ : 'text-text-muted hover:bg-surface-hover'
443
+ }`}
444
+ >
445
+ <i className={`bx ${t.icon} text-sm`} />
446
+ </button>
447
+ ))}
448
+ </div>
449
+ </div>
450
+
451
+ {/* Canvas background */}
452
+ <div className="px-3 py-2">
453
+ <p className="text-text-dim text-xs uppercase tracking-wider mb-2">
454
+ {t('menu.canvasBackground')}
455
+ </p>
456
+ <div className="flex items-center gap-1.5">
457
+ {CANVAS_BACKGROUNDS.map((bg) => (
458
+ <button
459
+ key={bg.color}
460
+ onClick={() => setCanvasBackground(bg.color)}
461
+ title={t(bg.label)}
462
+ className={`w-7 h-7 rounded-full border-2 cursor-pointer transition-all duration-200 ${
463
+ canvasBackground === bg.color
464
+ ? 'border-accent scale-110'
465
+ : 'border-border hover:border-border-light'
466
+ }`}
467
+ style={{ backgroundColor: bg.color }}
468
+ />
469
+ ))}
470
+ </div>
471
+ </div>
472
+ </div>
473
+ </>
474
+ )
475
+ }
@@ -0,0 +1,85 @@
1
+ "use client"
2
+
3
+ import useSketchStore from '../store/useSketchStore'
4
+ import { useCallback } from 'react'
5
+
6
+ export default function Footer() {
7
+ const zoom = useSketchStore((s) => s.zoom)
8
+ const setZoom = useSketchStore((s) => s.setZoom)
9
+ const viewMode = useSketchStore((s) => s.viewMode)
10
+ const zenMode = useSketchStore((s) => s.zenMode)
11
+
12
+ const zoomPercent = Math.round(zoom * 100)
13
+
14
+ const handleZoomIn = useCallback(() => {
15
+ if (window.zoomFromCenter) window.zoomFromCenter(1)
16
+ }, [])
17
+
18
+ const handleZoomOut = useCallback(() => {
19
+ if (window.zoomFromCenter) window.zoomFromCenter(-1)
20
+ }, [])
21
+
22
+ const handleZoomReset = useCallback(() => {
23
+ if (window.zoomReset) window.zoomReset()
24
+ }, [])
25
+
26
+ const handleUndo = useCallback(() => {
27
+ if (window.undo) window.undo()
28
+ }, [])
29
+
30
+ const handleRedo = useCallback(() => {
31
+ if (window.redo) window.redo()
32
+ }, [])
33
+
34
+ if (viewMode || zenMode) return null
35
+
36
+ return (
37
+ <div className="absolute bottom-1 right-5 flex items-center gap-2.5 z-[1000] font-[lixFont]">
38
+ {/* Undo/Redo */}
39
+ <div className="flex items-center bg-surface rounded-lg overflow-hidden">
40
+ <button
41
+ onClick={handleUndo}
42
+ title="Undo (Ctrl+Z)"
43
+ className="w-9 h-9 flex items-center justify-center text-text-muted hover:text-text-primary hover:bg-surface-hover transition-all duration-200"
44
+ >
45
+ <i className="bx bx-undo text-lg" />
46
+ </button>
47
+ <div className="w-px h-5 bg-border-light" />
48
+ <button
49
+ onClick={handleRedo}
50
+ title="Redo (Ctrl+Shift+Z)"
51
+ className="w-9 h-9 flex items-center justify-center text-text-muted hover:text-text-primary hover:bg-surface-hover transition-all duration-200"
52
+ >
53
+ <i className="bx bx-redo text-lg" />
54
+ </button>
55
+ </div>
56
+
57
+ {/* Zoom controls */}
58
+ <div className="flex items-center bg-surface rounded-lg overflow-hidden">
59
+ <button
60
+ onClick={handleZoomOut}
61
+ title="Zoom Out (Ctrl+-)"
62
+ className="w-9 h-9 flex items-center justify-center text-text-muted hover:text-text-primary hover:bg-surface-hover transition-all duration-200"
63
+ >
64
+ <i className="bx bx-minus text-lg" />
65
+ </button>
66
+ <div className="w-px h-5 bg-border-light" />
67
+ <button
68
+ onClick={handleZoomReset}
69
+ title="Reset Zoom (Ctrl+0)"
70
+ className="min-w-[52px] h-9 flex items-center justify-center text-text-secondary text-sm px-2 hover:bg-surface-hover transition-all duration-200"
71
+ >
72
+ {zoomPercent}%
73
+ </button>
74
+ <div className="w-px h-5 bg-border-light" />
75
+ <button
76
+ onClick={handleZoomIn}
77
+ title="Zoom In (Ctrl++)"
78
+ className="w-9 h-9 flex items-center justify-center text-text-muted hover:text-text-primary hover:bg-surface-hover transition-all duration-200"
79
+ >
80
+ <i className="bx bx-plus text-lg" />
81
+ </button>
82
+ </div>
83
+ </div>
84
+ )
85
+ }