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