@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,263 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import useSketchStore, { TOOLS } from '../../store/useSketchStore'
|
|
4
|
+
import useUIStore from '../../store/useUIStore'
|
|
5
|
+
import ShapeSidebar, { ToolbarButton, Divider, LayerControls } from './ShapeSidebar'
|
|
6
|
+
import { useState, useCallback, useEffect } from 'react'
|
|
7
|
+
import { useTranslation } from '../../hooks/useTranslation'
|
|
8
|
+
import { compressImage } from '../../../utils/imageCompressor.js'
|
|
9
|
+
import { IMAGE_ACCEPT_ATTR, isAllowedImage } from '../../../utils/allowedImageTypes.js'
|
|
10
|
+
|
|
11
|
+
const FILL_STYLES = [
|
|
12
|
+
{ id: 'transparent', label: 'None', icon: 'bx-x' },
|
|
13
|
+
{ id: 'solid', label: 'Solid', icon: 'bxs-square' },
|
|
14
|
+
{ id: 'grid', label: 'Grid', icon: 'bx-grid-alt' },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
const FILL_COLORS = [
|
|
18
|
+
'#1e1e28',
|
|
19
|
+
'#13171C',
|
|
20
|
+
'#1a1a2e',
|
|
21
|
+
'#0d1117',
|
|
22
|
+
'#2d2d3a',
|
|
23
|
+
'#ffffff',
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
export default function FrameSidebar() {
|
|
27
|
+
const { t } = useTranslation()
|
|
28
|
+
const activeTool = useSketchStore((s) => s.activeTool)
|
|
29
|
+
const selectedShapeSidebar = useSketchStore((s) => s.selectedShapeSidebar)
|
|
30
|
+
const toggleAIModal = useUIStore((s) => s.toggleAIModal)
|
|
31
|
+
const [frameName, setFrameName] = useState('Frame 1')
|
|
32
|
+
const [isGraph, setIsGraph] = useState(false)
|
|
33
|
+
const [fillStyle, setFillStyle] = useState('transparent')
|
|
34
|
+
const [fillColor, setFillColor] = useState('#1e1e28')
|
|
35
|
+
|
|
36
|
+
// Sync state from the actual selected frame when sidebar opens
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (selectedShapeSidebar === 'frame' || activeTool === TOOLS.FRAME) {
|
|
39
|
+
const shape = typeof window !== 'undefined' ? window.currentShape : null
|
|
40
|
+
if (shape && shape.shapeName === 'frame') {
|
|
41
|
+
if (shape.frameName) setFrameName(shape.frameName)
|
|
42
|
+
setIsGraph(shape._frameType === 'graph')
|
|
43
|
+
setFillStyle(shape.fillStyle || 'transparent')
|
|
44
|
+
setFillColor(shape.fillColor || '#1e1e28')
|
|
45
|
+
} else {
|
|
46
|
+
setIsGraph(false)
|
|
47
|
+
setFillStyle('transparent')
|
|
48
|
+
setFillColor('#1e1e28')
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}, [selectedShapeSidebar, activeTool])
|
|
52
|
+
|
|
53
|
+
const updateName = useCallback((e) => {
|
|
54
|
+
const name = e.target.value
|
|
55
|
+
setFrameName(name)
|
|
56
|
+
const shape = window.currentShape
|
|
57
|
+
if (shape && shape.shapeName === 'frame') {
|
|
58
|
+
shape.frameName = name
|
|
59
|
+
shape.draw()
|
|
60
|
+
}
|
|
61
|
+
}, [])
|
|
62
|
+
|
|
63
|
+
const updateFillStyle = useCallback((style) => {
|
|
64
|
+
setFillStyle(style)
|
|
65
|
+
const shape = window.currentShape
|
|
66
|
+
if (shape && shape.shapeName === 'frame') {
|
|
67
|
+
shape.fillStyle = style
|
|
68
|
+
shape.draw()
|
|
69
|
+
}
|
|
70
|
+
}, [])
|
|
71
|
+
|
|
72
|
+
const updateFillColor = useCallback((color) => {
|
|
73
|
+
setFillColor(color)
|
|
74
|
+
const shape = window.currentShape
|
|
75
|
+
if (shape && shape.shapeName === 'frame') {
|
|
76
|
+
shape.fillColor = color
|
|
77
|
+
shape.draw()
|
|
78
|
+
}
|
|
79
|
+
}, [])
|
|
80
|
+
|
|
81
|
+
const resizeToFit = useCallback(() => {
|
|
82
|
+
const shape = window.currentShape
|
|
83
|
+
if (shape && shape.shapeName === 'frame' && typeof shape.resizeToFitContents === 'function') {
|
|
84
|
+
shape.resizeToFitContents()
|
|
85
|
+
}
|
|
86
|
+
}, [])
|
|
87
|
+
|
|
88
|
+
const handleAIEdit = useCallback(() => {
|
|
89
|
+
const shape = window.currentShape
|
|
90
|
+
if (shape && shape.shapeName === 'frame') {
|
|
91
|
+
window.__aiEditTargetFrame = shape
|
|
92
|
+
toggleAIModal()
|
|
93
|
+
}
|
|
94
|
+
}, [toggleAIModal])
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<ShapeSidebar visible={activeTool === TOOLS.FRAME || selectedShapeSidebar === 'frame'}>
|
|
98
|
+
<ToolbarButton icon="bxs-rename" tooltip="Frame name">
|
|
99
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.name')}</p>
|
|
100
|
+
<input
|
|
101
|
+
type="text"
|
|
102
|
+
value={frameName}
|
|
103
|
+
onChange={updateName}
|
|
104
|
+
className="w-32 px-2.5 py-1.5 bg-white/[0.05] border border-white/[0.1] rounded-lg text-white text-xs outline-none focus:border-[#5B57D1]/50 transition-all duration-150 font-[lixFont]"
|
|
105
|
+
spellCheck={false}
|
|
106
|
+
/>
|
|
107
|
+
</ToolbarButton>
|
|
108
|
+
|
|
109
|
+
<Divider />
|
|
110
|
+
|
|
111
|
+
{/* Fill style */}
|
|
112
|
+
<ToolbarButton icon="bxs-palette" tooltip="Fill style">
|
|
113
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.background')}</p>
|
|
114
|
+
<div className="flex gap-1 mb-2.5">
|
|
115
|
+
{FILL_STYLES.map((s) => (
|
|
116
|
+
<button
|
|
117
|
+
key={s.id}
|
|
118
|
+
onClick={() => updateFillStyle(s.id)}
|
|
119
|
+
title={s.label}
|
|
120
|
+
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 ${
|
|
121
|
+
fillStyle === s.id
|
|
122
|
+
? 'bg-white/[0.12] text-white'
|
|
123
|
+
: 'text-text-muted hover:text-white hover:bg-white/[0.06]'
|
|
124
|
+
}`}
|
|
125
|
+
>
|
|
126
|
+
<i className={`bx ${s.icon} text-sm`} />
|
|
127
|
+
{s.label}
|
|
128
|
+
</button>
|
|
129
|
+
))}
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* Color picker — only show for solid/grid */}
|
|
133
|
+
{fillStyle !== 'transparent' && (
|
|
134
|
+
<>
|
|
135
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-1.5">{t('sidebar.sectionHeader.color')}</p>
|
|
136
|
+
<div className="flex items-center gap-1.5">
|
|
137
|
+
{FILL_COLORS.map((c) => (
|
|
138
|
+
<button
|
|
139
|
+
key={c}
|
|
140
|
+
onClick={() => updateFillColor(c)}
|
|
141
|
+
title={c}
|
|
142
|
+
className={`w-6 h-6 rounded-md border-2 transition-all duration-100 ${
|
|
143
|
+
fillColor === c ? 'border-accent-blue scale-110' : 'border-white/10 hover:border-white/30'
|
|
144
|
+
}`}
|
|
145
|
+
style={{ backgroundColor: c }}
|
|
146
|
+
/>
|
|
147
|
+
))}
|
|
148
|
+
<label className="w-6 h-6 rounded-md border-2 border-white/10 hover:border-white/30 cursor-pointer overflow-hidden relative transition-all duration-100" title="Custom color">
|
|
149
|
+
<input
|
|
150
|
+
type="color"
|
|
151
|
+
value={fillColor}
|
|
152
|
+
onChange={(e) => updateFillColor(e.target.value)}
|
|
153
|
+
className="absolute inset-0 opacity-0 cursor-pointer"
|
|
154
|
+
/>
|
|
155
|
+
<i className="bx bx-palette text-xs text-text-muted absolute inset-0 flex items-center justify-center" />
|
|
156
|
+
</label>
|
|
157
|
+
</div>
|
|
158
|
+
</>
|
|
159
|
+
)}
|
|
160
|
+
</ToolbarButton>
|
|
161
|
+
|
|
162
|
+
<Divider />
|
|
163
|
+
|
|
164
|
+
<ToolbarButton icon="bx-image-alt" tooltip="Background image">
|
|
165
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.backgroundImage')}</p>
|
|
166
|
+
<div className="flex gap-1.5 mb-2">
|
|
167
|
+
<button
|
|
168
|
+
onClick={() => {
|
|
169
|
+
const input = document.createElement('input')
|
|
170
|
+
input.type = 'file'
|
|
171
|
+
input.accept = IMAGE_ACCEPT_ATTR
|
|
172
|
+
input.onchange = async (e) => {
|
|
173
|
+
const file = e.target.files?.[0]
|
|
174
|
+
if (!file) return
|
|
175
|
+
if (!isAllowedImage(file)) {
|
|
176
|
+
console.warn('[FrameSidebar] Rejected file type:', file.type)
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
const reader = new FileReader()
|
|
180
|
+
reader.onload = async (ev) => {
|
|
181
|
+
const shape = window.currentShape
|
|
182
|
+
if (!shape?.shapeName === 'frame' || !shape.setImageFromURL) return
|
|
183
|
+
// Compress heavily before setting
|
|
184
|
+
try {
|
|
185
|
+
const compressed = await compressImage(ev.target.result, { maxWidth: 1280, quality: 0.5 })
|
|
186
|
+
shape.setImageFromURL(compressed.dataUrl, shape._frameImageFit || 'cover')
|
|
187
|
+
} catch {
|
|
188
|
+
shape.setImageFromURL(ev.target.result, shape._frameImageFit || 'cover')
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
reader.readAsDataURL(file)
|
|
192
|
+
}
|
|
193
|
+
input.click()
|
|
194
|
+
}}
|
|
195
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-text-secondary text-xs hover:bg-white/[0.06] hover:text-white transition-all duration-100"
|
|
196
|
+
>
|
|
197
|
+
<i className="bx bx-upload text-sm" />
|
|
198
|
+
Set Image
|
|
199
|
+
</button>
|
|
200
|
+
<button
|
|
201
|
+
onClick={() => {
|
|
202
|
+
const shape = window.currentShape
|
|
203
|
+
if (shape?.shapeName === 'frame' && shape.setImageFromURL) {
|
|
204
|
+
shape.setImageFromURL(null)
|
|
205
|
+
shape.draw()
|
|
206
|
+
}
|
|
207
|
+
}}
|
|
208
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-text-secondary text-xs hover:bg-white/[0.06] hover:text-red-400 transition-all duration-100"
|
|
209
|
+
>
|
|
210
|
+
<i className="bx bx-x text-sm" />
|
|
211
|
+
Remove
|
|
212
|
+
</button>
|
|
213
|
+
</div>
|
|
214
|
+
{/* Fit controls — only show when frame has an image */}
|
|
215
|
+
{(() => {
|
|
216
|
+
const shape = typeof window !== 'undefined' ? window.currentShape : null
|
|
217
|
+
if (!shape?.shapeName === 'frame' || !shape?._frameImageURL) return null
|
|
218
|
+
return (
|
|
219
|
+
<>
|
|
220
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-1.5">{t('sidebar.sectionHeader.fit')}</p>
|
|
221
|
+
<div className="flex gap-1">
|
|
222
|
+
{[
|
|
223
|
+
{ id: 'cover', label: 'Cover' },
|
|
224
|
+
{ id: 'contain', label: 'Contain' },
|
|
225
|
+
{ id: 'fill', label: 'Stretch' },
|
|
226
|
+
].map((f) => (
|
|
227
|
+
<button
|
|
228
|
+
key={f.id}
|
|
229
|
+
onClick={() => {
|
|
230
|
+
const s = window.currentShape
|
|
231
|
+
if (s?.shapeName === 'frame' && s.setImageFromURL && s._frameImageURL) {
|
|
232
|
+
s.setImageFromURL(s._frameImageURL, f.id)
|
|
233
|
+
}
|
|
234
|
+
}}
|
|
235
|
+
className={`px-2 py-1 rounded-md text-[10px] transition-all ${
|
|
236
|
+
shape._frameImageFit === f.id
|
|
237
|
+
? 'bg-white/[0.12] text-white'
|
|
238
|
+
: 'text-text-muted hover:text-white hover:bg-white/[0.06]'
|
|
239
|
+
}`}
|
|
240
|
+
>{f.label}</button>
|
|
241
|
+
))}
|
|
242
|
+
</div>
|
|
243
|
+
</>
|
|
244
|
+
)
|
|
245
|
+
})()}
|
|
246
|
+
</ToolbarButton>
|
|
247
|
+
|
|
248
|
+
<Divider />
|
|
249
|
+
|
|
250
|
+
<ToolbarButton icon="bxs-expand" tooltip="Actions">
|
|
251
|
+
<button onClick={resizeToFit} className="flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-text-secondary text-xs hover:bg-white/[0.06] hover:text-white transition-all duration-100">
|
|
252
|
+
<i className="bx bxs-expand text-sm" />
|
|
253
|
+
Resize to Fit
|
|
254
|
+
</button>
|
|
255
|
+
</ToolbarButton>
|
|
256
|
+
|
|
257
|
+
{/* AI Edit / Graph Edit removed while AI is coming-soon. Restore the
|
|
258
|
+
button + handleAIEdit wiring when the assistant ships. */}
|
|
259
|
+
<Divider />
|
|
260
|
+
<LayerControls />
|
|
261
|
+
</ShapeSidebar>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import useSketchStore, { TOOLS } from '../../store/useSketchStore'
|
|
4
|
+
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
5
|
+
import { useTranslation } from '../../hooks/useTranslation'
|
|
6
|
+
|
|
7
|
+
// Origin of the icons API. When the package runs same-origin with
|
|
8
|
+
// sketch.elixpo this stays empty (relative path). External hosts (e.g.
|
|
9
|
+
// blogs.elixpo) point at the public sketch.elixpo deploy via
|
|
10
|
+
// window.__lixsketchIconsOrigin = 'https://sketch.elixpo.com'
|
|
11
|
+
// The endpoints are CORS-enabled so any origin can fetch.
|
|
12
|
+
const DEFAULT_ICONS_ORIGIN = 'https://sketch.elixpo.com';
|
|
13
|
+
function getIconsBaseUrl() {
|
|
14
|
+
if (typeof window === 'undefined') return '';
|
|
15
|
+
if (window.__lixsketchIconsOrigin) return window.__lixsketchIconsOrigin;
|
|
16
|
+
// Same-origin if the host is sketch.elixpo itself; otherwise default to prod.
|
|
17
|
+
if (typeof location !== 'undefined' && /sketch\.elixpo/.test(location.host)) return '';
|
|
18
|
+
return DEFAULT_ICONS_ORIGIN;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const iconResultCache = new Map()
|
|
22
|
+
|
|
23
|
+
const CATEGORIES = [
|
|
24
|
+
{ value: null, label: 'All', icon: 'bxs-grid-alt' },
|
|
25
|
+
{ value: 'tech', label: 'Tech', icon: 'bxs-chip' },
|
|
26
|
+
{ value: 'devops', label: 'DevOps', icon: 'bxs-server' },
|
|
27
|
+
{ value: 'design', label: 'Design', icon: 'bxs-palette' },
|
|
28
|
+
{ value: 'social media', label: 'Social', icon: 'bxs-share-alt' },
|
|
29
|
+
{ value: 'navigation', label: 'Nav', icon: 'bxs-navigation' },
|
|
30
|
+
{ value: 'business', label: 'Business', icon: 'bxs-briefcase' },
|
|
31
|
+
{ value: 'media', label: 'Media', icon: 'bxs-videos' },
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
// Normalize an SVG string so it renders fully within a fixed box.
|
|
35
|
+
// Ensures the inner <svg> has width/height="100%" and a viewBox.
|
|
36
|
+
function normalizeSvg(raw) {
|
|
37
|
+
if (!raw) return raw
|
|
38
|
+
// Parse to a temp element so we can inspect attributes
|
|
39
|
+
const tmp = document.createElement('div')
|
|
40
|
+
tmp.innerHTML = raw
|
|
41
|
+
const svgEl = tmp.querySelector('svg')
|
|
42
|
+
if (!svgEl) return raw
|
|
43
|
+
|
|
44
|
+
// Ensure viewBox exists — derive from width/height if missing
|
|
45
|
+
if (!svgEl.getAttribute('viewBox')) {
|
|
46
|
+
const w = parseFloat(svgEl.getAttribute('width')) || 24
|
|
47
|
+
const h = parseFloat(svgEl.getAttribute('height')) || 24
|
|
48
|
+
svgEl.setAttribute('viewBox', `0 0 ${w} ${h}`)
|
|
49
|
+
}
|
|
50
|
+
// Force the SVG to fill its container
|
|
51
|
+
svgEl.setAttribute('width', '100%')
|
|
52
|
+
svgEl.setAttribute('height', '100%')
|
|
53
|
+
return tmp.innerHTML
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function IconCell({ icon, onClick }) {
|
|
57
|
+
const name = icon.filename?.replace('.svg', '').replace(/_/g, ' ') || ''
|
|
58
|
+
const normalizedSvg = typeof document !== 'undefined' && icon.svg ? normalizeSvg(icon.svg) : icon.svg
|
|
59
|
+
return (
|
|
60
|
+
<button
|
|
61
|
+
onClick={onClick}
|
|
62
|
+
title={name}
|
|
63
|
+
style={{ width: '44px', height: '44px', overflow: 'hidden', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: '8px', cursor: 'pointer', background: 'transparent', border: 'none', padding: 0 }}
|
|
64
|
+
className="hover:bg-surface-hover transition-colors duration-100"
|
|
65
|
+
>
|
|
66
|
+
{normalizedSvg ? (
|
|
67
|
+
<div
|
|
68
|
+
style={{ width: '24px', height: '24px', overflow: 'visible', flexShrink: 0, pointerEvents: 'none', filter: 'brightness(0) invert(1)' }}
|
|
69
|
+
dangerouslySetInnerHTML={{ __html: normalizedSvg }}
|
|
70
|
+
/>
|
|
71
|
+
) : (
|
|
72
|
+
<img
|
|
73
|
+
src={`/icons/${encodeURIComponent(icon.filename)}`}
|
|
74
|
+
alt=""
|
|
75
|
+
style={{ width: '24px', height: '24px', pointerEvents: 'none', filter: 'invert(1)' }}
|
|
76
|
+
loading="lazy"
|
|
77
|
+
/>
|
|
78
|
+
)}
|
|
79
|
+
</button>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default function IconSidebar() {
|
|
84
|
+
const { t } = useTranslation()
|
|
85
|
+
const activeTool = useSketchStore((s) => s.activeTool)
|
|
86
|
+
const setActiveTool = useSketchStore((s) => s.setActiveTool)
|
|
87
|
+
const visible = activeTool === TOOLS.ICON
|
|
88
|
+
const [query, setQuery] = useState('')
|
|
89
|
+
const [category, setCategory] = useState(null)
|
|
90
|
+
const [icons, setIcons] = useState([])
|
|
91
|
+
const [loading, setLoading] = useState(false)
|
|
92
|
+
const debounceRef = useRef(null)
|
|
93
|
+
|
|
94
|
+
// Close on Escape
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!visible) return
|
|
97
|
+
const handler = (e) => {
|
|
98
|
+
if (e.key === 'Escape') {
|
|
99
|
+
e.stopPropagation()
|
|
100
|
+
setActiveTool(TOOLS.SELECT)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
document.addEventListener('keydown', handler, true)
|
|
104
|
+
return () => document.removeEventListener('keydown', handler, true)
|
|
105
|
+
}, [visible, setActiveTool])
|
|
106
|
+
|
|
107
|
+
// Close when clicking on the canvas (outside the sidebar)
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!visible) return
|
|
110
|
+
const svgEl = document.getElementById('freehand-canvas')
|
|
111
|
+
if (!svgEl) return
|
|
112
|
+
const handleCanvasClick = () => setActiveTool(TOOLS.SELECT)
|
|
113
|
+
svgEl.addEventListener('mousedown', handleCanvasClick)
|
|
114
|
+
return () => svgEl.removeEventListener('mousedown', handleCanvasClick)
|
|
115
|
+
}, [visible, setActiveTool])
|
|
116
|
+
|
|
117
|
+
const fetchIcons = useCallback(async (searchQuery, cat) => {
|
|
118
|
+
const params = new URLSearchParams()
|
|
119
|
+
if (searchQuery) params.set('q', searchQuery)
|
|
120
|
+
if (cat) params.set('category', cat)
|
|
121
|
+
params.set('inline', '1')
|
|
122
|
+
const cacheKey = params.toString()
|
|
123
|
+
|
|
124
|
+
// Return cached results instantly if available
|
|
125
|
+
if (iconResultCache.has(cacheKey)) {
|
|
126
|
+
setIcons(iconResultCache.get(cacheKey))
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
setLoading(true)
|
|
131
|
+
try {
|
|
132
|
+
const res = await fetch(`${getIconsBaseUrl()}/api/icons/search?${cacheKey}`)
|
|
133
|
+
if (res.ok) {
|
|
134
|
+
const data = await res.json()
|
|
135
|
+
const results = data.results || []
|
|
136
|
+
iconResultCache.set(cacheKey, results)
|
|
137
|
+
setIcons(results)
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.error('Icon fetch failed:', err)
|
|
141
|
+
}
|
|
142
|
+
setLoading(false)
|
|
143
|
+
}, [])
|
|
144
|
+
|
|
145
|
+
// Fetch icons when visibility, query, or category changes (debounced for query typing)
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (!visible) return
|
|
148
|
+
if (debounceRef.current) clearTimeout(debounceRef.current)
|
|
149
|
+
debounceRef.current = setTimeout(() => {
|
|
150
|
+
fetchIcons(query, category)
|
|
151
|
+
}, query ? 300 : 0)
|
|
152
|
+
return () => clearTimeout(debounceRef.current)
|
|
153
|
+
}, [query, visible, category, fetchIcons])
|
|
154
|
+
|
|
155
|
+
// Preload all categories in background on first sidebar open
|
|
156
|
+
const hasPreloaded = useRef(false)
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (!visible || hasPreloaded.current) return
|
|
159
|
+
hasPreloaded.current = true
|
|
160
|
+
// Fire-and-forget: preload each category so switching is instant
|
|
161
|
+
CATEGORIES.forEach((cat) => {
|
|
162
|
+
const params = new URLSearchParams()
|
|
163
|
+
if (cat.value) params.set('category', cat.value)
|
|
164
|
+
params.set('inline', '1')
|
|
165
|
+
const key = params.toString()
|
|
166
|
+
if (!iconResultCache.has(key)) {
|
|
167
|
+
fetch(`${getIconsBaseUrl()}/api/icons/search?${key}`)
|
|
168
|
+
.then((r) => r.ok ? r.json() : null)
|
|
169
|
+
.then((data) => { if (data?.results) iconResultCache.set(key, data.results) })
|
|
170
|
+
.catch(() => {})
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
}, [visible])
|
|
174
|
+
|
|
175
|
+
const handleIconClick = useCallback((icon) => {
|
|
176
|
+
if (typeof window === 'undefined') return
|
|
177
|
+
const place = (svgContent) => {
|
|
178
|
+
if (window.prepareIconPlacement) {
|
|
179
|
+
window.prepareIconPlacement(svgContent)
|
|
180
|
+
} else {
|
|
181
|
+
window.iconToPlace = svgContent
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (icon.svg) {
|
|
185
|
+
place(icon.svg)
|
|
186
|
+
} else {
|
|
187
|
+
fetch(`${getIconsBaseUrl()}/api/icons/serve?name=${encodeURIComponent(icon.filename)}`)
|
|
188
|
+
.then((r) => r.text())
|
|
189
|
+
.then(place)
|
|
190
|
+
.catch(() => {})
|
|
191
|
+
}
|
|
192
|
+
}, [])
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<div
|
|
196
|
+
className={`absolute top-[60px] right-2 bottom-[56px] w-[300px] bg-surface border border-border-light rounded-2xl z-[999] font-[lixFont] flex flex-col transition-transform duration-200 ${
|
|
197
|
+
visible ? 'translate-x-0' : 'translate-x-full'
|
|
198
|
+
}`}
|
|
199
|
+
>
|
|
200
|
+
{/* Header */}
|
|
201
|
+
<div className="px-3.5 pt-3.5 pb-2 shrink-0">
|
|
202
|
+
<div className="flex items-center justify-between mb-2.5">
|
|
203
|
+
<h3 className="text-text-primary text-sm font-medium">Icons</h3>
|
|
204
|
+
<button
|
|
205
|
+
onClick={() => setActiveTool(TOOLS.SELECT)}
|
|
206
|
+
className="w-6 h-6 flex items-center justify-center rounded-md text-text-muted hover:text-text-primary hover:bg-surface-hover transition-colors duration-100"
|
|
207
|
+
title="Close (Esc)"
|
|
208
|
+
>
|
|
209
|
+
<i className="bx bx-x text-lg" />
|
|
210
|
+
</button>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
{/* Search */}
|
|
214
|
+
<div className="flex items-center gap-2 bg-surface-hover border border-border-light rounded-lg px-2.5 py-2">
|
|
215
|
+
<i className="bx bxs-search text-text-muted text-sm" />
|
|
216
|
+
<input
|
|
217
|
+
id="iconSearchInput"
|
|
218
|
+
type="text"
|
|
219
|
+
value={query}
|
|
220
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
221
|
+
placeholder="Search icons..."
|
|
222
|
+
className="flex-1 bg-transparent text-text-primary text-sm outline-none placeholder:text-text-dim"
|
|
223
|
+
spellCheck={false}
|
|
224
|
+
/>
|
|
225
|
+
{query && (
|
|
226
|
+
<button onClick={() => setQuery('')} className="text-text-dim hover:text-text-secondary">
|
|
227
|
+
<i className="bx bxs-x-circle text-sm" />
|
|
228
|
+
</button>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
{/* Categories */}
|
|
234
|
+
<div className="flex flex-wrap gap-1 px-3.5 pb-2.5 shrink-0">
|
|
235
|
+
{CATEGORIES.map((cat) => (
|
|
236
|
+
<button
|
|
237
|
+
key={cat.value || 'all'}
|
|
238
|
+
onClick={() => setCategory(cat.value)}
|
|
239
|
+
className={`flex items-center gap-1 px-2 py-1 rounded-md text-xs whitespace-nowrap transition-colors duration-100 ${
|
|
240
|
+
category === cat.value
|
|
241
|
+
? 'bg-accent-blue/20 text-accent-blue-hover'
|
|
242
|
+
: 'text-text-muted hover:bg-surface-hover hover:text-text-primary'
|
|
243
|
+
}`}
|
|
244
|
+
>
|
|
245
|
+
<i className={`bx ${cat.icon} text-xs`} />
|
|
246
|
+
{cat.label}
|
|
247
|
+
</button>
|
|
248
|
+
))}
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
{/* Divider */}
|
|
252
|
+
<div className="h-px bg-surface-hover mx-3.5 shrink-0" />
|
|
253
|
+
|
|
254
|
+
{/* Icon grid — scrollable */}
|
|
255
|
+
<div className="flex-1 overflow-y-auto no-scrollbar px-3 py-2.5" id="iconsContainer">
|
|
256
|
+
{loading ? (
|
|
257
|
+
<div className="flex items-center justify-center py-12 text-text-muted text-sm">
|
|
258
|
+
<i className="bx bxs-hourglass bx-spin text-lg mr-2" />
|
|
259
|
+
Loading...
|
|
260
|
+
</div>
|
|
261
|
+
) : icons.length === 0 ? (
|
|
262
|
+
<div className="flex items-center justify-center py-12 text-text-muted text-sm">
|
|
263
|
+
No icons found
|
|
264
|
+
</div>
|
|
265
|
+
) : (
|
|
266
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(6, minmax(0, 1fr))', gap: '2px' }}>
|
|
267
|
+
{icons.map((icon, i) => (
|
|
268
|
+
<IconCell
|
|
269
|
+
key={icon.filename || i}
|
|
270
|
+
icon={icon}
|
|
271
|
+
onClick={() => handleIconClick(icon)}
|
|
272
|
+
/>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
)
|
|
279
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import useSketchStore from '../../store/useSketchStore'
|
|
4
|
+
import useUIStore from '../../store/useUIStore'
|
|
5
|
+
import ShapeSidebar, { Divider, LayerControls } from './ShapeSidebar'
|
|
6
|
+
import { useCallback } from 'react'
|
|
7
|
+
import { useTranslation } from '../../hooks/useTranslation'
|
|
8
|
+
|
|
9
|
+
export default function ImageSidebar() {
|
|
10
|
+
const { t } = useTranslation()
|
|
11
|
+
const selectedShapeSidebar = useSketchStore((s) => s.selectedShapeSidebar)
|
|
12
|
+
const toggleImageGenerateModal = useUIStore((s) => s.toggleImageGenerateModal)
|
|
13
|
+
|
|
14
|
+
const handleEditWithAI = useCallback(() => {
|
|
15
|
+
const shape = window.currentShape
|
|
16
|
+
if (!shape || shape.shapeName !== 'image') return
|
|
17
|
+
|
|
18
|
+
// Get the image URL from the element
|
|
19
|
+
const el = shape.element
|
|
20
|
+
const href = el?.getAttribute('href') || el?.getAttributeNS('http://www.w3.org/1999/xlink', 'href') || ''
|
|
21
|
+
const w = parseFloat(el?.getAttribute('width')) || 512
|
|
22
|
+
const h = parseFloat(el?.getAttribute('height')) || 512
|
|
23
|
+
|
|
24
|
+
// Store reference for the modal to pick up
|
|
25
|
+
window.__editImageRef = { imageUrl: href, width: w, height: h, shape }
|
|
26
|
+
toggleImageGenerateModal()
|
|
27
|
+
}, [toggleImageGenerateModal])
|
|
28
|
+
|
|
29
|
+
const handleReplace = useCallback(() => {
|
|
30
|
+
if (window.openImageFilePicker) {
|
|
31
|
+
// Store current shape so the new image replaces it
|
|
32
|
+
window.__replaceImageShape = window.currentShape
|
|
33
|
+
window.openImageFilePicker()
|
|
34
|
+
}
|
|
35
|
+
}, [])
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<ShapeSidebar visible={selectedShapeSidebar === 'image'}>
|
|
39
|
+
{/* Edit with AI */}
|
|
40
|
+
<button
|
|
41
|
+
onClick={handleEditWithAI}
|
|
42
|
+
title="Edit with AI"
|
|
43
|
+
className="h-9 flex items-center gap-1.5 px-3 rounded-lg text-text-muted hover:text-purple-400 hover:bg-purple-500/10 transition-all duration-100"
|
|
44
|
+
>
|
|
45
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
|
46
|
+
<path d="M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8L12 2z" />
|
|
47
|
+
</svg>
|
|
48
|
+
<span className="text-xs">AI Edit</span>
|
|
49
|
+
</button>
|
|
50
|
+
|
|
51
|
+
<Divider />
|
|
52
|
+
|
|
53
|
+
{/* Replace image */}
|
|
54
|
+
<button
|
|
55
|
+
onClick={handleReplace}
|
|
56
|
+
title="Replace image"
|
|
57
|
+
className="h-9 flex items-center gap-1.5 px-3 rounded-lg text-text-muted hover:text-white hover:bg-white/[0.06] transition-all duration-100"
|
|
58
|
+
>
|
|
59
|
+
<i className="bx bx-upload text-base" />
|
|
60
|
+
<span className="text-xs">Replace</span>
|
|
61
|
+
</button>
|
|
62
|
+
|
|
63
|
+
<Divider />
|
|
64
|
+
|
|
65
|
+
{/* Layer controls */}
|
|
66
|
+
<LayerControls />
|
|
67
|
+
</ShapeSidebar>
|
|
68
|
+
)
|
|
69
|
+
}
|