@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,130 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
4
|
+
import useUIStore from '../../store/useUIStore'
|
|
5
|
+
|
|
6
|
+
const SAMPLE = `# LixScript — paste shapes / arrows in a simple DSL.
|
|
7
|
+
# Hit Cmd/Ctrl+Enter to render onto the canvas.
|
|
8
|
+
|
|
9
|
+
rect A "Idea" 100 100 220 90
|
|
10
|
+
rect B "Plan" 400 100 220 90
|
|
11
|
+
rect C "Ship" 700 100 220 90
|
|
12
|
+
|
|
13
|
+
arrow A -> B
|
|
14
|
+
arrow B -> C
|
|
15
|
+
`
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* LixScript modal — takes a script the user types and parses it directly
|
|
19
|
+
* into engine shapes via @elixpo/lixsketch's LixScriptParser. No AI
|
|
20
|
+
* inference, no worker round-trip. Strictly client-side parsing.
|
|
21
|
+
*/
|
|
22
|
+
export default function LixScriptModal() {
|
|
23
|
+
const open = useUIStore((s) => s.aiModalOpen)
|
|
24
|
+
const toggle = useUIStore((s) => s.toggleAIModal)
|
|
25
|
+
const [source, setSource] = useState(SAMPLE)
|
|
26
|
+
const [busy, setBusy] = useState(false)
|
|
27
|
+
const [status, setStatus] = useState(null) // { tone, message }
|
|
28
|
+
const taRef = useRef(null)
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!open) return
|
|
32
|
+
requestAnimationFrame(() => taRef.current?.focus())
|
|
33
|
+
const onKey = (e) => {
|
|
34
|
+
if (e.key === 'Escape') toggle()
|
|
35
|
+
}
|
|
36
|
+
document.addEventListener('keydown', onKey)
|
|
37
|
+
return () => document.removeEventListener('keydown', onKey)
|
|
38
|
+
}, [open, toggle])
|
|
39
|
+
|
|
40
|
+
const render = useCallback(async () => {
|
|
41
|
+
if (busy) return
|
|
42
|
+
setBusy(true)
|
|
43
|
+
setStatus(null)
|
|
44
|
+
try {
|
|
45
|
+
// Lazy-import the parser. Pulling it eagerly would bring the whole
|
|
46
|
+
// shape graph into the entry chunk (see SceneSerializer comment
|
|
47
|
+
// in LixSketchCanvas) and crash with "svg is not defined" before
|
|
48
|
+
// engine.init() has wired the engine globals.
|
|
49
|
+
const mod = await import('../../../core/LixScriptParser.js')
|
|
50
|
+
const parsed = mod.parseLixScript(source)
|
|
51
|
+
const resolved = mod.resolveShapeRefs ? mod.resolveShapeRefs(parsed) : parsed
|
|
52
|
+
mod.renderLixScript(resolved)
|
|
53
|
+
setStatus({ tone: 'success', message: `Rendered ${Array.isArray(resolved) ? resolved.length : '?'} elements.` })
|
|
54
|
+
// Auto-close shortly after a successful render.
|
|
55
|
+
setTimeout(() => { toggle() }, 700)
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.warn('[LixScriptModal] parse failed:', err)
|
|
58
|
+
setStatus({ tone: 'error', message: err?.message || 'Parse failed.' })
|
|
59
|
+
} finally {
|
|
60
|
+
setBusy(false)
|
|
61
|
+
}
|
|
62
|
+
}, [source, busy, toggle])
|
|
63
|
+
|
|
64
|
+
const handleKeyDown = useCallback((e) => {
|
|
65
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
66
|
+
e.preventDefault()
|
|
67
|
+
render()
|
|
68
|
+
}
|
|
69
|
+
}, [render])
|
|
70
|
+
|
|
71
|
+
if (!open) return null
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
className="fixed inset-0 z-[1100] flex items-center justify-center bg-black/60 backdrop-blur-sm font-[lixFont]"
|
|
76
|
+
onClick={toggle}
|
|
77
|
+
role="dialog"
|
|
78
|
+
aria-modal="true"
|
|
79
|
+
>
|
|
80
|
+
<div
|
|
81
|
+
onClick={(e) => e.stopPropagation()}
|
|
82
|
+
className="relative w-[560px] max-w-[94vw] bg-surface border border-border-light rounded-2xl p-5 shadow-2xl flex flex-col gap-3"
|
|
83
|
+
>
|
|
84
|
+
<button
|
|
85
|
+
onClick={toggle}
|
|
86
|
+
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"
|
|
87
|
+
title="Close (Esc)"
|
|
88
|
+
>
|
|
89
|
+
<i className="bx bx-x text-lg" />
|
|
90
|
+
</button>
|
|
91
|
+
|
|
92
|
+
<div className="flex items-center gap-2">
|
|
93
|
+
<div className="w-8 h-8 rounded-lg bg-accent-blue/10 border border-accent-blue/30 flex items-center justify-center">
|
|
94
|
+
<i className="bx bx-code-alt text-accent-blue" />
|
|
95
|
+
</div>
|
|
96
|
+
<div className="flex-1">
|
|
97
|
+
<h2 className="text-text-primary text-sm font-medium">LixScript</h2>
|
|
98
|
+
<p className="text-text-dim text-[11px]">Type shapes + arrows, render directly. No AI involved.</p>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<textarea
|
|
103
|
+
ref={taRef}
|
|
104
|
+
value={source}
|
|
105
|
+
onChange={(e) => setSource(e.target.value)}
|
|
106
|
+
onKeyDown={handleKeyDown}
|
|
107
|
+
spellCheck={false}
|
|
108
|
+
className="w-full h-[260px] 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"
|
|
109
|
+
/>
|
|
110
|
+
|
|
111
|
+
<div className="flex items-center justify-between">
|
|
112
|
+
{status ? (
|
|
113
|
+
<span className={`text-[11px] ${status.tone === 'error' ? 'text-red-400' : 'text-green-400'}`}>
|
|
114
|
+
{status.message}
|
|
115
|
+
</span>
|
|
116
|
+
) : (
|
|
117
|
+
<span className="text-text-dim text-[11px]">Cmd/Ctrl+Enter to render · Esc to close</span>
|
|
118
|
+
)}
|
|
119
|
+
<button
|
|
120
|
+
onClick={render}
|
|
121
|
+
disabled={busy}
|
|
122
|
+
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"
|
|
123
|
+
>
|
|
124
|
+
{busy ? 'Rendering…' : 'Render'}
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import useUIStore from '../../store/useUIStore'
|
|
4
|
+
import { useTranslation } from '../../hooks/useTranslation'
|
|
5
|
+
import { useMemo } from 'react'
|
|
6
|
+
|
|
7
|
+
function ShortcutRow({ keys, action }) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex items-center justify-between py-1.5">
|
|
10
|
+
<span className="text-text-secondary text-xs">{action}</span>
|
|
11
|
+
<div className="flex items-center gap-1">
|
|
12
|
+
{keys.split('+').map((key, i) => (
|
|
13
|
+
<span key={i}>
|
|
14
|
+
{i > 0 && <span className="text-text-dim text-xs mx-0.5">+</span>}
|
|
15
|
+
<kbd className="px-1.5 py-0.5 bg-surface-dark rounded text-text-muted text-xs border border-border font-[lixFont]">
|
|
16
|
+
{key.trim()}
|
|
17
|
+
</kbd>
|
|
18
|
+
</span>
|
|
19
|
+
))}
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ShortcutSection({ title, shortcuts }) {
|
|
26
|
+
return (
|
|
27
|
+
<div>
|
|
28
|
+
<h3 className="text-text-dim text-xs uppercase tracking-wider mb-2">{title}</h3>
|
|
29
|
+
{shortcuts.map((s) => (
|
|
30
|
+
<ShortcutRow key={s.action} keys={s.keys} action={s.action} />
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default function ShortcutsModal() {
|
|
37
|
+
const { t } = useTranslation()
|
|
38
|
+
const shortcutsModalOpen = useUIStore((s) => s.shortcutsModalOpen)
|
|
39
|
+
const toggleShortcutsModal = useUIStore((s) => s.toggleShortcutsModal)
|
|
40
|
+
|
|
41
|
+
const TOOL_SHORTCUTS = useMemo(() => [
|
|
42
|
+
{ keys: 'H', action: t('shortcuts.pan') },
|
|
43
|
+
{ keys: 'V / 1', action: t('shortcuts.selection') },
|
|
44
|
+
{ keys: 'R / 2', action: t('shortcuts.rectangle') },
|
|
45
|
+
{ keys: '3', action: t('shortcuts.diamond') },
|
|
46
|
+
{ keys: 'O / 4', action: t('shortcuts.circle') },
|
|
47
|
+
{ keys: 'A / 5', action: t('shortcuts.arrow') },
|
|
48
|
+
{ keys: 'L / 6', action: t('shortcuts.line') },
|
|
49
|
+
{ keys: 'P / 7', action: t('shortcuts.freehand') },
|
|
50
|
+
{ keys: 'T / 8', action: t('shortcuts.text') },
|
|
51
|
+
{ keys: '9', action: t('shortcuts.image') },
|
|
52
|
+
{ keys: 'E / 0', action: t('shortcuts.eraser') },
|
|
53
|
+
{ keys: 'F', action: t('shortcuts.frame') },
|
|
54
|
+
{ keys: 'K', action: t('shortcuts.laser') },
|
|
55
|
+
], [t])
|
|
56
|
+
|
|
57
|
+
const ACTION_SHORTCUTS = useMemo(() => [
|
|
58
|
+
{ keys: 'Ctrl+A', action: t('shortcuts.selectAll') },
|
|
59
|
+
{ keys: 'Ctrl+G', action: t('shortcuts.group') },
|
|
60
|
+
{ keys: 'Ctrl+Shift+G', action: t('shortcuts.ungroup') },
|
|
61
|
+
{ keys: 'Ctrl+D', action: t('shortcuts.duplicate') },
|
|
62
|
+
{ keys: 'Ctrl+S', action: t('shortcuts.quickSave') },
|
|
63
|
+
{ keys: 'Ctrl+Shift+S', action: t('shortcuts.saveShare') },
|
|
64
|
+
{ keys: 'Ctrl+F', action: t('shortcuts.findOnCanvas') },
|
|
65
|
+
{ keys: 'Ctrl+C', action: t('shortcuts.copy') },
|
|
66
|
+
{ keys: 'Ctrl+V', action: t('shortcuts.paste') },
|
|
67
|
+
{ keys: 'Ctrl+Z', action: t('shortcuts.undo') },
|
|
68
|
+
{ keys: 'Ctrl+Shift+Z', action: t('shortcuts.redo') },
|
|
69
|
+
{ keys: 'Esc', action: t('shortcuts.deselect') },
|
|
70
|
+
{ keys: 'Del', action: t('shortcuts.delete') },
|
|
71
|
+
{ keys: 'Space', action: t('shortcuts.holdToPan') },
|
|
72
|
+
{ keys: 'Shift', action: t('shortcuts.straightDraw') },
|
|
73
|
+
], [t])
|
|
74
|
+
|
|
75
|
+
const VIEW_SHORTCUTS = useMemo(() => [
|
|
76
|
+
{ keys: 'Ctrl++', action: t('shortcuts.zoomIn') },
|
|
77
|
+
{ keys: 'Ctrl+-', action: t('shortcuts.zoomOut') },
|
|
78
|
+
{ keys: 'Ctrl+0', action: t('shortcuts.resetZoom') },
|
|
79
|
+
{ keys: "Ctrl+'", action: t('shortcuts.toggleGrid') },
|
|
80
|
+
{ keys: 'Ctrl+/', action: t('shortcuts.shortcutsHelp') },
|
|
81
|
+
], [t])
|
|
82
|
+
|
|
83
|
+
if (!shortcutsModalOpen) return null
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div
|
|
87
|
+
className="fixed inset-0 z-[9999] flex items-center justify-center font-[lixFont]"
|
|
88
|
+
onClick={toggleShortcutsModal}
|
|
89
|
+
>
|
|
90
|
+
{/* Backdrop */}
|
|
91
|
+
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
|
92
|
+
|
|
93
|
+
{/* Modal */}
|
|
94
|
+
<div
|
|
95
|
+
className="relative bg-surface-card border border-border-light rounded-2xl p-6 max-w-[600px] w-full mx-4 max-h-[80vh] overflow-y-auto no-scrollbar"
|
|
96
|
+
onClick={(e) => e.stopPropagation()}
|
|
97
|
+
>
|
|
98
|
+
{/* Header */}
|
|
99
|
+
<div className="flex items-center justify-between mb-5">
|
|
100
|
+
<h2 className="text-text-primary text-base font-medium">
|
|
101
|
+
{t('shortcuts.title')}
|
|
102
|
+
</h2>
|
|
103
|
+
<button
|
|
104
|
+
onClick={toggleShortcutsModal}
|
|
105
|
+
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"
|
|
106
|
+
>
|
|
107
|
+
<i className="bx bx-x text-xl" />
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{/* Two columns */}
|
|
112
|
+
<div className="grid grid-cols-2 gap-6">
|
|
113
|
+
{/* Left column - Tools */}
|
|
114
|
+
<ShortcutSection title={t('shortcuts.tools')} shortcuts={TOOL_SHORTCUTS} />
|
|
115
|
+
|
|
116
|
+
{/* Right column - Actions + View */}
|
|
117
|
+
<div className="flex flex-col gap-4">
|
|
118
|
+
<ShortcutSection title={t('shortcuts.actions')} shortcuts={ACTION_SHORTCUTS} />
|
|
119
|
+
<ShortcutSection title={t('shortcuts.view')} shortcuts={VIEW_SHORTCUTS} />
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import useSketchStore, { TOOLS } from '../../store/useSketchStore'
|
|
4
|
+
import ShapeSidebar, { ToolbarButton, Divider, LayerControls } from './ShapeSidebar'
|
|
5
|
+
import { useState, useCallback } from 'react'
|
|
6
|
+
import { useTranslation } from '../../hooks/useTranslation'
|
|
7
|
+
|
|
8
|
+
const STROKE_COLORS = ['#1a1a20', '#ffffff', '#FF8383', '#3A994C', '#56A2E8', '#FFD700', '#FF69B4', '#A855F7']
|
|
9
|
+
|
|
10
|
+
const HEAD_STYLES = [
|
|
11
|
+
{ value: 'default', svg: '<svg width="22" height="12" viewBox="0 0 24 14"><line x1="2" y1="7" x2="16" y2="7" stroke="#fff" stroke-width="2"/><polyline points="13,2 19,7 13,12" fill="none" stroke="#fff" stroke-width="2" stroke-linejoin="round"/></svg>' },
|
|
12
|
+
{ value: 'square', svg: '<svg width="22" height="12" viewBox="0 0 24 14"><line x1="2" y1="7" x2="14" y2="7" stroke="#fff" stroke-width="2"/><rect x="14" y="3" width="6" height="8" fill="none" stroke="#fff" stroke-width="1.5"/></svg>' },
|
|
13
|
+
{ value: 'outline', svg: '<svg width="22" height="12" viewBox="0 0 24 14"><line x1="2" y1="7" x2="13" y2="7" stroke="#fff" stroke-width="2"/><polygon points="13,2 21,7 13,12" fill="none" stroke="#fff" stroke-width="1.5"/></svg>' },
|
|
14
|
+
{ value: 'solid', svg: '<svg width="22" height="12" viewBox="0 0 24 14"><line x1="2" y1="7" x2="13" y2="7" stroke="#fff" stroke-width="2"/><polygon points="13,2 21,7 13,12" fill="#fff"/></svg>' },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
function SvgIcon({ svg }) {
|
|
18
|
+
return <span dangerouslySetInnerHTML={{ __html: svg }} />
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ColorGrid({ colors, selected, onSelect }) {
|
|
22
|
+
return (
|
|
23
|
+
<div className="grid grid-cols-4 gap-1.5">
|
|
24
|
+
{colors.map((c) => (
|
|
25
|
+
<button key={c} onClick={() => onSelect(c)}
|
|
26
|
+
className={`w-7 h-7 rounded-md border-[1.5px] transition-all duration-100 ${selected === c ? 'border-[#5B57D1] scale-110' : 'border-white/[0.08] hover:border-white/20'}`}
|
|
27
|
+
style={{ backgroundColor: c }}
|
|
28
|
+
/>
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default function ArrowSidebar() {
|
|
35
|
+
const { t } = useTranslation()
|
|
36
|
+
const activeTool = useSketchStore((s) => s.activeTool)
|
|
37
|
+
const selectedShapeSidebar = useSketchStore((s) => s.selectedShapeSidebar)
|
|
38
|
+
const [headStyle, setHeadStyle] = useState('default')
|
|
39
|
+
const [strokeColor, setStrokeColor] = useState('#fff')
|
|
40
|
+
const [thickness, setThickness] = useState(2)
|
|
41
|
+
const [outlineStyle, setOutlineStyle] = useState('solid')
|
|
42
|
+
const [arrowType, setArrowType] = useState('straight')
|
|
43
|
+
const [curvature, setCurvature] = useState(20)
|
|
44
|
+
|
|
45
|
+
// Bridge helpers: update React state + engine state + selected arrow
|
|
46
|
+
const updateHead = useCallback((v) => {
|
|
47
|
+
setHeadStyle(v)
|
|
48
|
+
if (window.arrowToolSettings) window.arrowToolSettings.headStyle = v
|
|
49
|
+
if (window.updateSelectedArrowStyle) window.updateSelectedArrowStyle({ arrowHeadStyle: v })
|
|
50
|
+
}, [])
|
|
51
|
+
|
|
52
|
+
const updateStroke = useCallback((v) => {
|
|
53
|
+
setStrokeColor(v)
|
|
54
|
+
if (window.arrowToolSettings) window.arrowToolSettings.strokeColor = v
|
|
55
|
+
if (window.updateSelectedArrowStyle) window.updateSelectedArrowStyle({ stroke: v })
|
|
56
|
+
}, [])
|
|
57
|
+
|
|
58
|
+
const updateThickness = useCallback((v) => {
|
|
59
|
+
setThickness(v)
|
|
60
|
+
if (window.arrowToolSettings) window.arrowToolSettings.strokeWidth = v
|
|
61
|
+
if (window.updateSelectedArrowStyle) window.updateSelectedArrowStyle({ strokeWidth: v })
|
|
62
|
+
}, [])
|
|
63
|
+
|
|
64
|
+
const updateOutline = useCallback((v) => {
|
|
65
|
+
setOutlineStyle(v)
|
|
66
|
+
if (window.arrowToolSettings) window.arrowToolSettings.outlineStyle = v
|
|
67
|
+
if (window.updateSelectedArrowStyle) window.updateSelectedArrowStyle({ arrowOutlineStyle: v })
|
|
68
|
+
}, [])
|
|
69
|
+
|
|
70
|
+
const updateType = useCallback((v) => {
|
|
71
|
+
setArrowType(v)
|
|
72
|
+
if (window.arrowToolSettings) window.arrowToolSettings.arrowCurved = v
|
|
73
|
+
if (window.updateSelectedArrowStyle) window.updateSelectedArrowStyle({ arrowCurved: v })
|
|
74
|
+
}, [])
|
|
75
|
+
|
|
76
|
+
const updateCurvature = useCallback((v) => {
|
|
77
|
+
setCurvature(v)
|
|
78
|
+
if (window.arrowToolSettings) window.arrowToolSettings.curveAmount = v
|
|
79
|
+
if (window.updateSelectedArrowStyle) window.updateSelectedArrowStyle({ arrowCurveAmount: v })
|
|
80
|
+
}, [])
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<ShapeSidebar visible={activeTool === TOOLS.ARROW || selectedShapeSidebar === 'arrow'}>
|
|
84
|
+
{/* Head style */}
|
|
85
|
+
<ToolbarButton icon="bxs-right-arrow" tooltip="Arrow head">
|
|
86
|
+
<p className="text-xs text-text-secondary uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.head')}</p>
|
|
87
|
+
<div className="flex items-center gap-1">
|
|
88
|
+
{HEAD_STYLES.map((h) => (
|
|
89
|
+
<button key={h.value} onClick={() => updateHead(h.value)}
|
|
90
|
+
className={`w-10 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${headStyle === h.value ? 'bg-[#5B57D1]/20 text-[#5B57D1]' : 'text-text-secondary hover:bg-white/[0.06]'}`}
|
|
91
|
+
>
|
|
92
|
+
<SvgIcon svg={h.svg} />
|
|
93
|
+
</button>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
</ToolbarButton>
|
|
97
|
+
|
|
98
|
+
<Divider />
|
|
99
|
+
|
|
100
|
+
<ToolbarButton tooltip={t('sidebar.strokeColor')}
|
|
101
|
+
preview={<span className="w-4 h-4 rounded-md border border-white/20" style={{ backgroundColor: strokeColor }} />}
|
|
102
|
+
>
|
|
103
|
+
<p className="text-xs text-text-secondary uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.stroke')}</p>
|
|
104
|
+
<ColorGrid colors={STROKE_COLORS} selected={strokeColor} onSelect={updateStroke} />
|
|
105
|
+
</ToolbarButton>
|
|
106
|
+
|
|
107
|
+
<Divider />
|
|
108
|
+
|
|
109
|
+
<ToolbarButton icon="bxs-edit-alt" tooltip={t('sidebar.strokeWidth')}>
|
|
110
|
+
<p className="text-xs text-text-secondary uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.width')}</p>
|
|
111
|
+
<div className="flex items-center gap-1">
|
|
112
|
+
{[1, 2, 4, 7].map((w) => (
|
|
113
|
+
<button key={w} onClick={() => updateThickness(w)}
|
|
114
|
+
className={`w-9 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${thickness === w ? 'bg-[#5B57D1]/20 text-[#5B57D1]' : 'text-text-secondary hover:bg-white/[0.06]'}`}
|
|
115
|
+
>
|
|
116
|
+
<div className="w-5 rounded-full bg-current" style={{ height: Math.max(1, w) }} />
|
|
117
|
+
</button>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
</ToolbarButton>
|
|
121
|
+
|
|
122
|
+
<Divider />
|
|
123
|
+
|
|
124
|
+
<ToolbarButton icon="bxs-minus-circle" tooltip="Stroke style">
|
|
125
|
+
<p className="text-xs text-text-secondary uppercase tracking-wider mb-2">{t('sidebar.style')}</p>
|
|
126
|
+
<div className="flex items-center gap-1">
|
|
127
|
+
{[{ v: 'solid', d: '' }, { v: 'dashed', d: '6 4' }, { v: 'dotted', d: '2 3' }].map((s) => (
|
|
128
|
+
<button key={s.v} onClick={() => updateOutline(s.v)}
|
|
129
|
+
className={`w-11 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${outlineStyle === s.v ? 'bg-[#5B57D1]/20' : 'hover:bg-white/[0.06]'}`}
|
|
130
|
+
>
|
|
131
|
+
<svg width="28" height="4" viewBox="0 0 28 4"><line x1="0" y1="2" x2="28" y2="2" stroke="#fff" strokeWidth="2" strokeDasharray={s.d} strokeLinecap="round" /></svg>
|
|
132
|
+
</button>
|
|
133
|
+
))}
|
|
134
|
+
</div>
|
|
135
|
+
</ToolbarButton>
|
|
136
|
+
|
|
137
|
+
<Divider />
|
|
138
|
+
|
|
139
|
+
{/* Arrow type */}
|
|
140
|
+
<ToolbarButton icon="bxs-share-alt" tooltip="Arrow type">
|
|
141
|
+
<p className="text-xs text-text-secondary uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.type')}</p>
|
|
142
|
+
<div className="flex flex-col gap-0.5">
|
|
143
|
+
{[
|
|
144
|
+
{ v: 'straight', i: 'bxs-right-arrow-alt', l: 'Straight' },
|
|
145
|
+
{ v: 'curved', i: 'bxs-analyse', l: 'Curved' },
|
|
146
|
+
{ v: 'elbow', i: 'bxs-network-chart', l: 'Elbow' },
|
|
147
|
+
].map((a) => (
|
|
148
|
+
<button key={a.v} onClick={() => updateType(a.v)}
|
|
149
|
+
className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 ${arrowType === a.v ? 'bg-[#5B57D1] text-white' : 'text-text-secondary hover:bg-white/[0.06]'}`}
|
|
150
|
+
>
|
|
151
|
+
<i className={`bx ${a.i} text-sm`} /> {a.l}
|
|
152
|
+
</button>
|
|
153
|
+
))}
|
|
154
|
+
{arrowType === 'curved' && (
|
|
155
|
+
<>
|
|
156
|
+
<div className="w-full h-px bg-white/[0.08] my-1" />
|
|
157
|
+
<p className="text-[9px] text-text-dim uppercase tracking-wider mb-1">Curvature</p>
|
|
158
|
+
<div className="flex items-center gap-1">
|
|
159
|
+
{[{ v: 8, l: 'Lo' }, { v: 20, l: 'Md' }, { v: 40, l: 'Hi' }].map((c) => (
|
|
160
|
+
<button key={c.v} onClick={() => updateCurvature(c.v)}
|
|
161
|
+
className={`flex-1 py-1 rounded-md text-xs text-center transition-all duration-100 ${curvature === c.v ? 'bg-[#5B57D1]/20 text-[#5B57D1]' : 'text-text-secondary hover:bg-white/[0.06]'}`}
|
|
162
|
+
>{c.l}</button>
|
|
163
|
+
))}
|
|
164
|
+
</div>
|
|
165
|
+
</>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
</ToolbarButton>
|
|
169
|
+
<Divider />
|
|
170
|
+
<LayerControls />
|
|
171
|
+
</ShapeSidebar>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import useSketchStore, { TOOLS } from '../../store/useSketchStore'
|
|
4
|
+
import ShapeSidebar, { ToolbarButton, Divider, LayerControls } from './ShapeSidebar'
|
|
5
|
+
import { useState, useCallback } from 'react'
|
|
6
|
+
import { useTranslation } from '../../hooks/useTranslation'
|
|
7
|
+
|
|
8
|
+
const STROKE_COLORS = ['#1a1a20', '#ffffff', '#FF8383', '#3A994C', '#56A2E8', '#FFD700', '#FF69B4', '#A855F7']
|
|
9
|
+
const BG_COLORS = ['transparent', '#f0f0f0', '#ffcccb', '#90ee90', '#add8e6', '#FFE4B5', '#DDA0DD', '#2d2d2d']
|
|
10
|
+
|
|
11
|
+
const FILLS = [
|
|
12
|
+
{ value: 'hachure', label: 'sidebar.fill.hachure' },
|
|
13
|
+
{ value: 'solid', label: 'sidebar.fill.solid' },
|
|
14
|
+
{ value: 'dots', label: 'sidebar.fill.dots' },
|
|
15
|
+
{ value: 'cross-hatch', label: 'sidebar.fill.cross' },
|
|
16
|
+
{ value: 'transparent', label: 'sidebar.fill.none' },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
function ColorGrid({ colors, selected, onSelect }) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="grid grid-cols-4 gap-1.5">
|
|
22
|
+
{colors.map((c) => {
|
|
23
|
+
const isTrans = c === 'transparent'
|
|
24
|
+
return (
|
|
25
|
+
<button
|
|
26
|
+
key={c}
|
|
27
|
+
onClick={() => onSelect(c)}
|
|
28
|
+
className={`w-7 h-7 rounded-md border-[1.5px] transition-all duration-100 ${
|
|
29
|
+
selected === c ? 'border-accent-blue scale-110' : 'border-white/[0.08] hover:border-white/20'
|
|
30
|
+
}`}
|
|
31
|
+
style={!isTrans ? { backgroundColor: c } : undefined}
|
|
32
|
+
>
|
|
33
|
+
{isTrans && (
|
|
34
|
+
<svg className="w-full h-full text-text-dim" viewBox="0 0 20 20">
|
|
35
|
+
<line x1="4" y1="16" x2="16" y2="4" stroke="currentColor" strokeWidth="2" />
|
|
36
|
+
</svg>
|
|
37
|
+
)}
|
|
38
|
+
</button>
|
|
39
|
+
)
|
|
40
|
+
})}
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default function CircleSidebar() {
|
|
46
|
+
const { t } = useTranslation()
|
|
47
|
+
const activeTool = useSketchStore((s) => s.activeTool)
|
|
48
|
+
const selectedShapeSidebar = useSketchStore((s) => s.selectedShapeSidebar)
|
|
49
|
+
const [strokeColor, setStrokeColor] = useState('#fff')
|
|
50
|
+
const [bgColor, setBgColor] = useState('transparent')
|
|
51
|
+
const [thickness, setThickness] = useState(2)
|
|
52
|
+
const [lineStyle, setLineStyle] = useState('solid')
|
|
53
|
+
const [fillStyle, setFillStyle] = useState('hachure')
|
|
54
|
+
|
|
55
|
+
const updateStroke = useCallback((v) => { setStrokeColor(v); if (window.updateSelectedCircleStyle) window.updateSelectedCircleStyle({ stroke: v }) }, [])
|
|
56
|
+
const updateBg = useCallback((v) => { setBgColor(v); if (window.updateSelectedCircleStyle) window.updateSelectedCircleStyle({ fill: v }) }, [])
|
|
57
|
+
const updateThickness = useCallback((v) => { setThickness(v); if (window.updateSelectedCircleStyle) window.updateSelectedCircleStyle({ strokeWidth: v }) }, [])
|
|
58
|
+
const updateStyle = useCallback((v) => { setLineStyle(v); if (window.updateSelectedCircleStyle) window.updateSelectedCircleStyle({ outlineStyle: v }) }, [])
|
|
59
|
+
const updateFill = useCallback((v) => { setFillStyle(v); if (window.updateSelectedCircleStyle) window.updateSelectedCircleStyle({ fillStyle: v }) }, [])
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<ShapeSidebar visible={activeTool === TOOLS.CIRCLE || selectedShapeSidebar === 'circle'}>
|
|
63
|
+
<ToolbarButton
|
|
64
|
+
tooltip={t('sidebar.strokeColor')}
|
|
65
|
+
preview={<span className="w-4 h-4 rounded-md border border-white/20" style={{ backgroundColor: strokeColor }} />}
|
|
66
|
+
>
|
|
67
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.stroke')}</p>
|
|
68
|
+
<ColorGrid colors={STROKE_COLORS} selected={strokeColor} onSelect={updateStroke} />
|
|
69
|
+
</ToolbarButton>
|
|
70
|
+
|
|
71
|
+
<Divider />
|
|
72
|
+
|
|
73
|
+
<ToolbarButton
|
|
74
|
+
tooltip={t('sidebar.fillColor')}
|
|
75
|
+
preview={
|
|
76
|
+
<span className="w-4 h-4 rounded-md border border-white/20" style={{ backgroundColor: bgColor === 'transparent' ? 'transparent' : bgColor }}>
|
|
77
|
+
{bgColor === 'transparent' && <svg className="w-full h-full text-text-dim" viewBox="0 0 16 16"><line x1="2" y1="14" x2="14" y2="2" stroke="currentColor" strokeWidth="1.5" /></svg>}
|
|
78
|
+
</span>
|
|
79
|
+
}
|
|
80
|
+
>
|
|
81
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.background')}</p>
|
|
82
|
+
<ColorGrid colors={BG_COLORS} selected={bgColor} onSelect={updateBg} />
|
|
83
|
+
</ToolbarButton>
|
|
84
|
+
|
|
85
|
+
<Divider />
|
|
86
|
+
|
|
87
|
+
<ToolbarButton icon="bxs-edit-alt" tooltip={t('sidebar.strokeWidth')}>
|
|
88
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.width')}</p>
|
|
89
|
+
<div className="flex items-center gap-1">
|
|
90
|
+
{[1, 2, 4, 7].map((w) => (
|
|
91
|
+
<button key={w} onClick={() => updateThickness(w)}
|
|
92
|
+
className={`w-9 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${thickness === w ? 'bg-[#5B57D1]/20 text-[#5B57D1]' : 'text-text-muted hover:bg-white/[0.06]'}`}
|
|
93
|
+
>
|
|
94
|
+
<div className="w-5 rounded-full bg-current" style={{ height: Math.max(1, w) }} />
|
|
95
|
+
</button>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
</ToolbarButton>
|
|
99
|
+
|
|
100
|
+
<Divider />
|
|
101
|
+
|
|
102
|
+
<ToolbarButton icon="bxs-minus-circle" tooltip="Stroke style">
|
|
103
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.style')}</p>
|
|
104
|
+
<div className="flex items-center gap-1">
|
|
105
|
+
{[{ v: 'solid', d: '' }, { v: 'dashed', d: '6 4' }, { v: 'dotted', d: '2 3' }].map((s) => (
|
|
106
|
+
<button key={s.v} onClick={() => updateStyle(s.v)}
|
|
107
|
+
className={`w-11 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${lineStyle === s.v ? 'bg-[#5B57D1]/20' : 'hover:bg-white/[0.06]'}`}
|
|
108
|
+
>
|
|
109
|
+
<svg width="28" height="4" viewBox="0 0 28 4"><line x1="0" y1="2" x2="28" y2="2" stroke="#fff" strokeWidth="2" strokeDasharray={s.d} strokeLinecap="round" /></svg>
|
|
110
|
+
</button>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
</ToolbarButton>
|
|
114
|
+
|
|
115
|
+
<Divider />
|
|
116
|
+
|
|
117
|
+
<ToolbarButton icon="bxs-brush" tooltip="Fill style">
|
|
118
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.fill')}</p>
|
|
119
|
+
<div className="flex flex-col gap-0.5">
|
|
120
|
+
{FILLS.map((f) => (
|
|
121
|
+
<button key={f.value} onClick={() => updateFill(f.value)}
|
|
122
|
+
className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 ${fillStyle === f.value ? 'bg-[#5B57D1] text-white' : 'text-text-secondary hover:bg-white/[0.06]'}`}
|
|
123
|
+
>
|
|
124
|
+
<span className="w-1.5 h-1.5 rounded-full bg-current" />
|
|
125
|
+
{t(f.label)}
|
|
126
|
+
</button>
|
|
127
|
+
))}
|
|
128
|
+
</div>
|
|
129
|
+
</ToolbarButton>
|
|
130
|
+
<Divider />
|
|
131
|
+
<LayerControls />
|
|
132
|
+
</ShapeSidebar>
|
|
133
|
+
)
|
|
134
|
+
}
|