@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,177 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
4
|
+
import useUIStore from '../../store/useUIStore'
|
|
5
|
+
|
|
6
|
+
export default function FindBar() {
|
|
7
|
+
const open = useUIStore((s) => s.findBarOpen)
|
|
8
|
+
const closeFindBar = useUIStore((s) => s.closeFindBar)
|
|
9
|
+
const [query, setQuery] = useState('')
|
|
10
|
+
const [results, setResults] = useState([])
|
|
11
|
+
const [activeIndex, setActiveIndex] = useState(-1)
|
|
12
|
+
const inputRef = useRef(null)
|
|
13
|
+
const highlightRef = useRef(null)
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (open) {
|
|
17
|
+
setQuery('')
|
|
18
|
+
setResults([])
|
|
19
|
+
setActiveIndex(-1)
|
|
20
|
+
setTimeout(() => inputRef.current?.focus(), 50)
|
|
21
|
+
} else {
|
|
22
|
+
// Clean up highlight when closing
|
|
23
|
+
removeHighlight()
|
|
24
|
+
}
|
|
25
|
+
}, [open])
|
|
26
|
+
|
|
27
|
+
const removeHighlight = useCallback(() => {
|
|
28
|
+
if (highlightRef.current && highlightRef.current.parentNode) {
|
|
29
|
+
highlightRef.current.parentNode.removeChild(highlightRef.current)
|
|
30
|
+
highlightRef.current = null
|
|
31
|
+
}
|
|
32
|
+
}, [])
|
|
33
|
+
|
|
34
|
+
const highlightShape = useCallback((result) => {
|
|
35
|
+
removeHighlight()
|
|
36
|
+
if (!result || !window.svg) return
|
|
37
|
+
|
|
38
|
+
const shape = result.shape
|
|
39
|
+
const x = shape.x || 0
|
|
40
|
+
const y = shape.y || 0
|
|
41
|
+
const w = shape.width || 100
|
|
42
|
+
const h = shape.height || 30
|
|
43
|
+
|
|
44
|
+
// Create highlight rect
|
|
45
|
+
const ns = 'http://www.w3.org/2000/svg'
|
|
46
|
+
const rect = document.createElementNS(ns, 'rect')
|
|
47
|
+
rect.setAttribute('x', x - 4)
|
|
48
|
+
rect.setAttribute('y', y - 4)
|
|
49
|
+
rect.setAttribute('width', w + 8)
|
|
50
|
+
rect.setAttribute('height', h + 8)
|
|
51
|
+
rect.setAttribute('fill', 'rgba(91, 87, 209, 0.15)')
|
|
52
|
+
rect.setAttribute('stroke', '#5B57D1')
|
|
53
|
+
rect.setAttribute('stroke-width', '2')
|
|
54
|
+
rect.setAttribute('stroke-dasharray', '4,2')
|
|
55
|
+
rect.setAttribute('rx', '4')
|
|
56
|
+
rect.setAttribute('pointer-events', 'none')
|
|
57
|
+
rect.classList.add('find-highlight')
|
|
58
|
+
window.svg.appendChild(rect)
|
|
59
|
+
highlightRef.current = rect
|
|
60
|
+
|
|
61
|
+
// Pan to the shape
|
|
62
|
+
const svgEl = window.svg
|
|
63
|
+
const vb = svgEl.viewBox.baseVal
|
|
64
|
+
const centerX = x + w / 2
|
|
65
|
+
const centerY = y + h / 2
|
|
66
|
+
const newX = centerX - vb.width / 2
|
|
67
|
+
const newY = centerY - vb.height / 2
|
|
68
|
+
svgEl.setAttribute('viewBox', `${newX} ${newY} ${vb.width} ${vb.height}`)
|
|
69
|
+
if (window.currentViewBox) {
|
|
70
|
+
window.currentViewBox.x = newX
|
|
71
|
+
window.currentViewBox.y = newY
|
|
72
|
+
}
|
|
73
|
+
}, [removeHighlight])
|
|
74
|
+
|
|
75
|
+
const doSearch = useCallback((q) => {
|
|
76
|
+
const finder = window.__sceneSerializer?.findText
|
|
77
|
+
if (!finder || !q.trim()) {
|
|
78
|
+
setResults([])
|
|
79
|
+
setActiveIndex(-1)
|
|
80
|
+
removeHighlight()
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
const found = finder(q)
|
|
84
|
+
setResults(found)
|
|
85
|
+
if (found.length > 0) {
|
|
86
|
+
setActiveIndex(0)
|
|
87
|
+
highlightShape(found[0])
|
|
88
|
+
} else {
|
|
89
|
+
setActiveIndex(-1)
|
|
90
|
+
removeHighlight()
|
|
91
|
+
}
|
|
92
|
+
}, [highlightShape, removeHighlight])
|
|
93
|
+
|
|
94
|
+
const goToResult = useCallback((idx) => {
|
|
95
|
+
if (idx < 0 || idx >= results.length) return
|
|
96
|
+
setActiveIndex(idx)
|
|
97
|
+
highlightShape(results[idx])
|
|
98
|
+
}, [results, highlightShape])
|
|
99
|
+
|
|
100
|
+
const handleKeyDown = (e) => {
|
|
101
|
+
if (e.key === 'Escape') {
|
|
102
|
+
closeFindBar()
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
if (e.key === 'Enter') {
|
|
106
|
+
e.preventDefault()
|
|
107
|
+
if (results.length === 0) {
|
|
108
|
+
doSearch(query)
|
|
109
|
+
} else {
|
|
110
|
+
// Go to next result
|
|
111
|
+
const next = (activeIndex + 1) % results.length
|
|
112
|
+
goToResult(next)
|
|
113
|
+
}
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const handleChange = (e) => {
|
|
119
|
+
const q = e.target.value
|
|
120
|
+
setQuery(q)
|
|
121
|
+
doSearch(q)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!open) return null
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div className="fixed top-14 right-4 z-[1000] font-[lixFont]">
|
|
128
|
+
<div className="flex items-center gap-2 bg-surface-card/95 backdrop-blur-lg border border-border-light rounded-xl px-3 py-2 shadow-xl min-w-[300px]">
|
|
129
|
+
<i className="bx bx-search text-text-muted text-sm" />
|
|
130
|
+
<input
|
|
131
|
+
ref={inputRef}
|
|
132
|
+
type="text"
|
|
133
|
+
value={query}
|
|
134
|
+
onChange={handleChange}
|
|
135
|
+
onKeyDown={handleKeyDown}
|
|
136
|
+
placeholder="Find text on canvas..."
|
|
137
|
+
className="flex-1 bg-transparent text-text-primary text-xs outline-none placeholder:text-text-dim font-[lixFont]"
|
|
138
|
+
/>
|
|
139
|
+
|
|
140
|
+
{results.length > 0 && (
|
|
141
|
+
<span className="text-text-dim text-xs whitespace-nowrap">
|
|
142
|
+
{activeIndex + 1}/{results.length}
|
|
143
|
+
</span>
|
|
144
|
+
)}
|
|
145
|
+
{query && results.length === 0 && (
|
|
146
|
+
<span className="text-red-400/70 text-xs whitespace-nowrap">
|
|
147
|
+
No results
|
|
148
|
+
</span>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
{results.length > 1 && (
|
|
152
|
+
<div className="flex items-center gap-0.5">
|
|
153
|
+
<button
|
|
154
|
+
onClick={() => goToResult((activeIndex - 1 + results.length) % results.length)}
|
|
155
|
+
className="w-5 h-5 flex items-center justify-center rounded text-text-muted hover:text-white hover:bg-white/10 transition-colors"
|
|
156
|
+
>
|
|
157
|
+
<i className="bx bx-chevron-up text-sm" />
|
|
158
|
+
</button>
|
|
159
|
+
<button
|
|
160
|
+
onClick={() => goToResult((activeIndex + 1) % results.length)}
|
|
161
|
+
className="w-5 h-5 flex items-center justify-center rounded text-text-muted hover:text-white hover:bg-white/10 transition-colors"
|
|
162
|
+
>
|
|
163
|
+
<i className="bx bx-chevron-down text-sm" />
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
<button
|
|
169
|
+
onClick={closeFindBar}
|
|
170
|
+
className="w-5 h-5 flex items-center justify-center rounded text-text-muted hover:text-white hover:bg-white/10 transition-colors"
|
|
171
|
+
>
|
|
172
|
+
<i className="bx bx-x text-sm" />
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
4
|
+
import useUIStore from '../../store/useUIStore'
|
|
5
|
+
import useSketchStore from '../../store/useSketchStore'
|
|
6
|
+
|
|
7
|
+
export default function ImageSourcePicker() {
|
|
8
|
+
const ref = useRef(null)
|
|
9
|
+
const toggleImageGenerateModal = useUIStore((s) => s.toggleImageGenerateModal)
|
|
10
|
+
const activeTool = useSketchStore((s) => s.activeTool)
|
|
11
|
+
const setActiveTool = useSketchStore((s) => s.setActiveTool)
|
|
12
|
+
|
|
13
|
+
// Derive visibility directly from Zustand — no intermediate state
|
|
14
|
+
const visible = activeTool === 'image'
|
|
15
|
+
|
|
16
|
+
const handleGenerateAI = useCallback(() => {
|
|
17
|
+
setActiveTool('select')
|
|
18
|
+
toggleImageGenerateModal()
|
|
19
|
+
}, [setActiveTool, toggleImageGenerateModal])
|
|
20
|
+
|
|
21
|
+
const handleUpload = useCallback(() => {
|
|
22
|
+
setActiveTool('select')
|
|
23
|
+
if (window.openImageFilePicker) {
|
|
24
|
+
window.openImageFilePicker()
|
|
25
|
+
}
|
|
26
|
+
}, [setActiveTool])
|
|
27
|
+
|
|
28
|
+
// Keep the global bridge for engine calls
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
window.__showImageSourcePicker = () => {
|
|
31
|
+
// The picker now shows automatically via Zustand activeTool,
|
|
32
|
+
// but keep this bridge so the engine call doesn't error
|
|
33
|
+
}
|
|
34
|
+
return () => {
|
|
35
|
+
delete window.__showImageSourcePicker
|
|
36
|
+
}
|
|
37
|
+
}, [])
|
|
38
|
+
|
|
39
|
+
// Close on outside click & keybinds
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!visible) return
|
|
42
|
+
|
|
43
|
+
const handleClick = (e) => {
|
|
44
|
+
if (ref.current && !ref.current.contains(e.target)) {
|
|
45
|
+
setActiveTool('select')
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const handleKeyDown = (e) => {
|
|
50
|
+
if (e.key === 'Escape') {
|
|
51
|
+
setActiveTool('select')
|
|
52
|
+
}
|
|
53
|
+
if (e.key === 'g' || e.key === 'G') {
|
|
54
|
+
e.preventDefault()
|
|
55
|
+
e.stopPropagation()
|
|
56
|
+
handleGenerateAI()
|
|
57
|
+
}
|
|
58
|
+
if (e.key === 'u' || e.key === 'U') {
|
|
59
|
+
e.preventDefault()
|
|
60
|
+
e.stopPropagation()
|
|
61
|
+
handleUpload()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Delay to avoid catching the click that opened it
|
|
66
|
+
const timer = setTimeout(() => {
|
|
67
|
+
document.addEventListener('mousedown', handleClick)
|
|
68
|
+
document.addEventListener('keydown', handleKeyDown, true)
|
|
69
|
+
}, 100)
|
|
70
|
+
return () => {
|
|
71
|
+
clearTimeout(timer)
|
|
72
|
+
document.removeEventListener('mousedown', handleClick)
|
|
73
|
+
document.removeEventListener('keydown', handleKeyDown, true)
|
|
74
|
+
}
|
|
75
|
+
}, [visible, setActiveTool, handleGenerateAI, handleUpload])
|
|
76
|
+
|
|
77
|
+
if (!visible) return null
|
|
78
|
+
|
|
79
|
+
// Compute position on each render when visible
|
|
80
|
+
let posX = 65
|
|
81
|
+
let posY = typeof window !== 'undefined' ? window.innerHeight / 2 - 50 : 300
|
|
82
|
+
if (typeof document !== 'undefined') {
|
|
83
|
+
const imageBtn = document.querySelector('button[title="Image (9)"]')
|
|
84
|
+
if (imageBtn) {
|
|
85
|
+
const rect = imageBtn.getBoundingClientRect()
|
|
86
|
+
posX = rect.right + 10
|
|
87
|
+
posY = rect.top - 10
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div
|
|
93
|
+
ref={ref}
|
|
94
|
+
className="fixed z-[1100] font-[lixFont]"
|
|
95
|
+
style={{ left: posX, top: posY }}
|
|
96
|
+
>
|
|
97
|
+
<div className="bg-surface-card border border-border-light rounded-xl p-1.5 shadow-2xl shadow-black/40 flex flex-col gap-1 min-w-[200px]">
|
|
98
|
+
<button
|
|
99
|
+
onClick={handleGenerateAI}
|
|
100
|
+
className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-text-muted hover:text-accent-blue hover:bg-surface-hover transition-all group"
|
|
101
|
+
>
|
|
102
|
+
<div className="w-8 h-8 rounded-lg bg-accent-blue/10 flex items-center justify-center group-hover:bg-accent-blue/20 transition-all">
|
|
103
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-accent-blue">
|
|
104
|
+
<path d="M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3z" />
|
|
105
|
+
<path d="M18 14l1 3 3 1-3 1-1 3-1-3-3-1 3-1 1-3z" />
|
|
106
|
+
</svg>
|
|
107
|
+
</div>
|
|
108
|
+
<div className="flex-1 text-left">
|
|
109
|
+
<div className="text-sm font-medium">Generate with AI</div>
|
|
110
|
+
<div className="text-[10px] text-text-dim">10 generations · 5 edits free</div>
|
|
111
|
+
</div>
|
|
112
|
+
<kbd className="text-[10px] text-text-dim bg-surface-dark px-1.5 py-0.5 rounded">G</kbd>
|
|
113
|
+
</button>
|
|
114
|
+
|
|
115
|
+
<div className="h-px bg-white/[0.06] mx-2" />
|
|
116
|
+
|
|
117
|
+
<button
|
|
118
|
+
onClick={handleUpload}
|
|
119
|
+
className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-text-muted hover:text-text-primary hover:bg-surface-hover transition-all group"
|
|
120
|
+
>
|
|
121
|
+
<div className="w-8 h-8 rounded-lg bg-surface-hover flex items-center justify-center group-hover:bg-surface-active transition-all">
|
|
122
|
+
<i className="bx bx-upload text-lg" />
|
|
123
|
+
</div>
|
|
124
|
+
<div className="flex-1 text-left">
|
|
125
|
+
<div className="text-sm font-medium">Upload from device</div>
|
|
126
|
+
<div className="text-[10px] text-text-dim">PNG, JPG, SVG, WebP</div>
|
|
127
|
+
</div>
|
|
128
|
+
<kbd className="text-[10px] text-text-dim bg-surface-dark px-1.5 py-0.5 rounded">U</kbd>
|
|
129
|
+
</button>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react'
|
|
4
|
+
import { showToast } from '../../utils/toast'
|
|
5
|
+
|
|
6
|
+
export default function MultiSelectActions() {
|
|
7
|
+
const [count, setCount] = useState(0)
|
|
8
|
+
const [groupState, setGroupState] = useState('none') // 'none' | 'partial' | 'all'
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
// Poll multi-selection state. Cheap — Set size + a single
|
|
12
|
+
// groupId pass over selectedShapes.
|
|
13
|
+
const interval = setInterval(() => {
|
|
14
|
+
const ms = window.multiSelection
|
|
15
|
+
const sel = ms?.selectedShapes
|
|
16
|
+
const n = sel ? sel.size : 0
|
|
17
|
+
setCount((prev) => (prev !== n ? n : prev))
|
|
18
|
+
|
|
19
|
+
if (n < 2) {
|
|
20
|
+
setGroupState('none')
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
let withId = 0
|
|
24
|
+
let groupIds = new Set()
|
|
25
|
+
sel.forEach((s) => {
|
|
26
|
+
if (s.groupId) {
|
|
27
|
+
withId++
|
|
28
|
+
groupIds.add(s.groupId)
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
// "all" = every shape has a groupId AND they all share the same one
|
|
32
|
+
if (withId === n && groupIds.size === 1) setGroupState('all')
|
|
33
|
+
else if (withId > 0) setGroupState('partial')
|
|
34
|
+
else setGroupState('none')
|
|
35
|
+
}, 200)
|
|
36
|
+
return () => clearInterval(interval)
|
|
37
|
+
}, [])
|
|
38
|
+
|
|
39
|
+
if (count < 2) return null
|
|
40
|
+
|
|
41
|
+
const handleFrame = () => {
|
|
42
|
+
if (window.frameSelectedShapes) {
|
|
43
|
+
window.frameSelectedShapes()
|
|
44
|
+
setCount(0)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const handleGroup = () => {
|
|
49
|
+
const ms = window.multiSelection
|
|
50
|
+
const sel = ms?.selectedShapes
|
|
51
|
+
if (!sel || sel.size < 2) return
|
|
52
|
+
const newId = `g-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`
|
|
53
|
+
sel.forEach((s) => { s.groupId = newId })
|
|
54
|
+
if (typeof ms.updateControls === 'function') ms.updateControls()
|
|
55
|
+
showToast(`Grouped ${sel.size} shapes`, { tone: 'success' })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const handleUngroup = () => {
|
|
59
|
+
const ms = window.multiSelection
|
|
60
|
+
const sel = ms?.selectedShapes
|
|
61
|
+
if (!sel || sel.size === 0) return
|
|
62
|
+
const groupIds = new Set()
|
|
63
|
+
sel.forEach((s) => { if (s.groupId) groupIds.add(s.groupId) })
|
|
64
|
+
if (groupIds.size === 0 || !Array.isArray(window.shapes)) return
|
|
65
|
+
let cleared = 0
|
|
66
|
+
for (const s of window.shapes) {
|
|
67
|
+
if (s.groupId && groupIds.has(s.groupId)) { s.groupId = null; cleared++ }
|
|
68
|
+
}
|
|
69
|
+
if (typeof ms.updateControls === 'function') ms.updateControls()
|
|
70
|
+
showToast(`Ungrouped ${cleared} shapes`, { tone: 'success' })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const grouped = groupState === 'all'
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="absolute top-14 left-1/2 -translate-x-1/2 z-[1000] font-[lixFont]">
|
|
77
|
+
<div className="flex items-center gap-2 bg-surface/90 backdrop-blur-lg border border-border-light rounded-xl px-3 py-1.5 shadow-lg">
|
|
78
|
+
<span className="text-text-muted text-xs">
|
|
79
|
+
{count} selected{grouped && <span className="ml-1 text-accent-blue">· grouped</span>}
|
|
80
|
+
</span>
|
|
81
|
+
<div className="w-px h-4 bg-border-light" />
|
|
82
|
+
|
|
83
|
+
{/* Frame is only useful when shapes aren't already grouped — a
|
|
84
|
+
grouped set is conceptually a "frameless frame" already. */}
|
|
85
|
+
{!grouped && (
|
|
86
|
+
<button
|
|
87
|
+
onClick={handleFrame}
|
|
88
|
+
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"
|
|
89
|
+
title="Wrap selection in a frame"
|
|
90
|
+
>
|
|
91
|
+
<i className="bx bx-crop text-sm" />
|
|
92
|
+
Frame it
|
|
93
|
+
</button>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
{/* Group / Ungroup toggle. Group is shown when nothing in the
|
|
97
|
+
selection is grouped yet; Ungroup once they're a single group. */}
|
|
98
|
+
{grouped ? (
|
|
99
|
+
<button
|
|
100
|
+
onClick={handleUngroup}
|
|
101
|
+
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs text-yellow-400 hover:bg-yellow-500/10 transition-all duration-150"
|
|
102
|
+
title="Ungroup (Ctrl+Shift+G)"
|
|
103
|
+
>
|
|
104
|
+
<i className="bx bx-unlink text-sm" />
|
|
105
|
+
Ungroup
|
|
106
|
+
</button>
|
|
107
|
+
) : (
|
|
108
|
+
<button
|
|
109
|
+
onClick={handleGroup}
|
|
110
|
+
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs text-text-secondary hover:bg-surface-hover transition-all duration-150"
|
|
111
|
+
title="Group (Ctrl+G)"
|
|
112
|
+
>
|
|
113
|
+
<i className="bx bx-link text-sm" />
|
|
114
|
+
Group
|
|
115
|
+
</button>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useRef, useState, useEffect } from 'react'
|
|
4
|
+
import useSketchStore, { TOOLS } from '../../store/useSketchStore'
|
|
5
|
+
import useSketchEngine from '../../hooks/useSketchEngine'
|
|
6
|
+
|
|
7
|
+
const GRID_SIZE = 20
|
|
8
|
+
|
|
9
|
+
export default function SVGCanvas() {
|
|
10
|
+
const [svgReady, setSvgReady] = useState(false)
|
|
11
|
+
const svgRef = useRef(null)
|
|
12
|
+
const canvasBackground = useSketchStore((s) => s.canvasBackground)
|
|
13
|
+
const gridEnabled = useSketchStore((s) => s.gridEnabled)
|
|
14
|
+
const getCursor = useSketchStore((s) => s.getCursor)
|
|
15
|
+
const cursor = getCursor()
|
|
16
|
+
|
|
17
|
+
// viewBox is owned imperatively by the lixsketch engine (zoom/pan call
|
|
18
|
+
// svg.setAttribute('viewBox', …) directly). React must NOT re-render
|
|
19
|
+
// the viewBox prop or it'll snap back over engine writes — which is
|
|
20
|
+
// what was causing zoom deformity and the pan-release jump in split
|
|
21
|
+
// mode. The initial value is set in the effect below; React never
|
|
22
|
+
// rewrites it after that.
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const applyImperative = (w, h) => {
|
|
25
|
+
const zoom = window.currentZoom || 1
|
|
26
|
+
const cv = window.currentViewBox || { x: 0, y: 0 }
|
|
27
|
+
const vbW = w / zoom
|
|
28
|
+
const vbH = h / zoom
|
|
29
|
+
const x = cv.x || 0
|
|
30
|
+
const y = cv.y || 0
|
|
31
|
+
window.currentViewBox = { x, y, width: vbW, height: vbH }
|
|
32
|
+
const el = svgRef.current
|
|
33
|
+
if (el) el.setAttribute('viewBox', `${x} ${y} ${vbW} ${vbH}`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const sync = () => {
|
|
37
|
+
const el = svgRef.current
|
|
38
|
+
if (!el) return
|
|
39
|
+
const rect = el.getBoundingClientRect()
|
|
40
|
+
const w = Math.max(1, Math.round(rect.width))
|
|
41
|
+
const h = Math.max(1, Math.round(rect.height))
|
|
42
|
+
applyImperative(w, h)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
sync()
|
|
46
|
+
setSvgReady(true)
|
|
47
|
+
|
|
48
|
+
window.addEventListener('resize', sync)
|
|
49
|
+
|
|
50
|
+
let ro
|
|
51
|
+
if (typeof ResizeObserver !== 'undefined' && svgRef.current) {
|
|
52
|
+
ro = new ResizeObserver(sync)
|
|
53
|
+
ro.observe(svgRef.current)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return () => {
|
|
57
|
+
window.removeEventListener('resize', sync)
|
|
58
|
+
if (ro) ro.disconnect()
|
|
59
|
+
}
|
|
60
|
+
}, [])
|
|
61
|
+
|
|
62
|
+
// Close icon sidebar when clicking on canvas without an icon ready to place
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const svg = svgRef.current
|
|
65
|
+
if (!svg) return
|
|
66
|
+
|
|
67
|
+
const handleCanvasClick = () => {
|
|
68
|
+
const activeTool = useSketchStore.getState().activeTool
|
|
69
|
+
// If icon tool is active but no icon is queued for placement, close the sidebar
|
|
70
|
+
if (activeTool === TOOLS.ICON && !window.isIconToolActive) {
|
|
71
|
+
useSketchStore.getState().setActiveTool(TOOLS.SELECT)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
svg.addEventListener('pointerdown', handleCanvasClick)
|
|
76
|
+
return () => svg.removeEventListener('pointerdown', handleCanvasClick)
|
|
77
|
+
}, [svgReady])
|
|
78
|
+
|
|
79
|
+
// Initialize the imperative sketch engine on this SVG element
|
|
80
|
+
useSketchEngine(svgRef, svgReady)
|
|
81
|
+
|
|
82
|
+
// Expose grid state to engine
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
window.__gridEnabled = gridEnabled
|
|
85
|
+
}, [gridEnabled])
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<svg
|
|
89
|
+
id="freehand-canvas"
|
|
90
|
+
ref={svgRef}
|
|
91
|
+
className="absolute inset-0 w-full h-full"
|
|
92
|
+
style={{
|
|
93
|
+
background: canvasBackground,
|
|
94
|
+
cursor,
|
|
95
|
+
userSelect: 'none',
|
|
96
|
+
WebkitUserSelect: 'none',
|
|
97
|
+
touchAction: 'none',
|
|
98
|
+
}}
|
|
99
|
+
preserveAspectRatio="none"
|
|
100
|
+
suppressHydrationWarning
|
|
101
|
+
>
|
|
102
|
+
{gridEnabled && (
|
|
103
|
+
<defs>
|
|
104
|
+
<pattern
|
|
105
|
+
id="grid-small"
|
|
106
|
+
width={GRID_SIZE}
|
|
107
|
+
height={GRID_SIZE}
|
|
108
|
+
patternUnits="userSpaceOnUse"
|
|
109
|
+
>
|
|
110
|
+
<path
|
|
111
|
+
d={`M ${GRID_SIZE} 0 L 0 0 0 ${GRID_SIZE}`}
|
|
112
|
+
fill="none"
|
|
113
|
+
stroke="rgba(255,255,255,0.06)"
|
|
114
|
+
strokeWidth="0.5"
|
|
115
|
+
/>
|
|
116
|
+
</pattern>
|
|
117
|
+
<pattern
|
|
118
|
+
id="grid-large"
|
|
119
|
+
width={GRID_SIZE * 5}
|
|
120
|
+
height={GRID_SIZE * 5}
|
|
121
|
+
patternUnits="userSpaceOnUse"
|
|
122
|
+
>
|
|
123
|
+
<rect width={GRID_SIZE * 5} height={GRID_SIZE * 5} fill="url(#grid-small)" />
|
|
124
|
+
<path
|
|
125
|
+
d={`M ${GRID_SIZE * 5} 0 L 0 0 0 ${GRID_SIZE * 5}`}
|
|
126
|
+
fill="none"
|
|
127
|
+
stroke="rgba(255,255,255,0.12)"
|
|
128
|
+
strokeWidth="0.8"
|
|
129
|
+
/>
|
|
130
|
+
</pattern>
|
|
131
|
+
</defs>
|
|
132
|
+
)}
|
|
133
|
+
{gridEnabled && (
|
|
134
|
+
<rect
|
|
135
|
+
x="-100000"
|
|
136
|
+
y="-100000"
|
|
137
|
+
width="200000"
|
|
138
|
+
height="200000"
|
|
139
|
+
fill="url(#grid-large)"
|
|
140
|
+
style={{ pointerEvents: 'none' }}
|
|
141
|
+
/>
|
|
142
|
+
)}
|
|
143
|
+
</svg>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import useUIStore from '../../store/useUIStore'
|
|
4
|
+
|
|
5
|
+
const STATUS_CONFIG = {
|
|
6
|
+
cloud: { color: 'bg-green-400', label: 'Synced to cloud' },
|
|
7
|
+
local: { color: 'bg-yellow-400', label: 'Saved locally' },
|
|
8
|
+
failed: { color: 'bg-red-400', label: 'Sync failed' },
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function SaveStatusIndicator() {
|
|
12
|
+
const saveStatus = useUIStore((s) => s.saveStatus)
|
|
13
|
+
|
|
14
|
+
if (saveStatus === 'idle') return null
|
|
15
|
+
|
|
16
|
+
const config = STATUS_CONFIG[saveStatus]
|
|
17
|
+
if (!config) return null
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="fixed bottom-3 left-1/2 -translate-x-1/2 z-[999] flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-card/80 backdrop-blur-md border border-border-light font-[lixFont] pointer-events-none select-none">
|
|
21
|
+
<span
|
|
22
|
+
className={`w-2 h-2 rounded-full shrink-0 ${config.color}`}
|
|
23
|
+
/>
|
|
24
|
+
<span className="text-text-dim text-[10px] tracking-wide">
|
|
25
|
+
{config.label}
|
|
26
|
+
</span>
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|