@elixpo/lixsketch 5.5.20 → 5.5.21
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/{CopyPaste-KJZ34MIP.js → CopyPaste-FJN7K5LS.js} +39 -11
- package/dist/react/CopyPaste-FJN7K5LS.js.map +7 -0
- package/dist/react/{EventDispatcher-XDRQS7BD.js → EventDispatcher-O7HCQQNM.js} +13 -13
- package/dist/react/Frame-RLSMJJSK.js +11 -0
- package/dist/react/{GraphEngine-KQXHNTZQ.js → GraphEngine-MM6ALA2N.js} +105 -148
- package/dist/react/{GraphEngine-KQXHNTZQ.js.map → GraphEngine-MM6ALA2N.js.map} +2 -2
- package/dist/react/IconShape-7OPXAKLK.js +10 -0
- package/dist/react/ImageShape-LRNJQCF3.js +10 -0
- package/dist/react/Line-WWQFBNX5.js +10 -0
- package/dist/react/{SceneSerializer-VY3HY4PF.js → SceneSerializer-GSJR7ZHC.js} +32 -8
- package/dist/react/SceneSerializer-GSJR7ZHC.js.map +7 -0
- package/dist/react/{Selection-U2AR6NQS.js → Selection-ZL3G6IAO.js} +4 -4
- package/dist/react/{SketchEngine-ZFYUTKET.js → SketchEngine-BFLHJPCG.js} +21 -21
- package/dist/react/{UndoRedo-BZ6W4QTV.js → UndoRedo-NL7KLR5B.js} +2 -2
- package/dist/react/{arrowTool-LKFSZKXC.js → arrowTool-CNVGM6RX.js} +3 -3
- package/dist/react/{chunk-RFHMFWVR.js → chunk-2FMFOPGW.js} +5 -6
- package/dist/react/{chunk-RFHMFWVR.js.map → chunk-2FMFOPGW.js.map} +2 -2
- package/dist/react/{chunk-SBSM3GQD.js → chunk-2THKDSWW.js} +4 -4
- package/dist/react/{chunk-JOPQ27YH.js → chunk-4BLMNGTR.js} +3 -3
- package/dist/react/{chunk-C62SXFBL.js → chunk-4TAS67SL.js} +23 -22
- package/dist/react/chunk-4TAS67SL.js.map +7 -0
- package/dist/react/{chunk-7VBI7S7T.js → chunk-AH4PWNEB.js} +10 -11
- package/dist/react/chunk-AH4PWNEB.js.map +7 -0
- package/dist/react/{chunk-R5QXEVZ5.js → chunk-GC3KTQDA.js} +74 -15
- package/dist/react/chunk-GC3KTQDA.js.map +7 -0
- package/dist/react/{chunk-2V7G2PLX.js → chunk-GPIYXREE.js} +3 -3
- package/dist/react/{chunk-73VLPCBC.js → chunk-HFQZYCHJ.js} +3 -3
- package/dist/react/{chunk-C72OHQLE.js → chunk-ILGAFR56.js} +3 -3
- package/dist/react/{chunk-75G7XSZU.js → chunk-KVAQPCUH.js} +17 -3
- package/dist/react/chunk-KVAQPCUH.js.map +7 -0
- package/dist/react/{chunk-WEWMB35W.js → chunk-OFGJJKPH.js} +3 -3
- package/dist/react/{chunk-6G3ITAS5.js → chunk-SZD7DKYJ.js} +18 -9
- package/dist/react/chunk-SZD7DKYJ.js.map +7 -0
- package/dist/react/{chunk-MYYBFZD3.js → chunk-U3TB4KRS.js} +45 -6
- package/dist/react/chunk-U3TB4KRS.js.map +7 -0
- package/dist/react/{chunk-UQBT7F4K.js → chunk-V2QTYSBK.js} +2 -2
- package/dist/react/{chunk-2I3GH65Z.js → chunk-XFQECTTF.js} +44 -19
- package/dist/react/chunk-XFQECTTF.js.map +7 -0
- package/dist/react/{chunk-O64N5JPO.js → chunk-YF3TK7SC.js} +2 -2
- package/dist/react/{circleTool-RDOIMKKV.js → circleTool-KICNRQCI.js} +4 -4
- package/dist/react/{codeTool-VBDB2ES6.js → codeTool-NCQ73R6M.js} +4 -4
- package/dist/react/{frameTool-Z5ZDCX6H.js → frameTool-Y6KB67DO.js} +4 -4
- package/dist/react/{freehandTool-AAQCTMVM.js → freehandTool-MMULHCQ7.js} +4 -4
- package/dist/react/{iconTool-5WNDHI5T.js → iconTool-LSW2DYGB.js} +4 -4
- package/dist/react/{imageTool-ECPMT3BL.js → imageTool-2DSR32RX.js} +4 -4
- package/dist/react/index.js +141 -66
- package/dist/react/index.js.map +3 -3
- package/dist/react/{lineTool-XLOOZE3E.js → lineTool-N5I7IGT7.js} +4 -4
- package/dist/react/{rectangleTool-AEYDDJCA.js → rectangleTool-YUT7ZK2E.js} +4 -4
- package/dist/react/{textTool-5IJKTWVQ.js → textTool-IQOYPELP.js} +5 -5
- package/package.json +1 -1
- package/src/core/CopyPaste.js +34 -4
- package/src/core/GraphEngine.js +129 -185
- package/src/core/SceneSerializer.js +26 -0
- package/src/core/Selection.js +12 -11
- package/src/core/UndoRedo.js +47 -3
- package/src/react/components/canvas/MultiSelectActions.jsx +4 -1
- package/src/react/components/sidebars/IconSidebar.jsx +14 -23
- package/src/react/components/sidebars/ImageSidebar.jsx +47 -24
- package/src/react/store/useUIStore.js +71 -14
- package/src/shapes/Frame.js +19 -2
- package/src/shapes/IconShape.js +23 -25
- package/src/shapes/ImageShape.js +6 -2
- package/src/tools/frameTool.js +19 -9
- package/src/tools/iconTool.js +60 -25
- package/src/tools/imageTool.js +77 -13
- package/dist/react/CopyPaste-KJZ34MIP.js.map +0 -7
- package/dist/react/Frame-QLBAVLGW.js +0 -11
- package/dist/react/IconShape-32EYI2MA.js +0 -10
- package/dist/react/ImageShape-MYP2DVVH.js +0 -10
- package/dist/react/Line-FCCF2BOB.js +0 -10
- package/dist/react/SceneSerializer-VY3HY4PF.js.map +0 -7
- package/dist/react/chunk-2I3GH65Z.js.map +0 -7
- package/dist/react/chunk-6G3ITAS5.js.map +0 -7
- package/dist/react/chunk-75G7XSZU.js.map +0 -7
- package/dist/react/chunk-7VBI7S7T.js.map +0 -7
- package/dist/react/chunk-C62SXFBL.js.map +0 -7
- package/dist/react/chunk-MYYBFZD3.js.map +0 -7
- package/dist/react/chunk-R5QXEVZ5.js.map +0 -7
- /package/dist/react/{EventDispatcher-XDRQS7BD.js.map → EventDispatcher-O7HCQQNM.js.map} +0 -0
- /package/dist/react/{Frame-QLBAVLGW.js.map → Frame-RLSMJJSK.js.map} +0 -0
- /package/dist/react/{IconShape-32EYI2MA.js.map → IconShape-7OPXAKLK.js.map} +0 -0
- /package/dist/react/{ImageShape-MYP2DVVH.js.map → ImageShape-LRNJQCF3.js.map} +0 -0
- /package/dist/react/{Line-FCCF2BOB.js.map → Line-WWQFBNX5.js.map} +0 -0
- /package/dist/react/{Selection-U2AR6NQS.js.map → Selection-ZL3G6IAO.js.map} +0 -0
- /package/dist/react/{SketchEngine-ZFYUTKET.js.map → SketchEngine-BFLHJPCG.js.map} +0 -0
- /package/dist/react/{UndoRedo-BZ6W4QTV.js.map → UndoRedo-NL7KLR5B.js.map} +0 -0
- /package/dist/react/{arrowTool-LKFSZKXC.js.map → arrowTool-CNVGM6RX.js.map} +0 -0
- /package/dist/react/{chunk-SBSM3GQD.js.map → chunk-2THKDSWW.js.map} +0 -0
- /package/dist/react/{chunk-JOPQ27YH.js.map → chunk-4BLMNGTR.js.map} +0 -0
- /package/dist/react/{chunk-2V7G2PLX.js.map → chunk-GPIYXREE.js.map} +0 -0
- /package/dist/react/{chunk-73VLPCBC.js.map → chunk-HFQZYCHJ.js.map} +0 -0
- /package/dist/react/{chunk-C72OHQLE.js.map → chunk-ILGAFR56.js.map} +0 -0
- /package/dist/react/{chunk-WEWMB35W.js.map → chunk-OFGJJKPH.js.map} +0 -0
- /package/dist/react/{chunk-UQBT7F4K.js.map → chunk-V2QTYSBK.js.map} +0 -0
- /package/dist/react/{chunk-O64N5JPO.js.map → chunk-YF3TK7SC.js.map} +0 -0
- /package/dist/react/{circleTool-RDOIMKKV.js.map → circleTool-KICNRQCI.js.map} +0 -0
- /package/dist/react/{codeTool-VBDB2ES6.js.map → codeTool-NCQ73R6M.js.map} +0 -0
- /package/dist/react/{frameTool-Z5ZDCX6H.js.map → frameTool-Y6KB67DO.js.map} +0 -0
- /package/dist/react/{freehandTool-AAQCTMVM.js.map → freehandTool-MMULHCQ7.js.map} +0 -0
- /package/dist/react/{iconTool-5WNDHI5T.js.map → iconTool-LSW2DYGB.js.map} +0 -0
- /package/dist/react/{imageTool-ECPMT3BL.js.map → imageTool-2DSR32RX.js.map} +0 -0
- /package/dist/react/{lineTool-XLOOZE3E.js.map → lineTool-N5I7IGT7.js.map} +0 -0
- /package/dist/react/{rectangleTool-AEYDDJCA.js.map → rectangleTool-YUT7ZK2E.js.map} +0 -0
- /package/dist/react/{textTool-5IJKTWVQ.js.map → textTool-IQOYPELP.js.map} +0 -0
|
@@ -6,6 +6,7 @@ import { showToast } from '../../utils/toast'
|
|
|
6
6
|
export default function MultiSelectActions() {
|
|
7
7
|
const [count, setCount] = useState(0)
|
|
8
8
|
const [groupState, setGroupState] = useState('none') // 'none' | 'partial' | 'all'
|
|
9
|
+
const [frameEligible, setFrameEligible] = useState(false)
|
|
9
10
|
|
|
10
11
|
useEffect(() => {
|
|
11
12
|
// Poll multi-selection state. Cheap — Set size + a single
|
|
@@ -18,6 +19,7 @@ export default function MultiSelectActions() {
|
|
|
18
19
|
|
|
19
20
|
if (n < 2) {
|
|
20
21
|
setGroupState('none')
|
|
22
|
+
setFrameEligible(false)
|
|
21
23
|
return
|
|
22
24
|
}
|
|
23
25
|
let withId = 0
|
|
@@ -28,6 +30,7 @@ export default function MultiSelectActions() {
|
|
|
28
30
|
groupIds.add(s.groupId)
|
|
29
31
|
}
|
|
30
32
|
})
|
|
33
|
+
setFrameEligible(Array.from(sel).every((s) => s.shapeName !== 'frame' && !s.parentFrame))
|
|
31
34
|
// "all" = every shape has a groupId AND they all share the same one
|
|
32
35
|
if (withId === n && groupIds.size === 1) setGroupState('all')
|
|
33
36
|
else if (withId > 0) setGroupState('partial')
|
|
@@ -82,7 +85,7 @@ export default function MultiSelectActions() {
|
|
|
82
85
|
|
|
83
86
|
{/* Frame is only useful when shapes aren't already grouped — a
|
|
84
87
|
grouped set is conceptually a "frameless frame" already. */}
|
|
85
|
-
{!grouped && (
|
|
88
|
+
{!grouped && frameEligible && (
|
|
86
89
|
<button
|
|
87
90
|
onClick={handleFrame}
|
|
88
91
|
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs text-accent-blue hover:bg-accent-blue/10 transition-all duration-150"
|
|
@@ -90,6 +90,14 @@ export default function IconSidebar() {
|
|
|
90
90
|
const [icons, setIcons] = useState([])
|
|
91
91
|
const [loading, setLoading] = useState(false)
|
|
92
92
|
const debounceRef = useRef(null)
|
|
93
|
+
const searchInputRef = useRef(null)
|
|
94
|
+
const requestSequenceRef = useRef(0)
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!visible) return
|
|
98
|
+
const frame = requestAnimationFrame(() => searchInputRef.current?.focus())
|
|
99
|
+
return () => cancelAnimationFrame(frame)
|
|
100
|
+
}, [visible])
|
|
93
101
|
|
|
94
102
|
// Close on Escape
|
|
95
103
|
useEffect(() => {
|
|
@@ -115,6 +123,7 @@ export default function IconSidebar() {
|
|
|
115
123
|
}, [visible, setActiveTool])
|
|
116
124
|
|
|
117
125
|
const fetchIcons = useCallback(async (searchQuery, cat) => {
|
|
126
|
+
const requestSequence = ++requestSequenceRef.current
|
|
118
127
|
const params = new URLSearchParams()
|
|
119
128
|
if (searchQuery) params.set('q', searchQuery)
|
|
120
129
|
if (cat) params.set('category', cat)
|
|
@@ -124,6 +133,7 @@ export default function IconSidebar() {
|
|
|
124
133
|
// Return cached results instantly if available
|
|
125
134
|
if (iconResultCache.has(cacheKey)) {
|
|
126
135
|
setIcons(iconResultCache.get(cacheKey))
|
|
136
|
+
setLoading(false)
|
|
127
137
|
return
|
|
128
138
|
}
|
|
129
139
|
|
|
@@ -134,12 +144,12 @@ export default function IconSidebar() {
|
|
|
134
144
|
const data = await res.json()
|
|
135
145
|
const results = data.results || []
|
|
136
146
|
iconResultCache.set(cacheKey, results)
|
|
137
|
-
setIcons(results)
|
|
147
|
+
if (requestSequence === requestSequenceRef.current) setIcons(results)
|
|
138
148
|
}
|
|
139
149
|
} catch (err) {
|
|
140
150
|
console.error('Icon fetch failed:', err)
|
|
141
151
|
}
|
|
142
|
-
setLoading(false)
|
|
152
|
+
if (requestSequence === requestSequenceRef.current) setLoading(false)
|
|
143
153
|
}, [])
|
|
144
154
|
|
|
145
155
|
// Fetch icons when visibility, query, or category changes (debounced for query typing)
|
|
@@ -148,30 +158,10 @@ export default function IconSidebar() {
|
|
|
148
158
|
if (debounceRef.current) clearTimeout(debounceRef.current)
|
|
149
159
|
debounceRef.current = setTimeout(() => {
|
|
150
160
|
fetchIcons(query, category)
|
|
151
|
-
}, query ?
|
|
161
|
+
}, query ? 80 : 0)
|
|
152
162
|
return () => clearTimeout(debounceRef.current)
|
|
153
163
|
}, [query, visible, category, fetchIcons])
|
|
154
164
|
|
|
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
165
|
const handleIconClick = useCallback((icon) => {
|
|
176
166
|
if (typeof window === 'undefined') return
|
|
177
167
|
const place = (svgContent) => {
|
|
@@ -214,6 +204,7 @@ export default function IconSidebar() {
|
|
|
214
204
|
<div className="flex items-center gap-2 bg-surface-hover border border-border-light rounded-lg px-2.5 py-2">
|
|
215
205
|
<i className="bx bxs-search text-text-muted text-sm" />
|
|
216
206
|
<input
|
|
207
|
+
ref={searchInputRef}
|
|
217
208
|
id="iconSearchInput"
|
|
218
209
|
type="text"
|
|
219
210
|
value={query}
|
|
@@ -10,6 +10,15 @@ export default function ImageSidebar() {
|
|
|
10
10
|
const { t } = useTranslation()
|
|
11
11
|
const selectedShapeSidebar = useSketchStore((s) => s.selectedShapeSidebar)
|
|
12
12
|
const toggleImageGenerateModal = useUIStore((s) => s.toggleImageGenerateModal)
|
|
13
|
+
const toggleAIModal = useUIStore((s) => s.toggleAIModal)
|
|
14
|
+
const isGraph = typeof window !== 'undefined' && window.currentShape?._frameType === 'graph'
|
|
15
|
+
|
|
16
|
+
const handleEditGraph = useCallback(() => {
|
|
17
|
+
const shape = window.currentShape
|
|
18
|
+
if (!shape || shape._frameType !== 'graph') return
|
|
19
|
+
window.__aiEditTargetFrame = shape
|
|
20
|
+
toggleAIModal()
|
|
21
|
+
}, [toggleAIModal])
|
|
13
22
|
|
|
14
23
|
const handleEditWithAI = useCallback(() => {
|
|
15
24
|
const shape = window.currentShape
|
|
@@ -36,31 +45,45 @@ export default function ImageSidebar() {
|
|
|
36
45
|
|
|
37
46
|
return (
|
|
38
47
|
<ShapeSidebar visible={selectedShapeSidebar === 'image'}>
|
|
39
|
-
{
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
{isGraph && (
|
|
49
|
+
<>
|
|
50
|
+
<button
|
|
51
|
+
onClick={handleEditGraph}
|
|
52
|
+
title="Edit graph"
|
|
53
|
+
className="h-9 flex cursor-pointer items-center gap-1.5 px-3 rounded-lg text-text-muted hover:text-accent hover:bg-accent/10 transition-all duration-100"
|
|
54
|
+
>
|
|
55
|
+
<i className="bx bx-line-chart text-base" />
|
|
56
|
+
<span className="text-xs">Edit graph</span>
|
|
57
|
+
</button>
|
|
58
|
+
<Divider />
|
|
59
|
+
</>
|
|
60
|
+
)}
|
|
50
61
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
{/* Edit with AI */}
|
|
63
|
+
{!isGraph && (
|
|
64
|
+
<>
|
|
65
|
+
<button
|
|
66
|
+
onClick={handleEditWithAI}
|
|
67
|
+
title="Edit with AI"
|
|
68
|
+
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"
|
|
69
|
+
>
|
|
70
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
|
71
|
+
<path d="M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8L12 2z" />
|
|
72
|
+
</svg>
|
|
73
|
+
<span className="text-xs">AI Edit</span>
|
|
74
|
+
</button>
|
|
75
|
+
<Divider />
|
|
76
|
+
<button
|
|
77
|
+
onClick={handleReplace}
|
|
78
|
+
title="Replace image"
|
|
79
|
+
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"
|
|
80
|
+
>
|
|
81
|
+
<i className="bx bx-upload text-base" />
|
|
82
|
+
<span className="text-xs">Replace</span>
|
|
83
|
+
</button>
|
|
84
|
+
<Divider />
|
|
85
|
+
</>
|
|
86
|
+
)}
|
|
64
87
|
|
|
65
88
|
{/* Layer controls */}
|
|
66
89
|
<LayerControls />
|
|
@@ -29,25 +29,82 @@ function invertShapeColors(prevResolved, nextResolved) {
|
|
|
29
29
|
|
|
30
30
|
for (const shape of shapes) {
|
|
31
31
|
let changed = false
|
|
32
|
-
if (shape.
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
if (shape.shapeName === 'icon' || shape.type === 'icon') {
|
|
33
|
+
const isDark = nextResolved === 'dark'
|
|
34
|
+
const targetDarkColor = '#1a1a2e'
|
|
35
|
+
const bgDarkColor = '#15111f'
|
|
36
|
+
|
|
37
|
+
if (shape.group) {
|
|
38
|
+
shape.group.querySelectorAll('*').forEach((element) => {
|
|
39
|
+
if (element.nodeType === 1) {
|
|
40
|
+
const fill = element.getAttribute('fill')
|
|
41
|
+
const stroke = element.getAttribute('stroke')
|
|
42
|
+
|
|
43
|
+
if (fill && fill !== 'none' && fill !== 'transparent') {
|
|
44
|
+
const normFill = fill.toLowerCase().trim()
|
|
45
|
+
if (isDark) {
|
|
46
|
+
if (normFill === targetDarkColor || normFill === '#000000' || normFill === '#000') {
|
|
47
|
+
element.setAttribute('fill', '#ffffff')
|
|
48
|
+
changed = true
|
|
49
|
+
} else if (normFill === '#ffffff' || normFill === '#fff') {
|
|
50
|
+
element.setAttribute('fill', bgDarkColor)
|
|
51
|
+
changed = true
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
if (normFill === '#ffffff' || normFill === '#fff') {
|
|
55
|
+
element.setAttribute('fill', targetDarkColor)
|
|
56
|
+
changed = true
|
|
57
|
+
} else if (normFill === bgDarkColor) {
|
|
58
|
+
element.setAttribute('fill', '#ffffff')
|
|
59
|
+
changed = true
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (stroke && stroke !== 'none' && stroke !== 'transparent') {
|
|
65
|
+
const normStroke = stroke.toLowerCase().trim()
|
|
66
|
+
if (isDark) {
|
|
67
|
+
if (normStroke === targetDarkColor || normStroke === '#000000' || normStroke === '#000') {
|
|
68
|
+
element.setAttribute('stroke', '#ffffff')
|
|
69
|
+
changed = true
|
|
70
|
+
} else if (normStroke === '#ffffff' || normStroke === '#fff') {
|
|
71
|
+
element.setAttribute('stroke', bgDarkColor)
|
|
72
|
+
changed = true
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
if (normStroke === '#ffffff' || normStroke === '#fff') {
|
|
76
|
+
element.setAttribute('stroke', targetDarkColor)
|
|
77
|
+
changed = true
|
|
78
|
+
} else if (normStroke === bgDarkColor) {
|
|
79
|
+
element.setAttribute('stroke', '#ffffff')
|
|
80
|
+
changed = true
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
if (shape.options) {
|
|
89
|
+
if (normalize(shape.options.stroke) === from) {
|
|
90
|
+
shape.options.stroke = to
|
|
91
|
+
changed = true
|
|
92
|
+
}
|
|
93
|
+
if (normalize(shape.options.fill) === from) {
|
|
94
|
+
shape.options.fill = to
|
|
95
|
+
changed = true
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Text shapes store color directly
|
|
99
|
+
if (shape.color !== undefined && normalize(shape.color) === from) {
|
|
100
|
+
shape.color = to
|
|
35
101
|
changed = true
|
|
36
102
|
}
|
|
37
|
-
if (normalize(shape.
|
|
38
|
-
shape.
|
|
103
|
+
if (shape.strokeColor !== undefined && normalize(shape.strokeColor) === from) {
|
|
104
|
+
shape.strokeColor = to
|
|
39
105
|
changed = true
|
|
40
106
|
}
|
|
41
107
|
}
|
|
42
|
-
// Text shapes store color directly
|
|
43
|
-
if (shape.color !== undefined && normalize(shape.color) === from) {
|
|
44
|
-
shape.color = to
|
|
45
|
-
changed = true
|
|
46
|
-
}
|
|
47
|
-
if (shape.strokeColor !== undefined && normalize(shape.strokeColor) === from) {
|
|
48
|
-
shape.strokeColor = to
|
|
49
|
-
changed = true
|
|
50
|
-
}
|
|
51
108
|
if (changed && typeof shape.draw === 'function') {
|
|
52
109
|
shape.draw()
|
|
53
110
|
}
|
package/src/shapes/Frame.js
CHANGED
|
@@ -480,6 +480,7 @@ move(dx, dy) {
|
|
|
480
480
|
labelText.textContent = this.frameName || "Frame";
|
|
481
481
|
labelText.style.cursor = "pointer";
|
|
482
482
|
labelText.style.userSelect = "none";
|
|
483
|
+
labelText.style.pointerEvents = "all";
|
|
483
484
|
|
|
484
485
|
if (this.rotation !== 0) {
|
|
485
486
|
const centerX = this.x + this.width / 2;
|
|
@@ -487,6 +488,11 @@ move(dx, dy) {
|
|
|
487
488
|
labelText.setAttribute("transform", `rotate(${this.rotation}, ${centerX}, ${centerY})`);
|
|
488
489
|
}
|
|
489
490
|
|
|
491
|
+
// Do not let the first click select/redraw the frame: replacing the label
|
|
492
|
+
// between clicks prevents the browser from ever emitting `dblclick`.
|
|
493
|
+
labelText.addEventListener('pointerdown', (e) => e.stopPropagation());
|
|
494
|
+
labelText.addEventListener('click', (e) => e.stopPropagation());
|
|
495
|
+
|
|
490
496
|
// Add double-click event for renaming
|
|
491
497
|
labelText.addEventListener('dblclick', (e) => {
|
|
492
498
|
e.stopPropagation();
|
|
@@ -498,6 +504,10 @@ move(dx, dy) {
|
|
|
498
504
|
}
|
|
499
505
|
|
|
500
506
|
startLabelEdit(labelElement) {
|
|
507
|
+
if (this._labelEditor) {
|
|
508
|
+
this._labelEditor.querySelector('input')?.focus();
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
501
511
|
// Create a foreignObject to hold an HTML input
|
|
502
512
|
const foreignObject = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
|
|
503
513
|
foreignObject.setAttribute("x", this.x + 5);
|
|
@@ -535,6 +545,7 @@ startLabelEdit(labelElement) {
|
|
|
535
545
|
|
|
536
546
|
foreignObject.appendChild(input);
|
|
537
547
|
this.group.appendChild(foreignObject);
|
|
548
|
+
this._labelEditor = foreignObject;
|
|
538
549
|
|
|
539
550
|
// Hide the original label
|
|
540
551
|
labelElement.style.display = "none";
|
|
@@ -545,7 +556,10 @@ startLabelEdit(labelElement) {
|
|
|
545
556
|
input.select();
|
|
546
557
|
}, 10);
|
|
547
558
|
|
|
559
|
+
let finished = false;
|
|
548
560
|
const finishEdit = () => {
|
|
561
|
+
if (finished) return;
|
|
562
|
+
finished = true;
|
|
549
563
|
const newName = input.value.trim() || "Frame";
|
|
550
564
|
|
|
551
565
|
// Track name change in undo system if it actually changed
|
|
@@ -573,7 +587,8 @@ startLabelEdit(labelElement) {
|
|
|
573
587
|
this.frameName = newName;
|
|
574
588
|
|
|
575
589
|
// Remove the input
|
|
576
|
-
|
|
590
|
+
foreignObject.remove();
|
|
591
|
+
this._labelEditor = null;
|
|
577
592
|
|
|
578
593
|
// Show and update the label
|
|
579
594
|
labelElement.style.display = "block";
|
|
@@ -590,8 +605,10 @@ startLabelEdit(labelElement) {
|
|
|
590
605
|
finishEdit();
|
|
591
606
|
} else if (e.key === 'Escape') {
|
|
592
607
|
e.preventDefault();
|
|
608
|
+
finished = true;
|
|
593
609
|
// Cancel edit - restore original name
|
|
594
|
-
|
|
610
|
+
foreignObject.remove();
|
|
611
|
+
this._labelEditor = null;
|
|
595
612
|
labelElement.style.display = "block";
|
|
596
613
|
}
|
|
597
614
|
});
|
package/src/shapes/IconShape.js
CHANGED
|
@@ -79,6 +79,25 @@ class IconShape {
|
|
|
79
79
|
this.element.setAttribute('data-shape-height', value);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
_transformMetrics() {
|
|
83
|
+
const vbWidth = parseFloat(this.element.getAttribute('data-viewbox-width')) || 24;
|
|
84
|
+
const vbHeight = parseFloat(this.element.getAttribute('data-viewbox-height')) || 24;
|
|
85
|
+
const scale = Math.min(this.width / vbWidth, this.height / vbHeight);
|
|
86
|
+
return {
|
|
87
|
+
scale,
|
|
88
|
+
localCenterX: this.width / 2 / scale,
|
|
89
|
+
localCenterY: this.height / 2 / scale,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_applyTransform() {
|
|
94
|
+
const { scale, localCenterX, localCenterY } = this._transformMetrics();
|
|
95
|
+
this.element.setAttribute(
|
|
96
|
+
'transform',
|
|
97
|
+
`translate(${this.x}, ${this.y}) scale(${scale}) rotate(${this.rotation}, ${localCenterX}, ${localCenterY})`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
82
101
|
get rotation() {
|
|
83
102
|
const dataRotation = this.element.getAttribute('data-shape-rotation');
|
|
84
103
|
if (dataRotation) {
|
|
@@ -97,27 +116,14 @@ class IconShape {
|
|
|
97
116
|
|
|
98
117
|
set rotation(value) {
|
|
99
118
|
this.element.setAttribute('data-shape-rotation', value);
|
|
100
|
-
|
|
101
|
-
const vbWidth = parseFloat(this.element.getAttribute('data-viewbox-width')) || 24;
|
|
102
|
-
const vbHeight = parseFloat(this.element.getAttribute('data-viewbox-height')) || 24;
|
|
103
|
-
const scale = this.width / Math.max(vbWidth, vbHeight);
|
|
104
|
-
const localCenterX = this.width / 2 / scale;
|
|
105
|
-
const localCenterY = this.height / 2 / scale;
|
|
106
|
-
|
|
107
|
-
this.element.setAttribute('transform', `translate(${this.x}, ${this.y}) scale(${scale}) rotate(${value}, ${localCenterX}, ${localCenterY})`);
|
|
119
|
+
this._applyTransform();
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
move(dx, dy) {
|
|
111
123
|
this.x += dx;
|
|
112
124
|
this.y += dy;
|
|
113
125
|
|
|
114
|
-
|
|
115
|
-
const vbHeight = parseFloat(this.element.getAttribute('data-viewbox-height')) || 24;
|
|
116
|
-
const scale = this.width / Math.max(vbWidth, vbHeight);
|
|
117
|
-
const localCenterX = this.width / 2 / scale;
|
|
118
|
-
const localCenterY = this.height / 2 / scale;
|
|
119
|
-
|
|
120
|
-
this.element.setAttribute('transform', `translate(${this.x}, ${this.y}) scale(${scale}) rotate(${this.rotation}, ${localCenterX}, ${localCenterY})`);
|
|
126
|
+
this._applyTransform();
|
|
121
127
|
|
|
122
128
|
this.element.setAttribute('x', this.x);
|
|
123
129
|
this.element.setAttribute('y', this.y);
|
|
@@ -173,11 +179,7 @@ class IconShape {
|
|
|
173
179
|
}
|
|
174
180
|
|
|
175
181
|
draw() {
|
|
176
|
-
|
|
177
|
-
const localCenterX = this.width / 2 / scale;
|
|
178
|
-
const localCenterY = this.height / 2 / scale;
|
|
179
|
-
|
|
180
|
-
this.element.setAttribute('transform', `translate(${this.x}, ${this.y}) scale(${scale}) rotate(${this.rotation}, ${localCenterX}, ${localCenterY})`);
|
|
182
|
+
this._applyTransform();
|
|
181
183
|
|
|
182
184
|
this.element.setAttribute('data-shape-x', this.x);
|
|
183
185
|
this.element.setAttribute('data-shape-y', this.y);
|
|
@@ -246,11 +248,7 @@ class IconShape {
|
|
|
246
248
|
pos.parentFrame.addShapeToFrame(this);
|
|
247
249
|
}
|
|
248
250
|
|
|
249
|
-
|
|
250
|
-
const localCenterX = pos.width / 2 / scale;
|
|
251
|
-
const localCenterY = pos.height / 2 / scale;
|
|
252
|
-
|
|
253
|
-
this.element.setAttribute('transform', `translate(${pos.x}, ${pos.y}) scale(${scale}) rotate(${pos.rotation}, ${localCenterX}, ${localCenterY})`);
|
|
251
|
+
this._applyTransform();
|
|
254
252
|
|
|
255
253
|
this.element.setAttribute('data-shape-x', pos.x);
|
|
256
254
|
this.element.setAttribute('data-shape-y', pos.y);
|
package/src/shapes/ImageShape.js
CHANGED
|
@@ -262,8 +262,12 @@ class ImageShape {
|
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
selectShape() {
|
|
265
|
-
//
|
|
266
|
-
|
|
265
|
+
// Image interaction state lives in imageTool. Use its bridge so
|
|
266
|
+
// programmatically-created images (graphs, restored scenes, paste)
|
|
267
|
+
// receive the same drag/resize controls as pointer-selected images.
|
|
268
|
+
if (typeof window.__selectImageElement === 'function') {
|
|
269
|
+
window.__selectImageElement(this.element, { force: true });
|
|
270
|
+
}
|
|
267
271
|
}
|
|
268
272
|
}
|
|
269
273
|
|
package/src/tools/frameTool.js
CHANGED
|
@@ -143,9 +143,12 @@ const handleMouseDown = (e) => {
|
|
|
143
143
|
currentFrame = null;
|
|
144
144
|
currentShape = null;
|
|
145
145
|
} else {
|
|
146
|
-
pushCreateAction(currentFrame);
|
|
147
146
|
// Check for shapes that should be contained in the new frame
|
|
148
147
|
currentFrame.updateContainedShapes(true); // Apply clipping immediately for new frames
|
|
148
|
+
pushCreateAction(currentFrame, {
|
|
149
|
+
frameCreation: true,
|
|
150
|
+
containedShapes: [...currentFrame.containedShapes]
|
|
151
|
+
});
|
|
149
152
|
|
|
150
153
|
// Auto-select the new frame and switch to selection tool
|
|
151
154
|
const placedFrame = currentFrame;
|
|
@@ -177,17 +180,24 @@ const handleMouseDown = (e) => {
|
|
|
177
180
|
}
|
|
178
181
|
}
|
|
179
182
|
|
|
180
|
-
// Only check frame containment for shapes that aren't already in frames
|
|
183
|
+
// Only check frame containment for shapes that aren't already in frames.
|
|
184
|
+
// Record this fallback attachment too; individual shape tools normally do
|
|
185
|
+
// it themselves, but restored/imported shapes can reach this path.
|
|
181
186
|
if (!isDragging) {
|
|
182
187
|
shapes.forEach(shape => {
|
|
183
188
|
if (shape.shapeName !== 'frame' && !shape.parentFrame) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
+
let targetFrame = null;
|
|
190
|
+
for (let i = shapes.length - 1; i >= 0; i--) {
|
|
191
|
+
const frame = shapes[i];
|
|
192
|
+
if (frame.shapeName === 'frame' && frame.isShapeInFrame(shape)) {
|
|
193
|
+
targetFrame = frame;
|
|
194
|
+
break;
|
|
189
195
|
}
|
|
190
|
-
}
|
|
196
|
+
}
|
|
197
|
+
if (targetFrame) {
|
|
198
|
+
targetFrame.addShapeToFrame(shape);
|
|
199
|
+
pushFrameAttachmentAction(targetFrame, shape, 'attach', null);
|
|
200
|
+
}
|
|
191
201
|
}
|
|
192
202
|
});
|
|
193
203
|
}
|
|
@@ -278,4 +288,4 @@ export {
|
|
|
278
288
|
handleMouseDown as handleMouseDownFrame,
|
|
279
289
|
handleMouseMove as handleMouseMoveFrame,
|
|
280
290
|
handleMouseUp as handleMouseUpFrame
|
|
281
|
-
};
|
|
291
|
+
};
|
package/src/tools/iconTool.js
CHANGED
|
@@ -258,6 +258,18 @@ const handleMouseDownIcon = async (e) => {
|
|
|
258
258
|
return;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
// Helper to get inherited attribute from the SVG sub-tree
|
|
262
|
+
function getInheritedAttr(element, attrName) {
|
|
263
|
+
let curr = element;
|
|
264
|
+
while (curr && curr.nodeType === 1) {
|
|
265
|
+
if (curr.getAttribute(attrName)) {
|
|
266
|
+
return curr.getAttribute(attrName);
|
|
267
|
+
}
|
|
268
|
+
curr = curr.parentElement;
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
261
273
|
if (!isDraggingIcon || !iconToPlace || !isIconToolActive) {
|
|
262
274
|
return;
|
|
263
275
|
}
|
|
@@ -322,29 +334,50 @@ const handleMouseDownIcon = async (e) => {
|
|
|
322
334
|
backgroundRect.setAttribute('style', 'pointer-events: all; cursor: pointer;');
|
|
323
335
|
finalIconGroup.appendChild(backgroundRect);
|
|
324
336
|
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
337
|
+
const isDark = document.body && document.body.classList.contains('theme-dark');
|
|
338
|
+
const targetDarkColor = '#1a1a2e';
|
|
339
|
+
const bgDarkColor = '#15111f';
|
|
328
340
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
341
|
+
const applyThemeStyle = (element) => {
|
|
342
|
+
if (element.nodeType === 1) {
|
|
343
|
+
const fill = getInheritedAttr(element, 'fill');
|
|
344
|
+
const stroke = getInheritedAttr(element, 'stroke');
|
|
345
|
+
|
|
346
|
+
const normFill = fill ? fill.toLowerCase().trim() : null;
|
|
347
|
+
const normStroke = stroke ? stroke.toLowerCase().trim() : null;
|
|
348
|
+
|
|
349
|
+
// Handle fill
|
|
350
|
+
if (normFill === 'none' || normFill === 'transparent') {
|
|
351
|
+
element.setAttribute('fill', 'none');
|
|
352
|
+
} else if (!normFill || ['#000', '#000000', 'black', 'currentcolor', '#1a1a2e'].includes(normFill)) {
|
|
353
|
+
element.setAttribute('fill', isDark ? '#ffffff' : targetDarkColor);
|
|
354
|
+
} else if (['#fff', '#ffffff', 'white', '#15111f'].includes(normFill)) {
|
|
355
|
+
element.setAttribute('fill', isDark ? bgDarkColor : '#ffffff');
|
|
344
356
|
}
|
|
345
|
-
};
|
|
346
|
-
applyWhiteStyle(clonedChild);
|
|
347
357
|
|
|
358
|
+
// Handle stroke
|
|
359
|
+
if (normStroke === 'none' || normStroke === 'transparent') {
|
|
360
|
+
element.setAttribute('stroke', 'none');
|
|
361
|
+
} else if (normStroke && ['#000', '#000000', 'black', 'currentcolor', '#1a1a2e'].includes(normStroke)) {
|
|
362
|
+
element.setAttribute('stroke', isDark ? '#ffffff' : targetDarkColor);
|
|
363
|
+
} else if (normStroke && ['#fff', '#ffffff', 'white', '#15111f'].includes(normStroke)) {
|
|
364
|
+
element.setAttribute('stroke', isDark ? bgDarkColor : '#ffffff');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
for (let j = 0; j < element.children.length; j++) {
|
|
368
|
+
applyThemeStyle(element.children[j]);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// Apply theme styles to original elements while they are still in context
|
|
374
|
+
for (let i = 0; i < originalSvgElement.children.length; i++) {
|
|
375
|
+
applyThemeStyle(originalSvgElement.children[i]);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const allChildren = originalSvgElement.children;
|
|
379
|
+
for (let i = 0; i < allChildren.length; i++) {
|
|
380
|
+
const clonedChild = allChildren[i].cloneNode(true);
|
|
348
381
|
finalIconGroup.appendChild(clonedChild);
|
|
349
382
|
}
|
|
350
383
|
|
|
@@ -1250,8 +1283,8 @@ function normalizeSVGSize(svgContent, fillColor = '#fff', strokeColor = null) {
|
|
|
1250
1283
|
|
|
1251
1284
|
// Only apply to shape elements, not container elements like 'g'
|
|
1252
1285
|
if (['path', 'circle', 'rect', 'polygon', 'ellipse', 'polyline', 'line'].includes(tagName)) {
|
|
1253
|
-
const currentFill = element
|
|
1254
|
-
const currentStroke = element
|
|
1286
|
+
const currentFill = getInheritedAttr(element, 'fill');
|
|
1287
|
+
const currentStroke = getInheritedAttr(element, 'stroke');
|
|
1255
1288
|
|
|
1256
1289
|
// Check if parent <g> has explicit colors - if so, don't override
|
|
1257
1290
|
let parentG = element.parentElement;
|
|
@@ -1266,19 +1299,21 @@ function normalizeSVGSize(svgContent, fillColor = '#fff', strokeColor = null) {
|
|
|
1266
1299
|
}
|
|
1267
1300
|
|
|
1268
1301
|
// Only apply white fill if:
|
|
1269
|
-
// 1. Element doesn't have explicit 'none' fill
|
|
1302
|
+
// 1. Element doesn't have explicit/inherited 'none'/'transparent' fill
|
|
1270
1303
|
// 2. Element doesn't already have a specific color (other than black/default)
|
|
1271
1304
|
// 3. Parent <g> doesn't have explicit colors
|
|
1272
|
-
if (!hasParentColor && currentFill !== 'none') {
|
|
1305
|
+
if (!hasParentColor && currentFill !== 'none' && currentFill !== 'transparent') {
|
|
1273
1306
|
// Only override if it's black, default, or unset
|
|
1274
1307
|
if (!currentFill || currentFill === '#000' || currentFill === '#000000' || currentFill === 'black' || currentFill === 'currentColor') {
|
|
1275
1308
|
element.setAttribute('fill', fillColor);
|
|
1276
1309
|
}
|
|
1310
|
+
} else if (currentFill === 'none' || currentFill === 'transparent') {
|
|
1311
|
+
element.setAttribute('fill', 'none');
|
|
1277
1312
|
}
|
|
1278
1313
|
|
|
1279
1314
|
// Handle stroke similarly
|
|
1280
1315
|
if (strokeColor && !hasParentColor) {
|
|
1281
|
-
if (currentStroke && currentStroke !== 'none') {
|
|
1316
|
+
if (currentStroke && currentStroke !== 'none' && currentStroke !== 'transparent') {
|
|
1282
1317
|
if (!currentStroke || currentStroke === '#000' || currentStroke === '#000000' || currentStroke === 'black' || currentStroke === 'currentColor') {
|
|
1283
1318
|
element.setAttribute('stroke', strokeColor);
|
|
1284
1319
|
}
|