@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.
- package/dist/react/AIRenderer-M4LMJ66J.js +1105 -0
- package/dist/react/AIRenderer-M4LMJ66J.js.map +7 -0
- package/dist/react/Arrow-BJDAXPMH.js +8 -0
- package/dist/react/Arrow-BJDAXPMH.js.map +7 -0
- package/dist/react/Circle-KPG7TKIX.js +8 -0
- package/dist/react/Circle-KPG7TKIX.js.map +7 -0
- package/dist/react/CodeShape-K4DIZAIV.js +8 -0
- package/dist/react/CodeShape-K4DIZAIV.js.map +7 -0
- package/dist/react/CopyPaste-4CKJRHJ3.js +569 -0
- package/dist/react/CopyPaste-4CKJRHJ3.js.map +7 -0
- package/dist/react/EraserTrail-PA6IQLQN.js +20 -0
- package/dist/react/EraserTrail-PA6IQLQN.js.map +7 -0
- package/dist/react/EventDispatcher-SRFH6LQZ.js +463 -0
- package/dist/react/EventDispatcher-SRFH6LQZ.js.map +7 -0
- package/dist/react/Frame-6UR5XPAX.js +10 -0
- package/dist/react/Frame-6UR5XPAX.js.map +7 -0
- package/dist/react/FreehandStroke-CK24NUT3.js +8 -0
- package/dist/react/FreehandStroke-CK24NUT3.js.map +7 -0
- package/dist/react/GraphEngine-QE5B2PMR.js +340 -0
- package/dist/react/GraphEngine-QE5B2PMR.js.map +7 -0
- package/dist/react/GraphMathParser-2WMTY4IE.js +10 -0
- package/dist/react/GraphMathParser-2WMTY4IE.js.map +7 -0
- package/dist/react/IconShape-KMIA6T2H.js +10 -0
- package/dist/react/IconShape-KMIA6T2H.js.map +7 -0
- package/dist/react/ImageShape-CHHGBXUQ.js +10 -0
- package/dist/react/ImageShape-CHHGBXUQ.js.map +7 -0
- package/dist/react/LayerOrder-XCHF4MSS.js +66 -0
- package/dist/react/LayerOrder-XCHF4MSS.js.map +7 -0
- package/dist/react/Line-2KOQEEGA.js +10 -0
- package/dist/react/Line-2KOQEEGA.js.map +7 -0
- package/dist/react/LixScriptParser-JYWJX375.js +1033 -0
- package/dist/react/LixScriptParser-JYWJX375.js.map +7 -0
- package/dist/react/MermaidFlowchartRenderer-4QBAZ5MB.js +412 -0
- package/dist/react/MermaidFlowchartRenderer-4QBAZ5MB.js.map +7 -0
- package/dist/react/MermaidSequenceParser-UKG2NSZY.js +8 -0
- package/dist/react/MermaidSequenceParser-UKG2NSZY.js.map +7 -0
- package/dist/react/MermaidSequenceRenderer-HPLHY5II.js +387 -0
- package/dist/react/MermaidSequenceRenderer-HPLHY5II.js.map +7 -0
- package/dist/react/Rectangle-6FE7RN6W.js +8 -0
- package/dist/react/Rectangle-6FE7RN6W.js.map +7 -0
- package/dist/react/ResizeCode-WAMDTKSW.js +2 -0
- package/dist/react/ResizeCode-WAMDTKSW.js.map +7 -0
- package/dist/react/ResizeShapes-LKEQTITP.js +250 -0
- package/dist/react/ResizeShapes-LKEQTITP.js.map +7 -0
- package/dist/react/SceneSerializer-4U4B24JM.js +739 -0
- package/dist/react/SceneSerializer-4U4B24JM.js.map +7 -0
- package/dist/react/Selection-VVUHY5YJ.js +34 -0
- package/dist/react/Selection-VVUHY5YJ.js.map +7 -0
- package/dist/react/SketchEngine-YA4RG3KX.js +413 -0
- package/dist/react/SketchEngine-YA4RG3KX.js.map +7 -0
- package/dist/react/TextShape-54ZJ66E3.js +8 -0
- package/dist/react/TextShape-54ZJ66E3.js.map +7 -0
- package/dist/react/UndoRedo-M2JZUBKN.js +30 -0
- package/dist/react/UndoRedo-M2JZUBKN.js.map +7 -0
- package/dist/react/ZoomPan-U3TXC5AJ.js +186 -0
- package/dist/react/ZoomPan-U3TXC5AJ.js.map +7 -0
- package/dist/react/arrowTool-CWEMIRXK.js +17 -0
- package/dist/react/arrowTool-CWEMIRXK.js.map +7 -0
- package/dist/react/boxicons-5JNJQPUP.woff +0 -0
- package/dist/react/boxicons-6NVWQSCQ.eot +0 -0
- package/dist/react/boxicons-AQ5N6WP7.woff2 +0 -0
- package/dist/react/boxicons-GDR4JU3X.ttf +0 -0
- package/dist/react/boxicons-NNFHLMOR.svg +1660 -0
- package/dist/react/chunk-2OQXI7QB.js +564 -0
- package/dist/react/chunk-2OQXI7QB.js.map +7 -0
- package/dist/react/chunk-5OUBTHHO.js +187 -0
- package/dist/react/chunk-5OUBTHHO.js.map +7 -0
- package/dist/react/chunk-6PFXZKL2.js +628 -0
- package/dist/react/chunk-6PFXZKL2.js.map +7 -0
- package/dist/react/chunk-6SFNWGGZ.js +453 -0
- package/dist/react/chunk-6SFNWGGZ.js.map +7 -0
- package/dist/react/chunk-7TZSIR6V.js +210 -0
- package/dist/react/chunk-7TZSIR6V.js.map +7 -0
- package/dist/react/chunk-AJAZC37E.js +662 -0
- package/dist/react/chunk-AJAZC37E.js.map +7 -0
- package/dist/react/chunk-BU4D2WYQ.js +1213 -0
- package/dist/react/chunk-BU4D2WYQ.js.map +7 -0
- package/dist/react/chunk-BZE4CEBG.js +747 -0
- package/dist/react/chunk-BZE4CEBG.js.map +7 -0
- package/dist/react/chunk-CB2RANCB.js +212 -0
- package/dist/react/chunk-CB2RANCB.js.map +7 -0
- package/dist/react/chunk-DNAG7ML6.js +128 -0
- package/dist/react/chunk-DNAG7ML6.js.map +7 -0
- package/dist/react/chunk-HZWLPXFN.js +198 -0
- package/dist/react/chunk-HZWLPXFN.js.map +7 -0
- package/dist/react/chunk-I6HCCIXB.js +572 -0
- package/dist/react/chunk-I6HCCIXB.js.map +7 -0
- package/dist/react/chunk-IZ5OGWGW.js +235 -0
- package/dist/react/chunk-IZ5OGWGW.js.map +7 -0
- package/dist/react/chunk-JJCJNLNN.js +1490 -0
- package/dist/react/chunk-JJCJNLNN.js.map +7 -0
- package/dist/react/chunk-JQ3IRFDP.js +222 -0
- package/dist/react/chunk-JQ3IRFDP.js.map +7 -0
- package/dist/react/chunk-L5OARZJ7.js +1134 -0
- package/dist/react/chunk-L5OARZJ7.js.map +7 -0
- package/dist/react/chunk-LHRTXNVE.js +1600 -0
- package/dist/react/chunk-LHRTXNVE.js.map +7 -0
- package/dist/react/chunk-Q2GA4MBZ.js +1653 -0
- package/dist/react/chunk-Q2GA4MBZ.js.map +7 -0
- package/dist/react/chunk-QNSE5BAY.js +519 -0
- package/dist/react/chunk-QNSE5BAY.js.map +7 -0
- package/dist/react/chunk-QXFBG7OM.js +191 -0
- package/dist/react/chunk-QXFBG7OM.js.map +7 -0
- package/dist/react/chunk-T6ZVV5ZO.js +714 -0
- package/dist/react/chunk-T6ZVV5ZO.js.map +7 -0
- package/dist/react/chunk-URB6MY46.js +974 -0
- package/dist/react/chunk-URB6MY46.js.map +7 -0
- package/dist/react/chunk-VN62KLOC.js +931 -0
- package/dist/react/chunk-VN62KLOC.js.map +7 -0
- package/dist/react/chunk-VQVEPJSA.js +158 -0
- package/dist/react/chunk-VQVEPJSA.js.map +7 -0
- package/dist/react/chunk-XISEAGBV.js +1632 -0
- package/dist/react/chunk-XISEAGBV.js.map +7 -0
- package/dist/react/chunk-YKJUBFHE.js +575 -0
- package/dist/react/chunk-YKJUBFHE.js.map +7 -0
- package/dist/react/circleTool-NUATSEI2.js +14 -0
- package/dist/react/circleTool-NUATSEI2.js.map +7 -0
- package/dist/react/codeTool-MHCB2FNB.js +38 -0
- package/dist/react/codeTool-MHCB2FNB.js.map +7 -0
- package/dist/react/eraserTool-2GS2LQCS.js +103 -0
- package/dist/react/eraserTool-2GS2LQCS.js.map +7 -0
- package/dist/react/frameTool-PWVKKXSP.js +14 -0
- package/dist/react/frameTool-PWVKKXSP.js.map +7 -0
- package/dist/react/freehandTool-SLYGASVP.js +14 -0
- package/dist/react/freehandTool-SLYGASVP.js.map +7 -0
- package/dist/react/iconTool-GPZ7O7NH.js +18 -0
- package/dist/react/iconTool-GPZ7O7NH.js.map +7 -0
- package/dist/react/imageTool-D5R4XGEU.js +15 -0
- package/dist/react/imageTool-D5R4XGEU.js.map +7 -0
- package/dist/react/index.js +5342 -0
- package/dist/react/index.js.map +7 -0
- package/dist/react/laserTool-XVMV63AV.js +239 -0
- package/dist/react/laserTool-XVMV63AV.js.map +7 -0
- package/dist/react/lineTool-WNI5EMZ5.js +14 -0
- package/dist/react/lineTool-WNI5EMZ5.js.map +7 -0
- package/dist/react/lixCode-OJMVUXXI.ttf +0 -0
- package/dist/react/lixDefault-JEAPAUJL.ttf +0 -0
- package/dist/react/lixFancy-BPXYM5CQ.ttf +0 -0
- package/dist/react/lixFont-CPKEUDVM.woff2 +0 -0
- package/dist/react/rectangleTool-U7DZ6ZTA.js +14 -0
- package/dist/react/rectangleTool-U7DZ6ZTA.js.map +7 -0
- package/dist/react/styles.css +6627 -0
- package/dist/react/textTool-M7S6F3VC.js +21 -0
- package/dist/react/textTool-M7S6F3VC.js.map +7 -0
- package/package.json +39 -5
- package/src/EngineShortcuts.js +83 -27
- package/src/core/AIRenderer.js +30 -18
- package/src/core/GraphEngine.js +64 -41
- package/src/core/MermaidFlowchartRenderer.js +153 -88
- package/src/core/MermaidSequenceRenderer.js +168 -93
- package/src/core/SceneSerializer.js +86 -2
- package/src/core/Selection.js +119 -19
- package/src/core/UndoRedo.js +82 -15
- package/src/core/ZoomPan.js +15 -5
- package/src/index.js +14 -1
- package/src/react/LixSketchCanvas.jsx +347 -0
- package/src/react/components/AppMenu.jsx +475 -0
- package/src/react/components/Footer.jsx +85 -0
- package/src/react/components/Toolbar.jsx +140 -0
- package/src/react/components/canvas/CanvasLoadingOverlay.jsx +76 -0
- package/src/react/components/canvas/ContextMenu.jsx +466 -0
- package/src/react/components/canvas/FindBar.jsx +177 -0
- package/src/react/components/canvas/ImageSourcePicker.jsx +133 -0
- package/src/react/components/canvas/MultiSelectActions.jsx +120 -0
- package/src/react/components/canvas/SVGCanvas.jsx +145 -0
- package/src/react/components/canvas/SaveStatusIndicator.jsx +29 -0
- package/src/react/components/modals/CanvasPropertiesModal.jsx +358 -0
- package/src/react/components/modals/CommandPalette.jsx +307 -0
- package/src/react/components/modals/ExportImageModal.jsx +287 -0
- package/src/react/components/modals/GraphModal.jsx +174 -0
- package/src/react/components/modals/HelpModal.jsx +208 -0
- package/src/react/components/modals/LixScriptModal.jsx +130 -0
- package/src/react/components/modals/ShortcutsModal.jsx +125 -0
- package/src/react/components/sidebars/ArrowSidebar.jsx +173 -0
- package/src/react/components/sidebars/CircleSidebar.jsx +134 -0
- package/src/react/components/sidebars/FrameSidebar.jsx +263 -0
- package/src/react/components/sidebars/IconSidebar.jsx +279 -0
- package/src/react/components/sidebars/ImageSidebar.jsx +69 -0
- package/src/react/components/sidebars/LineSidebar.jsx +109 -0
- package/src/react/components/sidebars/PaintbrushSidebar.jsx +119 -0
- package/src/react/components/sidebars/RectangleSidebar.jsx +165 -0
- package/src/react/components/sidebars/ShapeSidebar.jsx +130 -0
- package/src/react/components/sidebars/TextSidebar.jsx +174 -0
- package/src/react/hooks/inertStores.js +58 -0
- package/src/react/hooks/useSketchEngine.js +76 -0
- package/src/react/hooks/useTranslation.js +92 -0
- package/src/react/index.js +41 -0
- package/src/react/store/useSketchStore.js +234 -0
- package/src/react/store/useUIStore.js +196 -0
- package/src/react/styles/arrowSidebar.css +263 -0
- package/src/react/styles/canvasStroke.css +47 -0
- package/src/react/styles/canvasTools.css +769 -0
- package/src/react/styles/circleSidebar.css +206 -0
- package/src/react/styles/fonts/fonts.css +29 -0
- package/src/react/styles/fonts/lixCode.ttf +0 -0
- package/src/react/styles/fonts/lixDefault.ttf +0 -0
- package/src/react/styles/fonts/lixDocs.ttf +0 -0
- package/src/react/styles/fonts/lixFancy.ttf +0 -0
- package/src/react/styles/fonts/lixFont.woff2 +0 -0
- package/src/react/styles/lineSidebar.css +207 -0
- package/src/react/styles/paintbrushSidebar.css +247 -0
- package/src/react/styles/squareSidebar.css +206 -0
- package/src/react/styles/writeText.css +149 -0
- package/src/react/styles.css +175 -0
- package/src/react/toolsEnum.js +20 -0
- package/src/react/utils/toast.js +18 -0
- package/src/shapes/Frame.js +7 -1
- package/src/shapes/Line.js +11 -5
- package/src/tools/arrowTool.js +30 -4
- package/src/tools/circleTool.js +33 -9
- package/src/tools/codeTool.js +5 -3
- package/src/tools/frameTool.js +47 -7
- package/src/tools/freehandTool.js +12 -4
- package/src/tools/iconTool.js +7 -1
- package/src/tools/imageTool.js +7 -1
- package/src/tools/lineTool.js +27 -3
- package/src/tools/rectangleTool.js +45 -8
- 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
|
+
}
|