@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.
- 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 +5349 -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 +482 -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,358 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react'
|
|
4
|
+
import useUIStore from '../../store/useUIStore'
|
|
5
|
+
import useSketchStore from '../../store/useSketchStore'
|
|
6
|
+
import useAuthStore from '../../hooks/inertStores'
|
|
7
|
+
import useCollabStore from '../../hooks/inertStores'
|
|
8
|
+
import { useProfileStore } from '../../hooks/inertStores'
|
|
9
|
+
import { getSessionID } from '../../hooks/inertStores'
|
|
10
|
+
import { triggerDocCloudSync, persistLayoutMode } from '../../hooks/inertStores'
|
|
11
|
+
|
|
12
|
+
function formatBytes(bytes) {
|
|
13
|
+
if (!bytes || bytes === 0) return '0 B'
|
|
14
|
+
const k = 1024
|
|
15
|
+
const sizes = ['B', 'KB', 'MB']
|
|
16
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
17
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function InfoRow({ label, value, icon, color }) {
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex items-center justify-between py-2 px-1">
|
|
23
|
+
<div className="flex items-center gap-2 text-text-dim text-xs">
|
|
24
|
+
<i className={`bx ${icon} text-sm ${color || 'text-text-muted'}`} />
|
|
25
|
+
{label}
|
|
26
|
+
</div>
|
|
27
|
+
<span className="text-text-secondary text-xs font-medium">{value}</span>
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default function CanvasPropertiesModal() {
|
|
33
|
+
const isOpen = useUIStore((s) => s.canvasPropertiesOpen)
|
|
34
|
+
const toggle = useUIStore((s) => s.toggleCanvasProperties)
|
|
35
|
+
const workspaceName = useUIStore((s) => s.workspaceName)
|
|
36
|
+
const saveStatus = useUIStore((s) => s.saveStatus)
|
|
37
|
+
|
|
38
|
+
const isAuthenticated = useAuthStore((s) => s.isAuthenticated)
|
|
39
|
+
const authUser = useAuthStore((s) => s.user)
|
|
40
|
+
const guestProfile = useProfileStore((s) => s.profile)
|
|
41
|
+
|
|
42
|
+
const collabConnected = useCollabStore((s) => s.connected)
|
|
43
|
+
const collabUsers = useCollabStore((s) => s.users)
|
|
44
|
+
const adminUserId = useCollabStore((s) => s.adminUserId)
|
|
45
|
+
const ws = useCollabStore((s) => s.ws)
|
|
46
|
+
|
|
47
|
+
const layoutMode = useSketchStore((s) => s.layoutMode)
|
|
48
|
+
const setLayoutMode = useSketchStore((s) => s.setLayoutMode)
|
|
49
|
+
const handleSetLayout = (mode) => {
|
|
50
|
+
if (mode === layoutMode) return
|
|
51
|
+
setLayoutMode(mode)
|
|
52
|
+
persistLayoutMode(mode)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Determine if current user is admin
|
|
56
|
+
const myUserId = isAuthenticated ? authUser?.id : guestProfile?.id
|
|
57
|
+
const isAdmin = myUserId && adminUserId && myUserId === adminUserId
|
|
58
|
+
|
|
59
|
+
const handleKickUser = (userId) => {
|
|
60
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return
|
|
61
|
+
ws.send(JSON.stringify({ type: 'kick', userId }))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const [stats, setStats] = useState({
|
|
65
|
+
shapeCount: 0,
|
|
66
|
+
canvasSize: '0 x 0',
|
|
67
|
+
zoom: 1,
|
|
68
|
+
dataSize: '0 B',
|
|
69
|
+
sessionId: '',
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!isOpen) return
|
|
74
|
+
|
|
75
|
+
const shapes = window.shapes || []
|
|
76
|
+
const vb = window.svg?.viewBox?.baseVal
|
|
77
|
+
const zoom = window.currentZoom || 1
|
|
78
|
+
const sessionId = getSessionID() || ''
|
|
79
|
+
|
|
80
|
+
// Estimate canvas data size from localStorage (per-session key, fall
|
|
81
|
+
// back to the legacy single-key entry).
|
|
82
|
+
const sceneSavedKey = sessionId ? `lixsketch-autosave-${sessionId}` : 'lixsketch-autosave'
|
|
83
|
+
const sceneSaved = localStorage.getItem(sceneSavedKey) || localStorage.getItem('lixsketch-autosave')
|
|
84
|
+
const dataSize = sceneSaved ? new Blob([sceneSaved]).size : 0
|
|
85
|
+
|
|
86
|
+
// Doc stats: pulled from the doc-autosave localStorage buffer.
|
|
87
|
+
const docKey = sessionId ? `lixsketch-doc-autosave-${sessionId}` : 'lixsketch-doc-autosave'
|
|
88
|
+
const docMetaKey = sessionId ? `lixsketch-doc-autosave-meta-${sessionId}` : 'lixsketch-doc-autosave-meta'
|
|
89
|
+
const docSaved = localStorage.getItem(docKey)
|
|
90
|
+
let docBlockCount = 0
|
|
91
|
+
let docSize = 0
|
|
92
|
+
let docSavedAt = null
|
|
93
|
+
if (docSaved) {
|
|
94
|
+
docSize = new Blob([docSaved]).size
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(docSaved)
|
|
97
|
+
if (Array.isArray(parsed?.blocks)) docBlockCount = parsed.blocks.length
|
|
98
|
+
if (parsed?.savedAt) docSavedAt = parsed.savedAt
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
101
|
+
let docCloudUpdatedAt = null
|
|
102
|
+
try {
|
|
103
|
+
const meta = JSON.parse(localStorage.getItem(docMetaKey) || '{}')
|
|
104
|
+
docCloudUpdatedAt = meta?.lastSeenUpdatedAt || null
|
|
105
|
+
} catch {}
|
|
106
|
+
|
|
107
|
+
// Count shape types
|
|
108
|
+
const typeCounts = {}
|
|
109
|
+
shapes.forEach(s => {
|
|
110
|
+
const name = s.shapeName || 'unknown'
|
|
111
|
+
typeCounts[name] = (typeCounts[name] || 0) + 1
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
setStats({
|
|
115
|
+
shapeCount: shapes.length,
|
|
116
|
+
canvasSize: vb ? `${Math.round(vb.width)} x ${Math.round(vb.height)}` : 'N/A',
|
|
117
|
+
zoom: Math.round(zoom * 100),
|
|
118
|
+
dataSize: formatBytes(dataSize),
|
|
119
|
+
sessionId,
|
|
120
|
+
typeCounts,
|
|
121
|
+
docBlockCount,
|
|
122
|
+
docSize: formatBytes(docSize),
|
|
123
|
+
docSavedAt,
|
|
124
|
+
docCloudUpdatedAt,
|
|
125
|
+
})
|
|
126
|
+
}, [isOpen])
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (!isOpen) return
|
|
130
|
+
const handleKey = (e) => {
|
|
131
|
+
if (e.key === 'Escape') toggle()
|
|
132
|
+
}
|
|
133
|
+
document.addEventListener('keydown', handleKey)
|
|
134
|
+
return () => document.removeEventListener('keydown', handleKey)
|
|
135
|
+
}, [isOpen, toggle])
|
|
136
|
+
|
|
137
|
+
if (!isOpen) return null
|
|
138
|
+
|
|
139
|
+
const ownerName = isAuthenticated
|
|
140
|
+
? (authUser?.displayName || authUser?.email || 'You')
|
|
141
|
+
: (guestProfile?.displayName || 'Guest')
|
|
142
|
+
|
|
143
|
+
const ownerType = isAuthenticated ? 'Authenticated' : 'Guest'
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div
|
|
147
|
+
className="fixed inset-0 z-[9999] flex items-center justify-center font-[lixFont]"
|
|
148
|
+
onClick={toggle}
|
|
149
|
+
>
|
|
150
|
+
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
|
151
|
+
|
|
152
|
+
<div
|
|
153
|
+
className="relative bg-surface-card border border-border-light rounded-2xl w-[380px] mx-4 max-h-[85vh] overflow-y-auto docs-scroll"
|
|
154
|
+
onClick={(e) => e.stopPropagation()}
|
|
155
|
+
>
|
|
156
|
+
{/* Header */}
|
|
157
|
+
<div className="flex items-center justify-between px-5 pt-4 pb-2">
|
|
158
|
+
<div className="flex items-center gap-2">
|
|
159
|
+
<i className="bx bx-info-circle text-lg text-accent-blue" />
|
|
160
|
+
<h2 className="text-text-primary text-sm font-medium">Canvas Properties</h2>
|
|
161
|
+
</div>
|
|
162
|
+
<button
|
|
163
|
+
onClick={toggle}
|
|
164
|
+
className="w-7 h-7 flex items-center justify-center rounded-lg text-text-muted hover:text-text-primary hover:bg-surface-hover cursor-pointer transition-all duration-200"
|
|
165
|
+
>
|
|
166
|
+
<i className="bx bx-x text-xl" />
|
|
167
|
+
</button>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<div className="px-5 pb-5 flex flex-col gap-3">
|
|
171
|
+
{/* Workspace Info */}
|
|
172
|
+
<div className="p-3 rounded-xl border border-border-light bg-surface/50">
|
|
173
|
+
<p className="text-text-dim text-[10px] uppercase tracking-wider mb-2">Workspace</p>
|
|
174
|
+
<InfoRow label="Name" value={workspaceName || 'Untitled'} icon="bx-rename" />
|
|
175
|
+
<InfoRow label="Owner" value={ownerName} icon="bx-user" />
|
|
176
|
+
<InfoRow label="Account" value={ownerType} icon="bx-shield" color={isAuthenticated ? 'text-green-400' : 'text-yellow-400'} />
|
|
177
|
+
<InfoRow
|
|
178
|
+
label="Session ID"
|
|
179
|
+
value={
|
|
180
|
+
<span className="font-mono text-[10px] text-text-dim max-w-[140px] truncate inline-block" title={stats.sessionId}>
|
|
181
|
+
{stats.sessionId}
|
|
182
|
+
</span>
|
|
183
|
+
}
|
|
184
|
+
icon="bx-hash"
|
|
185
|
+
/>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
{/* Canvas Stats */}
|
|
189
|
+
<div className="p-3 rounded-xl border border-border-light bg-surface/50">
|
|
190
|
+
<p className="text-text-dim text-[10px] uppercase tracking-wider mb-2">Canvas</p>
|
|
191
|
+
<InfoRow label="Shapes" value={stats.shapeCount} icon="bx-shape-square" color="text-blue-400" />
|
|
192
|
+
<InfoRow label="Viewport" value={stats.canvasSize} icon="bx-expand" />
|
|
193
|
+
<InfoRow label="Zoom" value={`${stats.zoom}%`} icon="bx-zoom-in" />
|
|
194
|
+
<InfoRow label="Local data" value={stats.dataSize} icon="bx-data" />
|
|
195
|
+
|
|
196
|
+
{/* Shape breakdown */}
|
|
197
|
+
{stats.typeCounts && Object.keys(stats.typeCounts).length > 0 && (
|
|
198
|
+
<div className="mt-2 pt-2 border-t border-border-light">
|
|
199
|
+
<p className="text-text-dim text-[10px] uppercase tracking-wider mb-1.5">Shape Breakdown</p>
|
|
200
|
+
<div className="flex flex-wrap gap-1.5">
|
|
201
|
+
{Object.entries(stats.typeCounts).map(([type, count]) => (
|
|
202
|
+
<span
|
|
203
|
+
key={type}
|
|
204
|
+
className="px-2 py-0.5 rounded-md bg-surface text-text-muted text-[10px] border border-border-light"
|
|
205
|
+
>
|
|
206
|
+
{type}: {count}
|
|
207
|
+
</span>
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
{/* Document */}
|
|
215
|
+
<div className="p-3 rounded-xl border border-border-light bg-surface/50">
|
|
216
|
+
<div className="flex items-center justify-between mb-2">
|
|
217
|
+
<p className="text-text-dim text-[10px] uppercase tracking-wider">Document</p>
|
|
218
|
+
<button
|
|
219
|
+
onClick={() => triggerDocCloudSync()}
|
|
220
|
+
title="Force sync the doc to cloud"
|
|
221
|
+
className="text-[10px] text-accent-blue hover:text-accent-blue-hover px-1.5 py-0.5 rounded hover:bg-accent-blue/10 cursor-pointer transition-all duration-200"
|
|
222
|
+
>
|
|
223
|
+
Sync now
|
|
224
|
+
</button>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
{/* Layout mode pill toggle */}
|
|
228
|
+
<div className="flex items-center gap-1 bg-surface/60 border border-border-light rounded-lg p-0.5 mb-2">
|
|
229
|
+
{[
|
|
230
|
+
{ key: 'canvas', icon: 'bx-pen', label: 'Canvas' },
|
|
231
|
+
{ key: 'split', icon: 'bx-layout', label: 'Split' },
|
|
232
|
+
{ key: 'docs', icon: 'bxs-notepad', label: 'Docs' },
|
|
233
|
+
].map((m) => {
|
|
234
|
+
const active = layoutMode === m.key
|
|
235
|
+
return (
|
|
236
|
+
<button
|
|
237
|
+
key={m.key}
|
|
238
|
+
onClick={() => handleSetLayout(m.key)}
|
|
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
|
+
|
|
252
|
+
<InfoRow label="Blocks" value={stats.docBlockCount || 0} icon="bx-list-ul" color="text-purple-400" />
|
|
253
|
+
<InfoRow label="Local size" value={stats.docSize || '0 B'} icon="bx-data" />
|
|
254
|
+
<InfoRow
|
|
255
|
+
label="Last local save"
|
|
256
|
+
value={
|
|
257
|
+
stats.docSavedAt
|
|
258
|
+
? new Date(stats.docSavedAt).toLocaleTimeString()
|
|
259
|
+
: '—'
|
|
260
|
+
}
|
|
261
|
+
icon="bx-time"
|
|
262
|
+
/>
|
|
263
|
+
<InfoRow
|
|
264
|
+
label="Last cloud sync"
|
|
265
|
+
value={
|
|
266
|
+
stats.docCloudUpdatedAt
|
|
267
|
+
? new Date(stats.docCloudUpdatedAt).toLocaleString()
|
|
268
|
+
: '—'
|
|
269
|
+
}
|
|
270
|
+
icon="bx-cloud"
|
|
271
|
+
color={stats.docCloudUpdatedAt ? 'text-green-400' : 'text-text-muted'}
|
|
272
|
+
/>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
{/* Save & Sync Status */}
|
|
276
|
+
<div className="p-3 rounded-xl border border-border-light bg-surface/50">
|
|
277
|
+
<p className="text-text-dim text-[10px] uppercase tracking-wider mb-2">Sync Status</p>
|
|
278
|
+
<div className="flex items-center gap-2 py-1.5">
|
|
279
|
+
<span className={`w-2 h-2 rounded-full ${
|
|
280
|
+
saveStatus === 'cloud' ? 'bg-green-400' :
|
|
281
|
+
saveStatus === 'local' ? 'bg-yellow-400' :
|
|
282
|
+
'bg-text-dim'
|
|
283
|
+
}`} />
|
|
284
|
+
<span className="text-text-secondary text-xs">
|
|
285
|
+
{saveStatus === 'cloud' ? 'Synced to cloud' :
|
|
286
|
+
saveStatus === 'local' ? 'Saved locally (pending cloud sync)' :
|
|
287
|
+
'Not saved yet'}
|
|
288
|
+
</span>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
{/* Collaboration Status */}
|
|
293
|
+
<div className="p-3 rounded-xl border border-border-light bg-surface/50">
|
|
294
|
+
<div className="flex items-center justify-between mb-2">
|
|
295
|
+
<p className="text-text-dim text-[10px] uppercase tracking-wider">Collaboration</p>
|
|
296
|
+
{collabConnected && collabUsers && (
|
|
297
|
+
<span className="text-text-dim text-[10px]">
|
|
298
|
+
{collabUsers.length} participant{collabUsers.length !== 1 ? 's' : ''}
|
|
299
|
+
</span>
|
|
300
|
+
)}
|
|
301
|
+
</div>
|
|
302
|
+
{collabConnected ? (
|
|
303
|
+
<>
|
|
304
|
+
<div className="flex items-center gap-2 py-1.5">
|
|
305
|
+
<span className="w-2 h-2 rounded-full bg-green-400 animate-pulse" />
|
|
306
|
+
<span className="text-green-400 text-xs">Live session active</span>
|
|
307
|
+
{isAdmin && (
|
|
308
|
+
<span className="ml-auto px-1.5 py-0.5 rounded bg-accent-blue/15 text-accent-blue text-[9px]">Admin</span>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
{collabUsers && collabUsers.length > 0 && (
|
|
312
|
+
<div className="mt-2 flex flex-col gap-1">
|
|
313
|
+
{collabUsers.map((user, i) => {
|
|
314
|
+
const isMe = user.userId === myUserId
|
|
315
|
+
const isUserAdmin = user.userId === adminUserId
|
|
316
|
+
return (
|
|
317
|
+
<div
|
|
318
|
+
key={user.userId || i}
|
|
319
|
+
className="flex items-center gap-2 px-2 py-1.5 rounded-lg bg-surface/80 border border-border-light"
|
|
320
|
+
>
|
|
321
|
+
<span
|
|
322
|
+
className="w-2.5 h-2.5 rounded-full shrink-0"
|
|
323
|
+
style={{ background: user.color || '#5B57D1' }}
|
|
324
|
+
/>
|
|
325
|
+
<span className="text-text-secondary text-[11px] flex-1 truncate">
|
|
326
|
+
{user.displayName || user.name || `User ${i + 1}`}
|
|
327
|
+
{isMe && <span className="text-text-dim ml-1">(you)</span>}
|
|
328
|
+
</span>
|
|
329
|
+
{isUserAdmin && (
|
|
330
|
+
<span className="text-[9px] text-accent-blue px-1 py-0.5 rounded bg-accent-blue/10">admin</span>
|
|
331
|
+
)}
|
|
332
|
+
{isAdmin && !isMe && (
|
|
333
|
+
<button
|
|
334
|
+
onClick={() => handleKickUser(user.userId)}
|
|
335
|
+
className="text-red-400/60 hover:text-red-400 text-[10px] px-1.5 py-0.5 rounded hover:bg-red-500/10 transition-all duration-200 cursor-pointer"
|
|
336
|
+
title={`Remove ${user.displayName || 'user'} from session`}
|
|
337
|
+
>
|
|
338
|
+
<i className="bx bx-x text-sm" />
|
|
339
|
+
</button>
|
|
340
|
+
)}
|
|
341
|
+
</div>
|
|
342
|
+
)
|
|
343
|
+
})}
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
346
|
+
</>
|
|
347
|
+
) : (
|
|
348
|
+
<div className="flex items-center gap-2 py-1.5">
|
|
349
|
+
<span className="w-2 h-2 rounded-full bg-text-dim" />
|
|
350
|
+
<span className="text-text-dim text-xs">No active session</span>
|
|
351
|
+
</div>
|
|
352
|
+
)}
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
)
|
|
358
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef } from 'react'
|
|
4
|
+
import useUIStore from '../../store/useUIStore'
|
|
5
|
+
import useSketchStore, { TOOLS } from '../../store/useSketchStore'
|
|
6
|
+
|
|
7
|
+
const COMMANDS = [
|
|
8
|
+
// --- App ---
|
|
9
|
+
{ label: 'Library', icon: 'bx-library', section: 'App' },
|
|
10
|
+
{ label: 'Find on canvas', icon: 'bx-search', section: 'App', shortcut: 'Ctrl+F', action: 'findOnCanvas' },
|
|
11
|
+
{ label: 'Live collaboration...', icon: 'bx-group', section: 'App' },
|
|
12
|
+
{ label: 'Share', icon: 'bx-share-alt', section: 'App' },
|
|
13
|
+
{ label: 'Toggle theme', icon: 'bx-sun', section: 'App', action: 'toggleTheme' },
|
|
14
|
+
|
|
15
|
+
// --- Export ---
|
|
16
|
+
{ label: 'Export image...', icon: 'bx-image', section: 'Export', shortcut: 'Ctrl+Shift+E', action: 'exportImage' },
|
|
17
|
+
{ label: 'Save to disk', icon: 'bx-download', section: 'Export', shortcut: 'Ctrl+S', action: 'save' },
|
|
18
|
+
{ label: 'Copy to clipboard as PNG', icon: 'bx-clipboard', section: 'Export', shortcut: 'Shift+Alt+C', action: 'copyPNG' },
|
|
19
|
+
{ label: 'Copy to clipboard as SVG', icon: 'bx-code-alt', section: 'Export', shortcut: null, action: 'copySVG' },
|
|
20
|
+
{ label: 'Open scene file', icon: 'bx-folder-open', section: 'Export', shortcut: 'Ctrl+O', action: 'load' },
|
|
21
|
+
|
|
22
|
+
// --- Editor ---
|
|
23
|
+
{ label: 'Undo', icon: 'bx-undo', section: 'Editor', action: 'undo' },
|
|
24
|
+
{ label: 'Redo', icon: 'bx-redo', section: 'Editor', action: 'redo' },
|
|
25
|
+
{ label: 'Zoom in', icon: 'bx-plus', section: 'Editor', shortcut: 'Ctrl++' },
|
|
26
|
+
{ label: 'Zoom out', icon: 'bx-minus', section: 'Editor', shortcut: 'Ctrl+-' },
|
|
27
|
+
{ label: 'Reset zoom', icon: 'bx-reset', section: 'Editor', shortcut: 'Ctrl+0' },
|
|
28
|
+
{ label: 'Zoom to fit all elements', icon: 'bx-fullscreen', section: 'Editor', shortcut: 'Shift+1' },
|
|
29
|
+
{ label: 'Toggle grid', icon: 'bx-grid-alt', section: 'Editor', shortcut: "Ctrl+'", action: 'toggleGrid' },
|
|
30
|
+
{ label: 'Shortcuts & help', icon: 'bx-help-circle', section: 'Editor', shortcut: '?', action: 'help' },
|
|
31
|
+
{ label: 'Select all', icon: 'bx-select-multiple', section: 'Editor', shortcut: 'Ctrl+A', action: 'selectAll' },
|
|
32
|
+
{ label: 'Reset canvas', icon: 'bx-trash', section: 'Editor', shortcut: 'Ctrl+Delete', action: 'resetCanvas' },
|
|
33
|
+
{ label: 'Canvas background', icon: 'bx-palette', section: 'Editor', action: 'openMenu' },
|
|
34
|
+
|
|
35
|
+
// --- Tools ---
|
|
36
|
+
{ label: 'Hand (panning tool)', icon: 'bx-hand', section: 'Tools', shortcut: 'H', action: 'tool:pan' },
|
|
37
|
+
{ label: 'Selection', icon: 'bx-pointer', section: 'Tools', shortcut: 'V', action: 'tool:select' },
|
|
38
|
+
{ label: 'Rectangle', icon: 'bx-rectangle', section: 'Tools', shortcut: 'R', action: 'tool:rectangle' },
|
|
39
|
+
{ label: 'Diamond', icon: 'bx-diamond', section: 'Tools', shortcut: 'D', action: 'tool:diamond' },
|
|
40
|
+
{ label: 'Circle', icon: 'bx-circle', section: 'Tools', shortcut: 'O', action: 'tool:circle' },
|
|
41
|
+
{ label: 'Arrow', icon: 'bx-right-arrow-alt', section: 'Tools', shortcut: 'A', action: 'tool:arrow' },
|
|
42
|
+
{ label: 'Line', icon: 'bx-minus', section: 'Tools', shortcut: 'L', action: 'tool:line' },
|
|
43
|
+
{ label: 'Draw', icon: 'bx-pencil', section: 'Tools', shortcut: 'P', action: 'tool:freehand' },
|
|
44
|
+
{ label: 'Text', icon: 'bx-text', section: 'Tools', shortcut: 'T', action: 'tool:text' },
|
|
45
|
+
{ label: 'Insert image', icon: 'bx-image-add', section: 'Tools', shortcut: '9', action: 'tool:image' },
|
|
46
|
+
{ label: 'Eraser', icon: 'bx-eraser', section: 'Tools', shortcut: 'E', action: 'tool:eraser' },
|
|
47
|
+
{ label: 'Laser pointer', icon: 'bxs-magic-wand', section: 'Tools', shortcut: 'K', action: 'tool:laser' },
|
|
48
|
+
{ label: 'Frame tool', icon: 'bx-border-all', section: 'Tools', shortcut: 'F', action: 'tool:frame' },
|
|
49
|
+
// AI entries removed — feature is hidden behind a coming-soon screen.
|
|
50
|
+
// Restore "Text to diagram..." / "Mermaid to diagram..." here when the
|
|
51
|
+
// assistant ships.
|
|
52
|
+
|
|
53
|
+
// --- Links ---
|
|
54
|
+
{ label: 'GitHub', icon: 'bxl-github', section: 'Links', action: 'link:github' },
|
|
55
|
+
{ label: 'Report an issue', icon: 'bx-bug', section: 'Links', action: 'link:issues' },
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
const TOOL_ACTION_MAP = {
|
|
59
|
+
'tool:pan': TOOLS.PAN,
|
|
60
|
+
'tool:select': TOOLS.SELECT,
|
|
61
|
+
'tool:rectangle': TOOLS.RECTANGLE,
|
|
62
|
+
'tool:diamond': TOOLS.RECTANGLE, // diamond is a rectangle variant
|
|
63
|
+
'tool:circle': TOOLS.CIRCLE,
|
|
64
|
+
'tool:arrow': TOOLS.ARROW,
|
|
65
|
+
'tool:line': TOOLS.LINE,
|
|
66
|
+
'tool:freehand': TOOLS.FREEHAND,
|
|
67
|
+
'tool:text': TOOLS.TEXT,
|
|
68
|
+
'tool:image': TOOLS.IMAGE,
|
|
69
|
+
'tool:eraser': TOOLS.ERASER,
|
|
70
|
+
'tool:laser': TOOLS.LASER,
|
|
71
|
+
'tool:frame': TOOLS.FRAME,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default function CommandPalette() {
|
|
75
|
+
const open = useUIStore((s) => s.commandPaletteOpen)
|
|
76
|
+
const toggleCommandPalette = useUIStore((s) => s.toggleCommandPalette)
|
|
77
|
+
const [query, setQuery] = useState('')
|
|
78
|
+
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
79
|
+
const inputRef = useRef(null)
|
|
80
|
+
const listRef = useRef(null)
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (open) {
|
|
84
|
+
setQuery('')
|
|
85
|
+
setSelectedIndex(0)
|
|
86
|
+
setTimeout(() => inputRef.current?.focus(), 50)
|
|
87
|
+
}
|
|
88
|
+
}, [open])
|
|
89
|
+
|
|
90
|
+
// Scroll selected item into view
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!open) return
|
|
93
|
+
const el = listRef.current?.querySelector(`[data-index="${selectedIndex}"]`)
|
|
94
|
+
if (el) el.scrollIntoView({ block: 'nearest' })
|
|
95
|
+
}, [selectedIndex, open])
|
|
96
|
+
|
|
97
|
+
// Reset selection on query change
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
setSelectedIndex(0)
|
|
100
|
+
}, [query])
|
|
101
|
+
|
|
102
|
+
if (!open) return null
|
|
103
|
+
|
|
104
|
+
const filtered = query
|
|
105
|
+
? COMMANDS.filter((c) =>
|
|
106
|
+
c.label.toLowerCase().includes(query.toLowerCase())
|
|
107
|
+
)
|
|
108
|
+
: COMMANDS
|
|
109
|
+
|
|
110
|
+
// Group into sections preserving order
|
|
111
|
+
const sectionOrder = []
|
|
112
|
+
const sections = {}
|
|
113
|
+
filtered.forEach((c) => {
|
|
114
|
+
if (!sections[c.section]) {
|
|
115
|
+
sections[c.section] = []
|
|
116
|
+
sectionOrder.push(c.section)
|
|
117
|
+
}
|
|
118
|
+
sections[c.section].push(c)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const handleCommand = (cmd) => {
|
|
122
|
+
toggleCommandPalette()
|
|
123
|
+
const action = cmd.action
|
|
124
|
+
|
|
125
|
+
if (!action) return
|
|
126
|
+
|
|
127
|
+
// Tool switching
|
|
128
|
+
if (action.startsWith('tool:')) {
|
|
129
|
+
const tool = TOOL_ACTION_MAP[action]
|
|
130
|
+
if (tool) useSketchStore.getState().setActiveTool(tool)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Links
|
|
135
|
+
if (action === 'link:github') {
|
|
136
|
+
window.open('https://github.com/elixpo/sketch.elixpo', '_blank')
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
if (action === 'link:issues') {
|
|
140
|
+
window.open('https://github.com/elixpo/sketch.elixpo/issues', '_blank')
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const serializer = window.__sceneSerializer
|
|
145
|
+
|
|
146
|
+
switch (action) {
|
|
147
|
+
case 'toggleTheme': {
|
|
148
|
+
const store = useUIStore.getState()
|
|
149
|
+
store.setTheme(store.theme === 'dark' ? 'light' : 'dark')
|
|
150
|
+
break
|
|
151
|
+
}
|
|
152
|
+
case 'toggleGrid':
|
|
153
|
+
useSketchStore.getState().toggleGrid()
|
|
154
|
+
break
|
|
155
|
+
case 'save': {
|
|
156
|
+
const name = useUIStore.getState().workspaceName || 'Untitled'
|
|
157
|
+
serializer?.download(name)
|
|
158
|
+
break
|
|
159
|
+
}
|
|
160
|
+
case 'load':
|
|
161
|
+
serializer?.upload()
|
|
162
|
+
break
|
|
163
|
+
case 'exportImage':
|
|
164
|
+
useUIStore.getState().toggleExportImageModal()
|
|
165
|
+
break
|
|
166
|
+
case 'copyPNG':
|
|
167
|
+
serializer?.copyAsPNG()
|
|
168
|
+
break
|
|
169
|
+
case 'copySVG':
|
|
170
|
+
serializer?.copyAsSVG()
|
|
171
|
+
break
|
|
172
|
+
case 'undo':
|
|
173
|
+
if (typeof window.undo === 'function') window.undo()
|
|
174
|
+
break
|
|
175
|
+
case 'redo':
|
|
176
|
+
if (typeof window.redo === 'function') window.redo()
|
|
177
|
+
break
|
|
178
|
+
case 'selectAll':
|
|
179
|
+
useSketchStore.getState().setActiveTool(TOOLS.SELECT)
|
|
180
|
+
if (window.multiSelection && window.shapes) {
|
|
181
|
+
window.multiSelection.clearSelection()
|
|
182
|
+
window.shapes.forEach(shape => window.multiSelection.addShape(shape))
|
|
183
|
+
}
|
|
184
|
+
break
|
|
185
|
+
case 'resetCanvas': {
|
|
186
|
+
const serializer = window.__sceneSerializer
|
|
187
|
+
if (serializer?.resetCanvas) {
|
|
188
|
+
serializer.resetCanvas()
|
|
189
|
+
}
|
|
190
|
+
useSketchStore.getState().clearShapes()
|
|
191
|
+
useSketchStore.getState().clearHistory()
|
|
192
|
+
break
|
|
193
|
+
}
|
|
194
|
+
case 'findOnCanvas':
|
|
195
|
+
useUIStore.getState().toggleFindBar()
|
|
196
|
+
break
|
|
197
|
+
case 'help':
|
|
198
|
+
useUIStore.getState().toggleHelpModal()
|
|
199
|
+
break
|
|
200
|
+
case 'aiModal':
|
|
201
|
+
useUIStore.getState().toggleAIModal()
|
|
202
|
+
break
|
|
203
|
+
case 'openMenu':
|
|
204
|
+
useUIStore.getState().toggleMenu()
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const handleKeyDown = (e) => {
|
|
210
|
+
if (e.key === 'Escape') {
|
|
211
|
+
toggleCommandPalette()
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
if (e.key === 'ArrowDown') {
|
|
215
|
+
e.preventDefault()
|
|
216
|
+
setSelectedIndex((i) => Math.min(i + 1, filtered.length - 1))
|
|
217
|
+
} else if (e.key === 'ArrowUp') {
|
|
218
|
+
e.preventDefault()
|
|
219
|
+
setSelectedIndex((i) => Math.max(i - 1, 0))
|
|
220
|
+
} else if (e.key === 'Enter' && filtered[selectedIndex]) {
|
|
221
|
+
e.preventDefault()
|
|
222
|
+
handleCommand(filtered[selectedIndex])
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let flatIndex = -1
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<div
|
|
230
|
+
className="fixed inset-0 z-[9999] flex items-start justify-center pt-[12vh] font-[lixFont]"
|
|
231
|
+
onClick={toggleCommandPalette}
|
|
232
|
+
>
|
|
233
|
+
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
|
234
|
+
|
|
235
|
+
<div
|
|
236
|
+
className="relative bg-surface-card border border-border-light rounded-2xl w-full max-w-[540px] mx-4 overflow-hidden"
|
|
237
|
+
onClick={(e) => e.stopPropagation()}
|
|
238
|
+
>
|
|
239
|
+
{/* Search */}
|
|
240
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b border-border-light">
|
|
241
|
+
<i className="bx bx-search text-text-muted text-lg" />
|
|
242
|
+
<input
|
|
243
|
+
ref={inputRef}
|
|
244
|
+
type="text"
|
|
245
|
+
value={query}
|
|
246
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
247
|
+
placeholder="Search commands..."
|
|
248
|
+
className="flex-1 bg-transparent text-text-primary text-sm outline-none placeholder:text-text-dim font-[lixFont]"
|
|
249
|
+
onKeyDown={handleKeyDown}
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
{/* Results */}
|
|
254
|
+
<div ref={listRef} className="max-h-[55vh] overflow-y-auto no-scrollbar py-2">
|
|
255
|
+
{filtered.length === 0 && (
|
|
256
|
+
<div className="px-4 py-6 text-center text-text-dim text-xs">
|
|
257
|
+
No commands found
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
|
|
261
|
+
{sectionOrder.map((section) => (
|
|
262
|
+
<div key={section}>
|
|
263
|
+
<div className="px-4 pt-3 pb-1">
|
|
264
|
+
<span className="text-text-dim text-xs uppercase tracking-wider">
|
|
265
|
+
{section}
|
|
266
|
+
</span>
|
|
267
|
+
</div>
|
|
268
|
+
{sections[section].map((cmd) => {
|
|
269
|
+
flatIndex++
|
|
270
|
+
const idx = flatIndex
|
|
271
|
+
return (
|
|
272
|
+
<button
|
|
273
|
+
key={cmd.label}
|
|
274
|
+
data-index={idx}
|
|
275
|
+
onClick={() => handleCommand(cmd)}
|
|
276
|
+
className={`w-full flex items-center justify-between px-4 py-2 text-text-secondary text-xs transition-all duration-100 ${
|
|
277
|
+
idx === selectedIndex
|
|
278
|
+
? 'bg-surface-hover text-text-primary'
|
|
279
|
+
: 'hover:bg-surface-hover/60'
|
|
280
|
+
}`}
|
|
281
|
+
>
|
|
282
|
+
<span className="flex items-center gap-3">
|
|
283
|
+
<i className={`bx ${cmd.icon} text-base text-text-muted`} />
|
|
284
|
+
{cmd.label}
|
|
285
|
+
</span>
|
|
286
|
+
{cmd.shortcut && (
|
|
287
|
+
<div className="flex items-center gap-0.5">
|
|
288
|
+
{cmd.shortcut.split('+').map((key, i) => (
|
|
289
|
+
<span key={i}>
|
|
290
|
+
{i > 0 && <span className="text-text-dim text-xs mx-0.5">+</span>}
|
|
291
|
+
<kbd className="px-1.5 py-0.5 bg-surface-dark rounded text-text-dim text-xs border border-border">
|
|
292
|
+
{key.trim()}
|
|
293
|
+
</kbd>
|
|
294
|
+
</span>
|
|
295
|
+
))}
|
|
296
|
+
</div>
|
|
297
|
+
)}
|
|
298
|
+
</button>
|
|
299
|
+
)
|
|
300
|
+
})}
|
|
301
|
+
</div>
|
|
302
|
+
))}
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
)
|
|
307
|
+
}
|