@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,109 @@
|
|
|
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
|
+
function ColorGrid({ colors, selected, onSelect }) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="grid grid-cols-4 gap-1.5">
|
|
13
|
+
{colors.map((c) => (
|
|
14
|
+
<button key={c} onClick={() => onSelect(c)}
|
|
15
|
+
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'}`}
|
|
16
|
+
style={{ backgroundColor: c }}
|
|
17
|
+
/>
|
|
18
|
+
))}
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default function LineSidebar() {
|
|
24
|
+
const { t } = useTranslation()
|
|
25
|
+
const activeTool = useSketchStore((s) => s.activeTool)
|
|
26
|
+
const selectedShapeSidebar = useSketchStore((s) => s.selectedShapeSidebar)
|
|
27
|
+
const [strokeColor, setStrokeColor] = useState('#fff')
|
|
28
|
+
const [thickness, setThickness] = useState(2)
|
|
29
|
+
const [lineStyle, setLineStyle] = useState('solid')
|
|
30
|
+
const [sloppiness, setSloppiness] = useState(0)
|
|
31
|
+
const [edge, setEdge] = useState('smooth')
|
|
32
|
+
|
|
33
|
+
const updateStroke = useCallback((v) => { setStrokeColor(v); if (window.updateSelectedLineStyle) window.updateSelectedLineStyle({ stroke: v }) }, [])
|
|
34
|
+
const updateThickness = useCallback((v) => { setThickness(v); if (window.updateSelectedLineStyle) window.updateSelectedLineStyle({ strokeWidth: v }) }, [])
|
|
35
|
+
const updateStyle = useCallback((v) => { setLineStyle(v); if (window.updateSelectedLineStyle) window.updateSelectedLineStyle({ strokeStyle: v }) }, [])
|
|
36
|
+
const updateSloppiness = useCallback((v) => { setSloppiness(v); if (window.updateSelectedLineStyle) window.updateSelectedLineStyle({ sloppiness: v }) }, [])
|
|
37
|
+
const updateEdge = useCallback((v) => { setEdge(v); if (window.updateSelectedLineStyle) window.updateSelectedLineStyle({ edge: v === 'smooth' ? 1 : 5 }) }, [])
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<ShapeSidebar visible={activeTool === TOOLS.LINE || selectedShapeSidebar === 'line'}>
|
|
41
|
+
<ToolbarButton tooltip={t('sidebar.strokeColor')}
|
|
42
|
+
preview={<span className="w-4 h-4 rounded-md border border-white/20" style={{ backgroundColor: strokeColor }} />}
|
|
43
|
+
>
|
|
44
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.stroke')}</p>
|
|
45
|
+
<ColorGrid colors={STROKE_COLORS} selected={strokeColor} onSelect={updateStroke} />
|
|
46
|
+
</ToolbarButton>
|
|
47
|
+
|
|
48
|
+
<Divider />
|
|
49
|
+
|
|
50
|
+
<ToolbarButton icon="bxs-edit-alt" tooltip={t('sidebar.strokeWidth')}>
|
|
51
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.width')}</p>
|
|
52
|
+
<div className="flex items-center gap-1">
|
|
53
|
+
{[1, 2, 4, 7].map((w) => (
|
|
54
|
+
<button key={w} onClick={() => updateThickness(w)}
|
|
55
|
+
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]'}`}
|
|
56
|
+
>
|
|
57
|
+
<div className="w-5 rounded-full bg-current" style={{ height: Math.max(1, w) }} />
|
|
58
|
+
</button>
|
|
59
|
+
))}
|
|
60
|
+
</div>
|
|
61
|
+
</ToolbarButton>
|
|
62
|
+
|
|
63
|
+
<Divider />
|
|
64
|
+
|
|
65
|
+
<ToolbarButton icon="bxs-minus-circle" tooltip="Stroke style">
|
|
66
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.style')}</p>
|
|
67
|
+
<div className="flex items-center gap-1">
|
|
68
|
+
{[{ v: 'solid', d: '' }, { v: 'dashed', d: '6 4' }, { v: 'dotted', d: '2 3' }].map((s) => (
|
|
69
|
+
<button key={s.v} onClick={() => updateStyle(s.v)}
|
|
70
|
+
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]'}`}
|
|
71
|
+
>
|
|
72
|
+
<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>
|
|
73
|
+
</button>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
</ToolbarButton>
|
|
77
|
+
|
|
78
|
+
<Divider />
|
|
79
|
+
|
|
80
|
+
<ToolbarButton icon="bxs-shape-polygon" tooltip="Sloppiness">
|
|
81
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.sloppiness')}</p>
|
|
82
|
+
<div className="flex items-center gap-1">
|
|
83
|
+
{[{ v: 0, l: '0' }, { v: 2, l: '2' }, { v: 4, l: '4' }].map((s) => (
|
|
84
|
+
<button key={s.v} onClick={() => updateSloppiness(s.v)}
|
|
85
|
+
className={`w-8 h-8 flex items-center justify-center rounded-lg text-xs transition-all duration-100 ${sloppiness === s.v ? 'bg-[#5B57D1]/20 text-accent-blue' : 'text-text-muted hover:bg-white/6'}`}
|
|
86
|
+
>{s.l}</button>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
</ToolbarButton>
|
|
90
|
+
|
|
91
|
+
<Divider />
|
|
92
|
+
|
|
93
|
+
<ToolbarButton icon="bxs-landscape" tooltip="Edge">
|
|
94
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.edge')}</p>
|
|
95
|
+
<div className="flex flex-col gap-0.5">
|
|
96
|
+
{[{ v: 'smooth', i: 'bxs-droplet', l: 'Smooth' }, { v: 'rough', i: 'bxs-bolt', l: 'Rough' }].map((e) => (
|
|
97
|
+
<button key={e.v} onClick={() => updateEdge(e.v)}
|
|
98
|
+
className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 ${edge === e.v ? 'bg-accent-blue text-white' : 'text-text-secondary hover:bg-white/6'}`}
|
|
99
|
+
>
|
|
100
|
+
<i className={`bx ${e.i} text-sm`} /> {e.l}
|
|
101
|
+
</button>
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
</ToolbarButton>
|
|
105
|
+
<Divider />
|
|
106
|
+
<LayerControls />
|
|
107
|
+
</ShapeSidebar>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
// Map taper UI values to perfect-freehand thinning values
|
|
11
|
+
const TAPER_MAP = { uniform: 0, pen: 0.5, brush: 0.8 }
|
|
12
|
+
|
|
13
|
+
function ColorGrid({ colors, selected, onSelect }) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="grid grid-cols-4 gap-1.5">
|
|
16
|
+
{colors.map((c) => (
|
|
17
|
+
<button key={c} onClick={() => onSelect(c)}
|
|
18
|
+
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'}`}
|
|
19
|
+
style={{ backgroundColor: c }}
|
|
20
|
+
/>
|
|
21
|
+
))}
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default function PaintbrushSidebar() {
|
|
27
|
+
const { t } = useTranslation()
|
|
28
|
+
const activeTool = useSketchStore((s) => s.activeTool)
|
|
29
|
+
const selectedShapeSidebar = useSketchStore((s) => s.selectedShapeSidebar)
|
|
30
|
+
const [strokeColor, setStrokeColor] = useState('#fff')
|
|
31
|
+
const [thickness, setThickness] = useState(2)
|
|
32
|
+
const [lineStyle, setLineStyle] = useState('solid')
|
|
33
|
+
const [taper, setTaper] = useState('uniform')
|
|
34
|
+
const [roughness, setRoughness] = useState('smooth')
|
|
35
|
+
const [opacity, setOpacity] = useState(1)
|
|
36
|
+
|
|
37
|
+
const updateStroke = useCallback((v) => { setStrokeColor(v); if (window.updateSelectedFreehandStyle) window.updateSelectedFreehandStyle({ stroke: v }) }, [])
|
|
38
|
+
const updateThickness = useCallback((v) => { setThickness(v); if (window.updateSelectedFreehandStyle) window.updateSelectedFreehandStyle({ strokeWidth: v }) }, [])
|
|
39
|
+
const updateTaper = useCallback((v) => { setTaper(v); if (window.updateSelectedFreehandStyle) window.updateSelectedFreehandStyle({ thinning: TAPER_MAP[v] }) }, [])
|
|
40
|
+
const updateRoughness = useCallback((v) => { setRoughness(v); if (window.updateSelectedFreehandStyle) window.updateSelectedFreehandStyle({ roughness: v }) }, [])
|
|
41
|
+
const updateOpacity = useCallback((v) => { setOpacity(v); if (window.updateSelectedFreehandStyle) window.updateSelectedFreehandStyle({ opacity: v }) }, [])
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<ShapeSidebar visible={activeTool === TOOLS.FREEHAND || selectedShapeSidebar === 'paintbrush'}>
|
|
45
|
+
<ToolbarButton tooltip={t('sidebar.strokeColor')}
|
|
46
|
+
preview={<span className="w-4 h-4 rounded-md border border-white/20" style={{ backgroundColor: strokeColor }} />}
|
|
47
|
+
>
|
|
48
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.stroke')}</p>
|
|
49
|
+
<ColorGrid colors={STROKE_COLORS} selected={strokeColor} onSelect={updateStroke} />
|
|
50
|
+
</ToolbarButton>
|
|
51
|
+
|
|
52
|
+
<Divider />
|
|
53
|
+
|
|
54
|
+
<ToolbarButton icon="bxs-edit-alt" tooltip={t('sidebar.strokeWidth')}>
|
|
55
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.width')}</p>
|
|
56
|
+
<div className="flex items-center gap-1">
|
|
57
|
+
{[1, 2, 4, 7].map((w) => (
|
|
58
|
+
<button key={w} onClick={() => updateThickness(w)}
|
|
59
|
+
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]'}`}
|
|
60
|
+
>
|
|
61
|
+
<div className="w-5 rounded-full bg-current" style={{ height: Math.max(1, w) }} />
|
|
62
|
+
</button>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
</ToolbarButton>
|
|
66
|
+
|
|
67
|
+
<Divider />
|
|
68
|
+
|
|
69
|
+
<ToolbarButton icon="bxs-pen" tooltip="Taper">
|
|
70
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.taper')}</p>
|
|
71
|
+
<div className="flex flex-col gap-0.5">
|
|
72
|
+
{[
|
|
73
|
+
{ v: 'uniform', i: 'bxs-minus-circle', l: 'Uniform' },
|
|
74
|
+
{ v: 'pen', i: 'bxs-pen', l: 'Pen' },
|
|
75
|
+
{ v: 'brush', i: 'bxs-brush', l: 'Brush' },
|
|
76
|
+
].map((t) => (
|
|
77
|
+
<button key={t.v} onClick={() => updateTaper(t.v)}
|
|
78
|
+
className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 ${taper === t.v ? 'bg-[#5B57D1] text-white' : 'text-text-secondary hover:bg-white/[0.06]'}`}
|
|
79
|
+
>
|
|
80
|
+
<i className={`bx ${t.i} text-sm`} /> {t.l}
|
|
81
|
+
</button>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
</ToolbarButton>
|
|
85
|
+
|
|
86
|
+
<Divider />
|
|
87
|
+
|
|
88
|
+
<ToolbarButton icon="bxs-shape-polygon" tooltip="Roughness">
|
|
89
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.roughness')}</p>
|
|
90
|
+
<div className="flex flex-col gap-0.5">
|
|
91
|
+
{[
|
|
92
|
+
{ v: 'smooth', i: 'bxs-droplet', l: 'Smooth' },
|
|
93
|
+
{ v: 'medium', i: 'bxs-leaf', l: 'Medium' },
|
|
94
|
+
{ v: 'rough', i: 'bxs-bolt', l: 'Rough' },
|
|
95
|
+
].map((r) => (
|
|
96
|
+
<button key={r.v} onClick={() => updateRoughness(r.v)}
|
|
97
|
+
className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 ${roughness === r.v ? 'bg-[#5B57D1] text-white' : 'text-text-secondary hover:bg-white/[0.06]'}`}
|
|
98
|
+
>
|
|
99
|
+
<i className={`bx ${r.i} text-sm`} /> {r.l}
|
|
100
|
+
</button>
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
</ToolbarButton>
|
|
104
|
+
|
|
105
|
+
<Divider />
|
|
106
|
+
|
|
107
|
+
<ToolbarButton icon="bxs-sun" tooltip={t('sidebar.opacity')}>
|
|
108
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">Opacity {Math.round(opacity * 100)}%</p>
|
|
109
|
+
<input
|
|
110
|
+
type="range" min="0" max="1" step="0.05" value={opacity}
|
|
111
|
+
onChange={(e) => updateOpacity(parseFloat(e.target.value))}
|
|
112
|
+
className="w-28 h-1 bg-white/10 rounded-full appearance-none cursor-pointer accent-[#5B57D1]"
|
|
113
|
+
/>
|
|
114
|
+
</ToolbarButton>
|
|
115
|
+
<Divider />
|
|
116
|
+
<LayerControls />
|
|
117
|
+
</ShapeSidebar>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
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-[#5B57D1] 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 RectangleSidebar() {
|
|
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 update = useCallback((changes, localSetters) => {
|
|
56
|
+
Object.entries(localSetters).forEach(([, fn]) => fn())
|
|
57
|
+
if (window.updateSelectedRectStyle) window.updateSelectedRectStyle(changes)
|
|
58
|
+
}, [])
|
|
59
|
+
|
|
60
|
+
const updateStroke = useCallback((v) => { setStrokeColor(v); if (window.updateSelectedRectStyle) window.updateSelectedRectStyle({ stroke: v }) }, [])
|
|
61
|
+
const updateBg = useCallback((v) => { setBgColor(v); if (window.updateSelectedRectStyle) window.updateSelectedRectStyle({ fill: v }) }, [])
|
|
62
|
+
const updateThickness = useCallback((v) => { setThickness(v); if (window.updateSelectedRectStyle) window.updateSelectedRectStyle({ strokeWidth: v }) }, [])
|
|
63
|
+
const updateStyle = useCallback((v) => { setLineStyle(v); if (window.updateSelectedRectStyle) window.updateSelectedRectStyle({ outlineStyle: v }) }, [])
|
|
64
|
+
const updateFill = useCallback((v) => { setFillStyle(v); if (window.updateSelectedRectStyle) window.updateSelectedRectStyle({ fillStyle: v }) }, [])
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<ShapeSidebar visible={activeTool === TOOLS.RECTANGLE || selectedShapeSidebar === 'rectangle'}>
|
|
68
|
+
{/* Stroke color */}
|
|
69
|
+
<ToolbarButton
|
|
70
|
+
tooltip={t('sidebar.strokeColor')}
|
|
71
|
+
preview={<span className="w-4 h-4 rounded-md border border-white/20" style={{ backgroundColor: strokeColor }} />}
|
|
72
|
+
>
|
|
73
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.stroke')}</p>
|
|
74
|
+
<ColorGrid colors={STROKE_COLORS} selected={strokeColor} onSelect={updateStroke} />
|
|
75
|
+
</ToolbarButton>
|
|
76
|
+
|
|
77
|
+
<Divider />
|
|
78
|
+
|
|
79
|
+
{/* Background */}
|
|
80
|
+
<ToolbarButton
|
|
81
|
+
tooltip={t('sidebar.fillColor')}
|
|
82
|
+
preview={
|
|
83
|
+
<span className="w-4 h-4 rounded-md border border-white/20" style={{ backgroundColor: bgColor === 'transparent' ? 'transparent' : bgColor }}>
|
|
84
|
+
{bgColor === 'transparent' && (
|
|
85
|
+
<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>
|
|
86
|
+
)}
|
|
87
|
+
</span>
|
|
88
|
+
}
|
|
89
|
+
>
|
|
90
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.background')}</p>
|
|
91
|
+
<ColorGrid colors={BG_COLORS} selected={bgColor} onSelect={updateBg} />
|
|
92
|
+
</ToolbarButton>
|
|
93
|
+
|
|
94
|
+
<Divider />
|
|
95
|
+
|
|
96
|
+
{/* Thickness */}
|
|
97
|
+
<ToolbarButton icon="bxs-edit-alt" tooltip={t('sidebar.strokeWidth')}>
|
|
98
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.width')}</p>
|
|
99
|
+
<div className="flex items-center gap-1">
|
|
100
|
+
{[1, 2, 4, 7].map((w) => (
|
|
101
|
+
<button
|
|
102
|
+
key={w}
|
|
103
|
+
onClick={() => updateThickness(w)}
|
|
104
|
+
className={`w-9 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${
|
|
105
|
+
thickness === w ? 'bg-[#5B57D1]/20 text-[#5B57D1]' : 'text-text-muted hover:bg-white/[0.06]'
|
|
106
|
+
}`}
|
|
107
|
+
>
|
|
108
|
+
<div className="w-5 rounded-full bg-current" style={{ height: Math.max(1, w) }} />
|
|
109
|
+
</button>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
</ToolbarButton>
|
|
113
|
+
|
|
114
|
+
<Divider />
|
|
115
|
+
|
|
116
|
+
{/* Stroke style */}
|
|
117
|
+
<ToolbarButton icon="bxs-minus-circle" tooltip="Stroke style">
|
|
118
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.style')}</p>
|
|
119
|
+
<div className="flex items-center gap-1">
|
|
120
|
+
{[
|
|
121
|
+
{ v: 'solid', d: '' },
|
|
122
|
+
{ v: 'dashed', d: '6 4' },
|
|
123
|
+
{ v: 'dotted', d: '2 3' },
|
|
124
|
+
].map((s) => (
|
|
125
|
+
<button
|
|
126
|
+
key={s.v}
|
|
127
|
+
onClick={() => updateStyle(s.v)}
|
|
128
|
+
className={`w-11 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${
|
|
129
|
+
lineStyle === s.v ? 'bg-[#5B57D1]/20' : 'hover:bg-white/[0.06]'
|
|
130
|
+
}`}
|
|
131
|
+
>
|
|
132
|
+
<svg width="28" height="4" viewBox="0 0 28 4">
|
|
133
|
+
<line x1="0" y1="2" x2="28" y2="2" stroke="#fff" strokeWidth="2" strokeDasharray={s.d} strokeLinecap="round" />
|
|
134
|
+
</svg>
|
|
135
|
+
</button>
|
|
136
|
+
))}
|
|
137
|
+
</div>
|
|
138
|
+
</ToolbarButton>
|
|
139
|
+
|
|
140
|
+
<Divider />
|
|
141
|
+
|
|
142
|
+
{/* Fill pattern */}
|
|
143
|
+
<ToolbarButton icon="bxs-brush" tooltip="Fill style">
|
|
144
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.fill')}</p>
|
|
145
|
+
<div className="flex flex-col gap-0.5">
|
|
146
|
+
{FILLS.map((f) => (
|
|
147
|
+
<button
|
|
148
|
+
key={f.value}
|
|
149
|
+
onClick={() => updateFill(f.value)}
|
|
150
|
+
className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 ${
|
|
151
|
+
fillStyle === f.value ? 'bg-[#5B57D1] text-white' : 'text-text-secondary hover:bg-white/[0.06]'
|
|
152
|
+
}`}
|
|
153
|
+
>
|
|
154
|
+
<span className="w-1.5 h-1.5 rounded-full bg-current" />
|
|
155
|
+
{t(f.label)}
|
|
156
|
+
</button>
|
|
157
|
+
))}
|
|
158
|
+
</div>
|
|
159
|
+
</ToolbarButton>
|
|
160
|
+
|
|
161
|
+
<Divider />
|
|
162
|
+
<LayerControls />
|
|
163
|
+
</ShapeSidebar>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect } from 'react'
|
|
4
|
+
import useSketchStore from '../../store/useSketchStore'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A toolbar button that opens a popover panel above it on click.
|
|
8
|
+
* Shows an icon (or custom preview) in the bar, popover shows full options.
|
|
9
|
+
*/
|
|
10
|
+
export function ToolbarButton({ icon, preview, children, tooltip }) {
|
|
11
|
+
const [open, setOpen] = useState(false)
|
|
12
|
+
const ref = useRef(null)
|
|
13
|
+
|
|
14
|
+
// Close on outside click
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!open) return
|
|
17
|
+
const handler = (e) => {
|
|
18
|
+
if (ref.current && !ref.current.contains(e.target)) setOpen(false)
|
|
19
|
+
}
|
|
20
|
+
document.addEventListener('mousedown', handler)
|
|
21
|
+
return () => document.removeEventListener('mousedown', handler)
|
|
22
|
+
}, [open])
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div ref={ref} className="relative flex items-center">
|
|
26
|
+
<button
|
|
27
|
+
onClick={() => setOpen(!open)}
|
|
28
|
+
title={tooltip}
|
|
29
|
+
className={`h-9 flex items-center gap-1.5 px-3 rounded-lg transition-all duration-100 ${
|
|
30
|
+
open
|
|
31
|
+
? 'bg-surface-active text-text-primary'
|
|
32
|
+
: 'text-text-muted hover:text-text-primary hover:bg-surface-hover'
|
|
33
|
+
}`}
|
|
34
|
+
>
|
|
35
|
+
{preview || (icon && <i className={`bx ${icon} text-base`} />)}
|
|
36
|
+
<svg className={`w-2 h-2 opacity-40 transition-transform duration-100 ${open ? 'rotate-180' : ''}`} viewBox="0 0 8 5" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
37
|
+
<path d="M1 1l3 3 3-3" />
|
|
38
|
+
</svg>
|
|
39
|
+
</button>
|
|
40
|
+
|
|
41
|
+
{open && (
|
|
42
|
+
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 z-20">
|
|
43
|
+
<div className="bg-surface-card border border-border-light rounded-xl p-3 shadow-xl shadow-black/10 min-w-max">
|
|
44
|
+
{children}
|
|
45
|
+
</div>
|
|
46
|
+
{/* Arrow pointer */}
|
|
47
|
+
<div className="absolute -bottom-[5px] left-1/2 -translate-x-1/2 w-2.5 h-2.5 rotate-45 bg-surface-card border-r border-b border-border-light" />
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Simple toolbar divider
|
|
56
|
+
*/
|
|
57
|
+
function Divider() {
|
|
58
|
+
return <div className="w-px h-5 bg-surface-hover mx-0.5 shrink-0" />
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Bottom toolbar container - appears when tool/shape is active
|
|
63
|
+
*/
|
|
64
|
+
export default function ShapeSidebar({ visible, children }) {
|
|
65
|
+
const viewMode = useSketchStore((s) => s.viewMode)
|
|
66
|
+
const show = visible && !viewMode
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
className={`absolute bottom-14 left-1/2 -translate-x-1/2 bg-surface border border-border-light rounded-xl px-2 py-1.5 z-[999] font-[lixFont] transition-all duration-200 ${
|
|
71
|
+
show
|
|
72
|
+
? 'opacity-100 pointer-events-auto translate-y-0'
|
|
73
|
+
: 'opacity-0 pointer-events-none translate-y-2'
|
|
74
|
+
}`}
|
|
75
|
+
>
|
|
76
|
+
<div className="flex items-center gap-0.5">
|
|
77
|
+
{children}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Layer ordering controls - add to any shape sidebar
|
|
85
|
+
*/
|
|
86
|
+
import { useTranslation } from '../../hooks/useTranslation'
|
|
87
|
+
|
|
88
|
+
function LayerControls() {
|
|
89
|
+
const { t } = useTranslation()
|
|
90
|
+
const doLayer = (method) => {
|
|
91
|
+
const shape = window.currentShape
|
|
92
|
+
if (!shape || !window.__layerOrder) return
|
|
93
|
+
window.__layerOrder[method](shape)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="flex items-center gap-0.5">
|
|
98
|
+
<button
|
|
99
|
+
onClick={() => doLayer('sendToBack')}
|
|
100
|
+
title={t('sidebar.sendToBack', { defaultValue: 'Send to back' })}
|
|
101
|
+
className="h-9 w-8 flex items-center justify-center rounded-lg text-text-muted hover:text-text-primary hover:bg-surface-hover transition-all duration-100"
|
|
102
|
+
>
|
|
103
|
+
<i className="bx bx-chevrons-down text-base" />
|
|
104
|
+
</button>
|
|
105
|
+
<button
|
|
106
|
+
onClick={() => doLayer('sendBackward')}
|
|
107
|
+
title={t('sidebar.sendBackward')}
|
|
108
|
+
className="h-9 w-8 flex items-center justify-center rounded-lg text-text-muted hover:text-text-primary hover:bg-surface-hover transition-all duration-100"
|
|
109
|
+
>
|
|
110
|
+
<i className="bx bx-chevron-down text-base" />
|
|
111
|
+
</button>
|
|
112
|
+
<button
|
|
113
|
+
onClick={() => doLayer('bringForward')}
|
|
114
|
+
title={t('sidebar.bringForward')}
|
|
115
|
+
className="h-9 w-8 flex items-center justify-center rounded-lg text-text-muted hover:text-text-primary hover:bg-surface-hover transition-all duration-100"
|
|
116
|
+
>
|
|
117
|
+
<i className="bx bx-chevron-up text-base" />
|
|
118
|
+
</button>
|
|
119
|
+
<button
|
|
120
|
+
onClick={() => doLayer('bringToFront')}
|
|
121
|
+
title={t('sidebar.bringToFront', { defaultValue: 'Bring to front' })}
|
|
122
|
+
className="h-9 w-8 flex items-center justify-center rounded-lg text-text-muted hover:text-text-primary hover:bg-surface-hover transition-all duration-100"
|
|
123
|
+
>
|
|
124
|
+
<i className="bx bx-chevrons-up text-base" />
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export { Divider, LayerControls }
|