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