@elixpo/lixsketch 5.6.9 → 5.6.11
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-R5M2CIDO.js → AIRenderer-JNEZYDYQ.js} +4 -4
- package/dist/react/Arrow-QMR77LGQ.js +9 -0
- package/dist/react/Circle-5WL5HZA5.js +11 -0
- package/dist/react/{CopyPaste-PPDS3AD6.js → CopyPaste-V7AY56H3.js} +74 -30
- package/dist/react/CopyPaste-V7AY56H3.js.map +7 -0
- package/dist/react/{EventDispatcher-UV7LFSAS.js → EventDispatcher-E4OJCMY6.js} +154 -57
- package/dist/react/{EventDispatcher-UV7LFSAS.js.map → EventDispatcher-E4OJCMY6.js.map} +3 -3
- package/dist/react/Frame-OZCDVPMI.js +11 -0
- package/dist/react/FreehandStroke-QSSYWBFK.js +9 -0
- package/dist/react/IconShape-HXUV4VRT.js +10 -0
- package/dist/react/ImageShape-JGLT2UGG.js +10 -0
- package/dist/react/Line-YONPLUYT.js +11 -0
- package/dist/react/{MermaidFlowchartRenderer-EL3Q2MNW.js → MermaidFlowchartRenderer-FSH43RUP.js} +26 -7
- package/dist/react/{MermaidFlowchartRenderer-EL3Q2MNW.js.map → MermaidFlowchartRenderer-FSH43RUP.js.map} +2 -2
- package/dist/react/{MermaidSequenceRenderer-K4KHHEVC.js → MermaidSequenceRenderer-FRPNOF2V.js} +11 -9
- package/dist/react/{MermaidSequenceRenderer-K4KHHEVC.js.map → MermaidSequenceRenderer-FRPNOF2V.js.map} +2 -2
- package/dist/react/{MermaidStructuredRenderer-6LN3WISP.js → MermaidStructuredRenderer-BZGQLRIH.js} +13 -6
- package/dist/react/{MermaidStructuredRenderer-6LN3WISP.js.map → MermaidStructuredRenderer-BZGQLRIH.js.map} +2 -2
- package/dist/react/Rectangle-7KMA3SC6.js +11 -0
- package/dist/react/{SceneSerializer-H6KMQCO2.js → SceneSerializer-7O72LEZP.js} +13 -12
- package/dist/react/{SceneSerializer-H6KMQCO2.js.map → SceneSerializer-7O72LEZP.js.map} +1 -1
- package/dist/react/{Selection-CFWRGVKH.js → Selection-S4FYSZIF.js} +5 -5
- package/dist/react/{SketchEngine-UFLZJQXS.js → SketchEngine-VTWURP2K.js} +26 -26
- package/dist/react/{UndoRedo-RXO7YHA2.js → UndoRedo-YLQJ6DXW.js} +8 -2
- package/dist/react/{ZoomPan-RR7KUYZT.js → ZoomPan-AEKWKUCH.js} +2 -2
- package/dist/react/{arrowTool-DO327FKU.js → arrowTool-C4RDWQEX.js} +3 -3
- package/dist/react/{chunk-X5ENBMGL.js → chunk-2U47AU6Y.js} +3 -3
- package/dist/react/{chunk-X5ENBMGL.js.map → chunk-2U47AU6Y.js.map} +1 -1
- package/dist/react/{chunk-RK3NVAER.js → chunk-75OPJOWN.js} +7 -7
- package/dist/react/chunk-75OPJOWN.js.map +7 -0
- package/dist/react/{chunk-DODPOWNX.js → chunk-CWE5IFG7.js} +5 -5
- package/dist/react/{chunk-DODPOWNX.js.map → chunk-CWE5IFG7.js.map} +1 -1
- package/dist/react/{chunk-O7KXLFZN.js → chunk-IS2MKWGH.js} +8 -7
- package/dist/react/chunk-IS2MKWGH.js.map +7 -0
- package/dist/react/{chunk-ZNZAICWY.js → chunk-JEHOEQ6Z.js} +13 -5
- package/dist/react/chunk-JEHOEQ6Z.js.map +7 -0
- package/dist/react/{chunk-BOZEDUAF.js → chunk-JYBJXTLM.js} +40 -15
- package/dist/react/chunk-JYBJXTLM.js.map +7 -0
- package/dist/react/{chunk-6LWUR7XB.js → chunk-LXEHYCKH.js} +4 -4
- package/dist/react/{chunk-6LWUR7XB.js.map → chunk-LXEHYCKH.js.map} +1 -1
- package/dist/react/chunk-M7OYYJC6.js +17 -0
- package/dist/react/chunk-M7OYYJC6.js.map +7 -0
- package/dist/react/chunk-MS4TIQ6Z.js +131 -0
- package/dist/react/chunk-MS4TIQ6Z.js.map +7 -0
- package/dist/react/{chunk-2ALS5JGS.js → chunk-OCJN32IL.js} +2 -2
- package/dist/react/{chunk-KZBKYHZO.js → chunk-P7VIYR6R.js} +8 -5
- package/dist/react/chunk-P7VIYR6R.js.map +7 -0
- package/dist/react/{chunk-KC26KJW4.js → chunk-PX2635H5.js} +32 -10
- package/dist/react/chunk-PX2635H5.js.map +7 -0
- package/dist/react/{chunk-37PF7FG2.js → chunk-SDG4U7TP.js} +7 -6
- package/dist/react/chunk-SDG4U7TP.js.map +7 -0
- package/dist/react/{chunk-CGMUXYPJ.js → chunk-STJERZ25.js} +8 -4
- package/dist/react/chunk-STJERZ25.js.map +7 -0
- package/dist/react/{chunk-VZPH7OAM.js → chunk-TCWCC4Z4.js} +7 -3
- package/dist/react/chunk-TCWCC4Z4.js.map +7 -0
- package/dist/react/{chunk-VZX55VQL.js → chunk-TFCBCWL5.js} +90 -33
- package/dist/react/chunk-TFCBCWL5.js.map +7 -0
- package/dist/react/{chunk-GQLXUZHQ.js → chunk-TWHJ6PJK.js} +5 -3
- package/dist/react/chunk-TWHJ6PJK.js.map +7 -0
- package/dist/react/{chunk-NH3NVSWT.js → chunk-UCOK4V5Z.js} +4 -3
- package/dist/react/{chunk-NH3NVSWT.js.map → chunk-UCOK4V5Z.js.map} +2 -2
- package/dist/react/{chunk-W7XUY4GR.js → chunk-VJDNKJBX.js} +56 -4
- package/dist/react/chunk-VJDNKJBX.js.map +7 -0
- package/dist/react/{chunk-BT2EIJD4.js → chunk-WCA3FGWE.js} +6 -3
- package/dist/react/chunk-WCA3FGWE.js.map +7 -0
- package/dist/react/{chunk-I7TVGJFF.js → chunk-Y3FJMYPX.js} +4 -4
- package/dist/react/{chunk-I7TVGJFF.js.map → chunk-Y3FJMYPX.js.map} +1 -1
- package/dist/react/{chunk-7DD2JRBC.js → chunk-YCGLGY4C.js} +2 -2
- package/dist/react/{circleTool-NK4RVDAR.js → circleTool-QQV2QEVF.js} +4 -4
- package/dist/react/{codeTool-3WF4HEOW.js → codeTool-PTCPSZ4L.js} +5 -5
- package/dist/react/{frameTool-VCNV2NG2.js → frameTool-7GQPLU4Y.js} +4 -4
- package/dist/react/{freehandTool-TBAOD46K.js → freehandTool-3RJXTVAK.js} +4 -4
- package/dist/react/{iconTool-65JCAKYF.js → iconTool-DW5YJX7P.js} +5 -5
- package/dist/react/{imageTool-UQZUBOK2.js → imageTool-7GZQKWYT.js} +5 -5
- package/dist/react/index.js +68 -63
- package/dist/react/index.js.map +2 -2
- package/dist/react/{lineTool-762KBYOM.js → lineTool-5CY7BHND.js} +4 -4
- package/dist/react/{rectangleTool-IQK3HZK6.js → rectangleTool-DK4QL4QW.js} +4 -4
- package/dist/react/styles.css +4 -0
- package/dist/react/{textTool-ITG2SBJK.js → textTool-GP5QQYF3.js} +6 -6
- package/package.json +1 -1
- package/src/core/CopyPaste.js +66 -18
- package/src/core/EventDispatcher.js +71 -47
- package/src/core/MermaidFlowchartRenderer.js +34 -8
- package/src/core/MermaidSequenceRenderer.js +10 -8
- package/src/core/MermaidStructuredRenderer.js +12 -5
- package/src/core/ScreenSpaceControls.js +102 -13
- package/src/core/Selection.js +26 -7
- package/src/core/SelectionGesture.js +50 -0
- package/src/core/UndoRedo.js +108 -35
- package/src/react/components/sidebars/CircleSidebar.jsx +11 -5
- package/src/react/components/sidebars/IconSidebar.jsx +10 -4
- package/src/react/components/sidebars/RectangleSidebar.jsx +11 -5
- package/src/react/components/sidebars/ShapeSidebar.jsx +29 -37
- package/src/react/components/sidebars/TextSidebar.jsx +10 -4
- package/src/react/store/useUIStore.js +8 -1
- package/src/react/styles.css +13 -0
- package/src/react/utils/canvasBackgrounds.js +11 -2
- package/src/shapes/Arrow.js +4 -3
- package/src/shapes/Circle.js +58 -2
- package/src/shapes/Frame.js +11 -4
- package/src/shapes/FreehandStroke.js +2 -1
- package/src/shapes/Line.js +47 -20
- package/src/shapes/Rectangle.js +3 -1
- package/src/tools/arrowTool.js +1 -1
- package/src/tools/circleTool.js +5 -2
- package/src/tools/codeTool.js +1 -1
- package/src/tools/frameTool.js +3 -2
- package/src/tools/freehandTool.js +1 -1
- package/src/tools/iconTool.js +4 -3
- package/src/tools/imageTool.js +3 -2
- package/src/tools/lineTool.js +5 -1
- package/src/tools/rectangleTool.js +1 -1
- package/src/tools/shapeRecognitionTool.js +1 -1
- package/src/tools/textTool.js +2 -2
- package/src/utils/themeColors.js +13 -0
- package/dist/react/Arrow-EF7ALH2Y.js +0 -8
- package/dist/react/Circle-HMM5PKJ2.js +0 -10
- package/dist/react/CopyPaste-PPDS3AD6.js.map +0 -7
- package/dist/react/Frame-DELCMYV6.js +0 -11
- package/dist/react/FreehandStroke-WXIJKD5I.js +0 -9
- package/dist/react/IconShape-PZHMZSLR.js +0 -10
- package/dist/react/ImageShape-ZMBZARRI.js +0 -10
- package/dist/react/Line-VPVPL6ET.js +0 -10
- package/dist/react/Rectangle-4U4CD2ER.js +0 -10
- package/dist/react/chunk-37PF7FG2.js.map +0 -7
- package/dist/react/chunk-BOZEDUAF.js.map +0 -7
- package/dist/react/chunk-BT2EIJD4.js.map +0 -7
- package/dist/react/chunk-CGMUXYPJ.js.map +0 -7
- package/dist/react/chunk-GQLXUZHQ.js.map +0 -7
- package/dist/react/chunk-KC26KJW4.js.map +0 -7
- package/dist/react/chunk-KZBKYHZO.js.map +0 -7
- package/dist/react/chunk-O7KXLFZN.js.map +0 -7
- package/dist/react/chunk-PAKA6HUX.js +0 -59
- package/dist/react/chunk-PAKA6HUX.js.map +0 -7
- package/dist/react/chunk-RK3NVAER.js.map +0 -7
- package/dist/react/chunk-VZPH7OAM.js.map +0 -7
- package/dist/react/chunk-VZX55VQL.js.map +0 -7
- package/dist/react/chunk-W7XUY4GR.js.map +0 -7
- package/dist/react/chunk-ZNZAICWY.js.map +0 -7
- /package/dist/react/{AIRenderer-R5M2CIDO.js.map → AIRenderer-JNEZYDYQ.js.map} +0 -0
- /package/dist/react/{Arrow-EF7ALH2Y.js.map → Arrow-QMR77LGQ.js.map} +0 -0
- /package/dist/react/{Circle-HMM5PKJ2.js.map → Circle-5WL5HZA5.js.map} +0 -0
- /package/dist/react/{Frame-DELCMYV6.js.map → Frame-OZCDVPMI.js.map} +0 -0
- /package/dist/react/{FreehandStroke-WXIJKD5I.js.map → FreehandStroke-QSSYWBFK.js.map} +0 -0
- /package/dist/react/{IconShape-PZHMZSLR.js.map → IconShape-HXUV4VRT.js.map} +0 -0
- /package/dist/react/{ImageShape-ZMBZARRI.js.map → ImageShape-JGLT2UGG.js.map} +0 -0
- /package/dist/react/{Line-VPVPL6ET.js.map → Line-YONPLUYT.js.map} +0 -0
- /package/dist/react/{Rectangle-4U4CD2ER.js.map → Rectangle-7KMA3SC6.js.map} +0 -0
- /package/dist/react/{Selection-CFWRGVKH.js.map → Selection-S4FYSZIF.js.map} +0 -0
- /package/dist/react/{SketchEngine-UFLZJQXS.js.map → SketchEngine-VTWURP2K.js.map} +0 -0
- /package/dist/react/{UndoRedo-RXO7YHA2.js.map → UndoRedo-YLQJ6DXW.js.map} +0 -0
- /package/dist/react/{ZoomPan-RR7KUYZT.js.map → ZoomPan-AEKWKUCH.js.map} +0 -0
- /package/dist/react/{arrowTool-DO327FKU.js.map → arrowTool-C4RDWQEX.js.map} +0 -0
- /package/dist/react/{chunk-2ALS5JGS.js.map → chunk-OCJN32IL.js.map} +0 -0
- /package/dist/react/{chunk-7DD2JRBC.js.map → chunk-YCGLGY4C.js.map} +0 -0
- /package/dist/react/{circleTool-NK4RVDAR.js.map → circleTool-QQV2QEVF.js.map} +0 -0
- /package/dist/react/{codeTool-3WF4HEOW.js.map → codeTool-PTCPSZ4L.js.map} +0 -0
- /package/dist/react/{frameTool-VCNV2NG2.js.map → frameTool-7GQPLU4Y.js.map} +0 -0
- /package/dist/react/{freehandTool-TBAOD46K.js.map → freehandTool-3RJXTVAK.js.map} +0 -0
- /package/dist/react/{iconTool-65JCAKYF.js.map → iconTool-DW5YJX7P.js.map} +0 -0
- /package/dist/react/{imageTool-UQZUBOK2.js.map → imageTool-7GZQKWYT.js.map} +0 -0
- /package/dist/react/{lineTool-762KBYOM.js.map → lineTool-5CY7BHND.js.map} +0 -0
- /package/dist/react/{rectangleTool-IQK3HZK6.js.map → rectangleTool-DK4QL4QW.js.map} +0 -0
- /package/dist/react/{textTool-ITG2SBJK.js.map → textTool-GP5QQYF3.js.map} +0 -0
|
@@ -95,7 +95,10 @@ export default function IconSidebar() {
|
|
|
95
95
|
|
|
96
96
|
useEffect(() => {
|
|
97
97
|
if (!visible) return
|
|
98
|
-
|
|
98
|
+
// Focusing a transformed, previously off-screen input can make the
|
|
99
|
+
// browser scroll its nearest canvas pane horizontally. Keep focus for
|
|
100
|
+
// keyboard search without allowing it to move the viewport.
|
|
101
|
+
const frame = requestAnimationFrame(() => searchInputRef.current?.focus({ preventScroll: true }))
|
|
99
102
|
return () => cancelAnimationFrame(frame)
|
|
100
103
|
}, [visible])
|
|
101
104
|
|
|
@@ -181,11 +184,14 @@ export default function IconSidebar() {
|
|
|
181
184
|
}
|
|
182
185
|
}, [])
|
|
183
186
|
|
|
187
|
+
// Keep the closed picker out of the DOM instead of translating it beyond
|
|
188
|
+
// the canvas bounds. This guarantees it cannot affect host layout or focus
|
|
189
|
+
// scrolling in embedded/package consumers.
|
|
190
|
+
if (!visible) return null
|
|
191
|
+
|
|
184
192
|
return (
|
|
185
193
|
<div
|
|
186
|
-
className=
|
|
187
|
-
visible ? 'translate-x-0' : 'translate-x-full'
|
|
188
|
-
}`}
|
|
194
|
+
className="absolute top-[60px] right-2 bottom-[112px] w-[300px] bg-surface border border-border-light rounded-2xl z-[999] font-[lixFont] flex flex-col"
|
|
189
195
|
>
|
|
190
196
|
{/* Header */}
|
|
191
197
|
<div className="px-3.5 pt-3.5 pb-2 shrink-0">
|
|
@@ -4,8 +4,9 @@ import useSketchStore, { TOOLS } from '../../store/useSketchStore'
|
|
|
4
4
|
import ShapeSidebar, { ToolbarButton, Divider, LayerControls } from './ShapeSidebar'
|
|
5
5
|
import { useState, useCallback } from 'react'
|
|
6
6
|
import { useTranslation } from '../../hooks/useTranslation'
|
|
7
|
+
import useUIStore from '../../store/useUIStore'
|
|
7
8
|
|
|
8
|
-
const STROKE_COLORS = ['#
|
|
9
|
+
const STROKE_COLORS = ['#000000', '#ffffff', '#FF8383', '#3A994C', '#56A2E8', '#FFD700', '#FF69B4', '#A855F7']
|
|
9
10
|
const BG_COLORS = ['transparent', '#f0f0f0', '#ffcccb', '#90ee90', '#add8e6', '#FFE4B5', '#DDA0DD', '#2d2d2d']
|
|
10
11
|
|
|
11
12
|
const FILLS = [
|
|
@@ -46,7 +47,12 @@ export default function RectangleSidebar() {
|
|
|
46
47
|
const { t } = useTranslation()
|
|
47
48
|
const activeTool = useSketchStore((s) => s.activeTool)
|
|
48
49
|
const selectedShapeSidebar = useSketchStore((s) => s.selectedShapeSidebar)
|
|
49
|
-
const
|
|
50
|
+
const theme = useUIStore((s) => s.theme)
|
|
51
|
+
const resolvedTheme = theme === 'system' && typeof window !== 'undefined'
|
|
52
|
+
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
|
53
|
+
: theme
|
|
54
|
+
const [strokeColor, setStrokeColor] = useState(null)
|
|
55
|
+
const displayedStrokeColor = strokeColor ?? (resolvedTheme === 'light' ? '#000000' : '#ffffff')
|
|
50
56
|
const [bgColor, setBgColor] = useState('transparent')
|
|
51
57
|
const [thickness, setThickness] = useState(2)
|
|
52
58
|
const [lineStyle, setLineStyle] = useState('solid')
|
|
@@ -68,10 +74,10 @@ export default function RectangleSidebar() {
|
|
|
68
74
|
{/* Stroke color */}
|
|
69
75
|
<ToolbarButton
|
|
70
76
|
tooltip={t('sidebar.strokeColor')}
|
|
71
|
-
preview={<span className="w-4 h-4 rounded-md border border-white/20" style={{ backgroundColor:
|
|
77
|
+
preview={<span className="w-4 h-4 rounded-md border border-white/20" style={{ backgroundColor: displayedStrokeColor }} />}
|
|
72
78
|
>
|
|
73
79
|
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.stroke')}</p>
|
|
74
|
-
<ColorGrid colors={STROKE_COLORS} selected={
|
|
80
|
+
<ColorGrid colors={STROKE_COLORS} selected={displayedStrokeColor} onSelect={updateStroke} />
|
|
75
81
|
</ToolbarButton>
|
|
76
82
|
|
|
77
83
|
<Divider />
|
|
@@ -130,7 +136,7 @@ export default function RectangleSidebar() {
|
|
|
130
136
|
}`}
|
|
131
137
|
>
|
|
132
138
|
<svg width="28" height="4" viewBox="0 0 28 4">
|
|
133
|
-
<line x1="0" y1="2" x2="28" y2="2" stroke="
|
|
139
|
+
<line x1="0" y1="2" x2="28" y2="2" stroke="currentColor" strokeWidth="2" strokeDasharray={s.d} strokeLinecap="round" />
|
|
134
140
|
</svg>
|
|
135
141
|
</button>
|
|
136
142
|
))}
|
|
@@ -7,7 +7,7 @@ import useSketchStore from '../../store/useSketchStore'
|
|
|
7
7
|
* A toolbar button that opens a popover panel above it on click.
|
|
8
8
|
* Shows an icon (or custom preview) in the bar, popover shows full options.
|
|
9
9
|
*/
|
|
10
|
-
export function ToolbarButton({ icon, preview, children, tooltip }) {
|
|
10
|
+
export function ToolbarButton({ icon, preview, children, tooltip, label = tooltip }) {
|
|
11
11
|
const [open, setOpen] = useState(false)
|
|
12
12
|
const ref = useRef(null)
|
|
13
13
|
|
|
@@ -24,15 +24,19 @@ export function ToolbarButton({ icon, preview, children, tooltip }) {
|
|
|
24
24
|
return (
|
|
25
25
|
<div ref={ref} className="relative flex items-center">
|
|
26
26
|
<button
|
|
27
|
+
type="button"
|
|
27
28
|
onClick={() => setOpen(!open)}
|
|
28
29
|
title={tooltip}
|
|
29
|
-
|
|
30
|
+
aria-expanded={open}
|
|
31
|
+
aria-haspopup="dialog"
|
|
32
|
+
className={`h-9 flex cursor-pointer items-center gap-1.5 rounded-lg border px-2.5 transition-all duration-100 ${
|
|
30
33
|
open
|
|
31
|
-
? 'bg-accent-blue/20 text-accent-blue'
|
|
32
|
-
: 'text-text-muted hover:
|
|
34
|
+
? 'border-border-accent bg-accent-blue/20 text-accent-blue'
|
|
35
|
+
: 'border-transparent text-text-muted hover:border-border-light hover:bg-surface-hover hover:text-text-primary'
|
|
33
36
|
}`}
|
|
34
37
|
>
|
|
35
38
|
{preview || (icon && <i className={`bx ${icon} text-base`} />)}
|
|
39
|
+
{label && <span className="whitespace-nowrap text-[11px] font-medium">{label}</span>}
|
|
36
40
|
<svg className={`w-2 h-2 opacity-40 transition-transform duration-100 ${open ? 'rotate-180' : ''}`} viewBox="0 0 8 5" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
37
41
|
<path d="M1 1l3 3 3-3" />
|
|
38
42
|
</svg>
|
|
@@ -55,7 +59,7 @@ export function ToolbarButton({ icon, preview, children, tooltip }) {
|
|
|
55
59
|
* Simple toolbar divider
|
|
56
60
|
*/
|
|
57
61
|
function Divider() {
|
|
58
|
-
return <div className="
|
|
62
|
+
return <div className="mx-0.5 h-6 w-px shrink-0 bg-border-light" aria-hidden="true" />
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
/**
|
|
@@ -67,13 +71,13 @@ export default function ShapeSidebar({ visible, children }) {
|
|
|
67
71
|
|
|
68
72
|
return (
|
|
69
73
|
<div
|
|
70
|
-
className={`absolute bottom-14 left-1/2 -translate-x-1/2
|
|
74
|
+
className={`absolute bottom-14 left-1/2 z-[999] max-w-[calc(100vw-24px)] -translate-x-1/2 overflow-x-auto rounded-xl border border-border-light bg-surface px-1.5 py-1.5 font-[lixFont] transition-all duration-200 ${
|
|
71
75
|
show
|
|
72
76
|
? 'opacity-100 pointer-events-auto translate-y-0'
|
|
73
77
|
: 'opacity-0 pointer-events-none translate-y-2'
|
|
74
78
|
}`}
|
|
75
79
|
>
|
|
76
|
-
<div className="flex items-center gap-
|
|
80
|
+
<div className="flex min-w-max items-center gap-1">
|
|
77
81
|
{children}
|
|
78
82
|
</div>
|
|
79
83
|
</div>
|
|
@@ -93,37 +97,25 @@ function LayerControls() {
|
|
|
93
97
|
window.__layerOrder[method](shape)
|
|
94
98
|
}
|
|
95
99
|
|
|
100
|
+
const actions = [
|
|
101
|
+
['sendToBack', 'bx-chevrons-down', t('sidebar.sendToBack', { defaultValue: 'Send to back' })],
|
|
102
|
+
['sendBackward', 'bx-chevron-down', t('sidebar.sendBackward')],
|
|
103
|
+
['bringForward', 'bx-chevron-up', t('sidebar.bringForward')],
|
|
104
|
+
['bringToFront', 'bx-chevrons-up', t('sidebar.bringToFront', { defaultValue: 'Bring to front' })],
|
|
105
|
+
]
|
|
106
|
+
|
|
96
107
|
return (
|
|
97
|
-
<
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
className="h-9 w-8 flex items-center justify-center rounded-lg text-text-muted hover:text-text-primary hover:bg-surface-hover transition-all duration-100"
|
|
109
|
-
>
|
|
110
|
-
<i className="bx bx-chevron-down text-base" />
|
|
111
|
-
</button>
|
|
112
|
-
<button
|
|
113
|
-
onClick={() => doLayer('bringForward')}
|
|
114
|
-
title={t('sidebar.bringForward')}
|
|
115
|
-
className="h-9 w-8 flex items-center justify-center rounded-lg text-text-muted hover:text-text-primary hover:bg-surface-hover transition-all duration-100"
|
|
116
|
-
>
|
|
117
|
-
<i className="bx bx-chevron-up text-base" />
|
|
118
|
-
</button>
|
|
119
|
-
<button
|
|
120
|
-
onClick={() => doLayer('bringToFront')}
|
|
121
|
-
title={t('sidebar.bringToFront', { defaultValue: 'Bring to front' })}
|
|
122
|
-
className="h-9 w-8 flex items-center justify-center rounded-lg text-text-muted hover:text-text-primary hover:bg-surface-hover transition-all duration-100"
|
|
123
|
-
>
|
|
124
|
-
<i className="bx bx-chevrons-up text-base" />
|
|
125
|
-
</button>
|
|
126
|
-
</div>
|
|
108
|
+
<ToolbarButton icon="bx-layer" tooltip="Arrange" label="Arrange">
|
|
109
|
+
<p className="mb-2 text-xs uppercase tracking-wider text-text-muted">Layer order</p>
|
|
110
|
+
<div className="grid grid-cols-2 gap-1">
|
|
111
|
+
{actions.map(([method, icon, label]) => (
|
|
112
|
+
<button key={method} type="button" onClick={() => doLayer(method)} className="flex min-w-32 cursor-pointer items-center gap-2 rounded-lg px-2.5 py-2 text-left text-xs text-text-secondary transition hover:bg-surface-hover hover:text-text-primary">
|
|
113
|
+
<i className={`bx ${icon} text-base`} aria-hidden="true" />
|
|
114
|
+
<span>{label}</span>
|
|
115
|
+
</button>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
</ToolbarButton>
|
|
127
119
|
)
|
|
128
120
|
}
|
|
129
121
|
|
|
@@ -4,8 +4,9 @@ import useSketchStore, { TOOLS } from '../../store/useSketchStore'
|
|
|
4
4
|
import ShapeSidebar, { ToolbarButton, Divider, LayerControls } from './ShapeSidebar'
|
|
5
5
|
import { useState, useCallback, useEffect } from 'react'
|
|
6
6
|
import { useTranslation } from '../../hooks/useTranslation'
|
|
7
|
+
import useUIStore from '../../store/useUIStore'
|
|
7
8
|
|
|
8
|
-
const TEXT_COLORS = ['#
|
|
9
|
+
const TEXT_COLORS = ['#000000', '#ffffff', '#FF8383', '#3A994C', '#56A2E8', '#FFD700', '#FF69B4', '#A855F7']
|
|
9
10
|
|
|
10
11
|
const FONTS = [
|
|
11
12
|
{ value: 'lixFont', label: 'Lix' },
|
|
@@ -26,7 +27,12 @@ export default function TextSidebar() {
|
|
|
26
27
|
const { t } = useTranslation()
|
|
27
28
|
const activeTool = useSketchStore((s) => s.activeTool)
|
|
28
29
|
const selectedShapeSidebar = useSketchStore((s) => s.selectedShapeSidebar)
|
|
29
|
-
const
|
|
30
|
+
const theme = useUIStore((s) => s.theme)
|
|
31
|
+
const resolvedTheme = theme === 'system' && typeof window !== 'undefined'
|
|
32
|
+
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
|
33
|
+
: theme
|
|
34
|
+
const [textColor, setTextColor] = useState(null)
|
|
35
|
+
const displayedTextColor = textColor ?? (resolvedTheme === 'light' ? '#000000' : '#ffffff')
|
|
30
36
|
const [fontSize, setFontSize] = useState('S')
|
|
31
37
|
const [font, setFont] = useState('lixFont')
|
|
32
38
|
const [codeMode, setCodeMode] = useState(false)
|
|
@@ -81,13 +87,13 @@ export default function TextSidebar() {
|
|
|
81
87
|
<ShapeSidebar visible={visible}>
|
|
82
88
|
{/* Color */}
|
|
83
89
|
<ToolbarButton tooltip={t('sidebar.textColor')}
|
|
84
|
-
preview={<span className="w-4 h-4 rounded-md border border-white/20" style={{ backgroundColor:
|
|
90
|
+
preview={<span className="w-4 h-4 rounded-md border border-white/20" style={{ backgroundColor: displayedTextColor }} />}
|
|
85
91
|
>
|
|
86
92
|
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.color')}</p>
|
|
87
93
|
<div className="grid grid-cols-4 gap-1.5">
|
|
88
94
|
{TEXT_COLORS.map((c) => (
|
|
89
95
|
<button key={c} onClick={() => updateColor(c)}
|
|
90
|
-
className={`w-7 h-7 rounded-md border-[1.5px] transition-all duration-100 ${
|
|
96
|
+
className={`w-7 h-7 rounded-md border-[1.5px] transition-all duration-100 ${displayedTextColor.toLowerCase() === c.toLowerCase() ? 'border-[#7667a8] scale-110' : 'border-white/[0.08] hover:border-white/20'}`}
|
|
91
97
|
style={{ backgroundColor: c }}
|
|
92
98
|
/>
|
|
93
99
|
))}
|
|
@@ -105,6 +105,13 @@ function invertShapeColors(prevResolved, nextResolved) {
|
|
|
105
105
|
shape.strokeColor = to
|
|
106
106
|
changed = true
|
|
107
107
|
}
|
|
108
|
+
if (shape.labelColor !== undefined) {
|
|
109
|
+
const labelColor = normalize(shape.labelColor)
|
|
110
|
+
if (labelColor === from || labelColor === '#e0e0e0' || labelColor === '#e8e3f3') {
|
|
111
|
+
shape.labelColor = to
|
|
112
|
+
changed = true
|
|
113
|
+
}
|
|
114
|
+
}
|
|
108
115
|
}
|
|
109
116
|
if (changed && typeof shape.draw === 'function') {
|
|
110
117
|
shape.draw()
|
|
@@ -140,7 +147,7 @@ function applyTheme(theme) {
|
|
|
140
147
|
body.classList.add(`theme-${resolved}`)
|
|
141
148
|
|
|
142
149
|
const svgEl = window.svg
|
|
143
|
-
if (svgEl) svgEl.style.background = resolved === 'light' ? '#
|
|
150
|
+
if (svgEl) svgEl.style.background = resolved === 'light' ? '#ffffff' : '#15111f'
|
|
144
151
|
}
|
|
145
152
|
|
|
146
153
|
const useUIStore = create((set, get) => ({
|
package/src/react/styles.css
CHANGED
|
@@ -35,6 +35,19 @@
|
|
|
35
35
|
|
|
36
36
|
.theme-light .lixsketch-canvas-root {
|
|
37
37
|
--lix-icon-filter: none;
|
|
38
|
+
--lixsketch-bg: #ffffff;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.theme-light .lixsketch-canvas-root :is(
|
|
42
|
+
.anchor,
|
|
43
|
+
.resize-anchor,
|
|
44
|
+
.resize-handle,
|
|
45
|
+
.rotate-anchor,
|
|
46
|
+
.rotation-anchor,
|
|
47
|
+
.multi-selection-anchor,
|
|
48
|
+
.multi-selection-rotation-anchor
|
|
49
|
+
) {
|
|
50
|
+
fill: #ffffff !important;
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
.theme-dark .lixsketch-canvas-root {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const CANVAS_BACKGROUNDS = {
|
|
2
2
|
light: [
|
|
3
|
-
{ color: '#
|
|
3
|
+
{ color: '#FFFFFF', label: 'menu.canvasBg.white' },
|
|
4
4
|
{ color: '#F6DBC0', label: 'menu.canvasBg.cream' },
|
|
5
5
|
{ color: '#F5EED2', label: 'menu.canvasBg.paper' },
|
|
6
6
|
{ color: '#F5F2ED', label: 'menu.canvasBg.skyTint' },
|
|
@@ -15,13 +15,22 @@ export const CANVAS_BACKGROUNDS = {
|
|
|
15
15
|
],
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export const DEFAULT_CANVAS_BACKGROUNDS = { dark: '#15111f', light: '#
|
|
18
|
+
export const DEFAULT_CANVAS_BACKGROUNDS = { dark: '#15111f', light: '#FFFFFF' }
|
|
19
|
+
const LEGACY_LIGHT_DEFAULT = '#F5F2ED'
|
|
20
|
+
const LIGHT_DEFAULT_MIGRATION_KEY = 'lixsketch-canvas-light-default-v2'
|
|
19
21
|
export const canvasBackgroundStorageKey = (theme) => `lixsketch-canvas-background-${theme}`
|
|
20
22
|
|
|
21
23
|
export function readCanvasBackground(theme, storage) {
|
|
22
24
|
const resolved = theme === 'light' ? 'light' : 'dark'
|
|
23
25
|
try {
|
|
24
26
|
const stored = storage?.getItem(canvasBackgroundStorageKey(resolved))
|
|
27
|
+
if (resolved === 'light' && storage?.getItem(LIGHT_DEFAULT_MIGRATION_KEY) !== '1') {
|
|
28
|
+
storage?.setItem?.(LIGHT_DEFAULT_MIGRATION_KEY, '1')
|
|
29
|
+
if (stored?.toLowerCase() === LEGACY_LIGHT_DEFAULT.toLowerCase()) {
|
|
30
|
+
storage?.setItem?.(canvasBackgroundStorageKey(resolved), DEFAULT_CANVAS_BACKGROUNDS.light)
|
|
31
|
+
return DEFAULT_CANVAS_BACKGROUNDS.light
|
|
32
|
+
}
|
|
33
|
+
}
|
|
25
34
|
const match = CANVAS_BACKGROUNDS[resolved].find(({ color }) => color.toLowerCase() === stored?.toLowerCase())
|
|
26
35
|
if (match) return match.color
|
|
27
36
|
} catch {}
|
package/src/shapes/Arrow.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
|
+
import { resolveThemeLabelColor } from '../utils/themeColors.js';
|
|
2
3
|
// Arrow shape class - extracted from drawArrow.js
|
|
3
4
|
// Depends on globals: svg, shapes, rough, currentShape, currentZoom
|
|
4
5
|
|
|
@@ -54,7 +55,7 @@ class Arrow {
|
|
|
54
55
|
// Embedded label support
|
|
55
56
|
this.label = options.label || '';
|
|
56
57
|
this.labelElement = null;
|
|
57
|
-
this.labelColor = options.labelColor
|
|
58
|
+
this.labelColor = resolveThemeLabelColor(options.labelColor);
|
|
58
59
|
this.labelFontSize = options.labelFontSize || 12;
|
|
59
60
|
this.labelBg = options.labelBg !== false; // labelBg:false → no knockout rect behind label
|
|
60
61
|
this.labelOffsetY = options.labelOffsetY || 0; // shift label along the perpendicular of the line, e.g. lift it clear of the stroke
|
|
@@ -784,7 +785,7 @@ class Arrow {
|
|
|
784
785
|
|
|
785
786
|
addAnchors() {
|
|
786
787
|
const anchorSize = 5 / currentZoom;
|
|
787
|
-
const anchorStrokeWidth = 2
|
|
788
|
+
const anchorStrokeWidth = 2;
|
|
788
789
|
|
|
789
790
|
let anchorPositions = [this.startPoint, this.endPoint];
|
|
790
791
|
|
|
@@ -2030,4 +2031,4 @@ updateFrameContainment() {
|
|
|
2030
2031
|
}
|
|
2031
2032
|
}
|
|
2032
2033
|
|
|
2033
|
-
export { Arrow };
|
|
2034
|
+
export { Arrow };
|
package/src/shapes/Circle.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
2
|
import { registerRotationAnchor } from '../core/ScreenSpaceControls.js';
|
|
3
3
|
import { canvasToLocal, localToCanvas } from '../core/CanvasSpace.js';
|
|
4
|
+
import { resolveThemeLabelColor } from '../utils/themeColors.js';
|
|
4
5
|
// Circle shape class - extracted from drawCircle.js
|
|
5
6
|
// Depends on globals: svg, shapes, rough, currentShape, currentZoom, rc
|
|
6
7
|
|
|
@@ -30,6 +31,11 @@ class Circle {
|
|
|
30
31
|
strokeDasharray: "",
|
|
31
32
|
...options
|
|
32
33
|
};
|
|
34
|
+
// RoughJS otherwise generates a different contour on every redraw.
|
|
35
|
+
// Keep a stable contour for later edits and the final drawing render.
|
|
36
|
+
if (!Number.isInteger(this.options.seed) || this.options.seed <= 0) {
|
|
37
|
+
this.options.seed = Math.floor(Math.random() * 0x7fffffff) + 1;
|
|
38
|
+
}
|
|
33
39
|
this.element = null;
|
|
34
40
|
this.isSelected = false;
|
|
35
41
|
this.rotation = 0;
|
|
@@ -48,7 +54,7 @@ class Circle {
|
|
|
48
54
|
// Embedded label support
|
|
49
55
|
this.label = options.label || '';
|
|
50
56
|
this.labelElement = null;
|
|
51
|
-
this.labelColor = options.labelColor
|
|
57
|
+
this.labelColor = resolveThemeLabelColor(options.labelColor);
|
|
52
58
|
this.labelFontSize = options.labelFontSize || 14;
|
|
53
59
|
this._isEditingLabel = false;
|
|
54
60
|
this._hitArea = null;
|
|
@@ -60,6 +66,9 @@ class Circle {
|
|
|
60
66
|
this.shadeDirection = options.shadeDirection || 'bottom';
|
|
61
67
|
this._shadeEllipse = null;
|
|
62
68
|
this._shadeGradient = null;
|
|
69
|
+
this.isBeingDrawn = false;
|
|
70
|
+
this._drawingRenderFrame = null;
|
|
71
|
+
this._isLivePreviewElement = false;
|
|
63
72
|
|
|
64
73
|
if(!this.group.parentNode) {
|
|
65
74
|
svg.appendChild(this.group);
|
|
@@ -105,7 +114,27 @@ class Circle {
|
|
|
105
114
|
const isInitialDraw = this.element === null;
|
|
106
115
|
const sizeChanged = this.rx !== this._lastDrawn.rx || this.ry !== this._lastDrawn.ry;
|
|
107
116
|
const optionsChanged = optionsString !== this._lastDrawn.options;
|
|
108
|
-
if (
|
|
117
|
+
if (this.isBeingDrawn) {
|
|
118
|
+
if (!this._isLivePreviewElement) {
|
|
119
|
+
if (this.element && this.element.parentNode === this.group) {
|
|
120
|
+
this.group.removeChild(this.element);
|
|
121
|
+
}
|
|
122
|
+
this.element = document.createElementNS('http://www.w3.org/2000/svg', 'ellipse');
|
|
123
|
+
this.element.setAttribute('class', 'circle-drawing-preview');
|
|
124
|
+
this.element.setAttribute('pointer-events', 'none');
|
|
125
|
+
this.group.appendChild(this.element);
|
|
126
|
+
this._isLivePreviewElement = true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.element.setAttribute('cx', 0);
|
|
130
|
+
this.element.setAttribute('cy', 0);
|
|
131
|
+
this.element.setAttribute('rx', this.rx);
|
|
132
|
+
this.element.setAttribute('ry', this.ry);
|
|
133
|
+
this.element.setAttribute('fill', this.options.fill || 'transparent');
|
|
134
|
+
this.element.setAttribute('stroke', this.options.stroke || '#fff');
|
|
135
|
+
this.element.setAttribute('stroke-width', this.options.strokeWidth || 2);
|
|
136
|
+
this.element.setAttribute('stroke-dasharray', this.options.strokeDasharray || '');
|
|
137
|
+
} else if (isInitialDraw || optionsChanged || sizeChanged || this._isLivePreviewElement) {
|
|
109
138
|
if (this.element && this.element.parentNode === this.group) {
|
|
110
139
|
this.group.removeChild(this.element);
|
|
111
140
|
}
|
|
@@ -113,6 +142,7 @@ class Circle {
|
|
|
113
142
|
const roughEllipse = rc.ellipse(0, 0, this.rx * 2, this.ry * 2, this.options);
|
|
114
143
|
this.element = roughEllipse;
|
|
115
144
|
this.group.appendChild(roughEllipse);
|
|
145
|
+
this._isLivePreviewElement = false;
|
|
116
146
|
|
|
117
147
|
this._lastDrawn.rx = this.rx;
|
|
118
148
|
this._lastDrawn.ry = this.ry;
|
|
@@ -151,6 +181,31 @@ class Circle {
|
|
|
151
181
|
}
|
|
152
182
|
}
|
|
153
183
|
|
|
184
|
+
scheduleDrawingRender() {
|
|
185
|
+
if (this._drawingRenderFrame !== null) return;
|
|
186
|
+
if (typeof requestAnimationFrame !== 'function') {
|
|
187
|
+
this.draw();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
this._drawingRenderFrame = requestAnimationFrame(() => {
|
|
191
|
+
this._drawingRenderFrame = null;
|
|
192
|
+
this.draw();
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
cancelDrawingRender() {
|
|
197
|
+
if (this._drawingRenderFrame !== null && typeof cancelAnimationFrame === 'function') {
|
|
198
|
+
cancelAnimationFrame(this._drawingRenderFrame);
|
|
199
|
+
}
|
|
200
|
+
this._drawingRenderFrame = null;
|
|
201
|
+
this.isBeingDrawn = false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
finalizeDrawing() {
|
|
205
|
+
this.cancelDrawingRender();
|
|
206
|
+
this.draw();
|
|
207
|
+
}
|
|
208
|
+
|
|
154
209
|
move(dx, dy) {
|
|
155
210
|
this.x += dx;
|
|
156
211
|
this.y += dy;
|
|
@@ -343,6 +398,7 @@ class Circle {
|
|
|
343
398
|
const pointsAttr = outlinePoints.map(p => p.join(',')).join(' ');
|
|
344
399
|
const outline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
|
|
345
400
|
outline.setAttribute('points', pointsAttr);
|
|
401
|
+
outline.setAttribute('class', 'selection-outline');
|
|
346
402
|
outline.setAttribute('fill', 'none');
|
|
347
403
|
outline.setAttribute('stroke', '#5B57D1');
|
|
348
404
|
outline.setAttribute('stroke-width', 1.5);
|
package/src/shapes/Frame.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { cleanupAttachments } from '../tools/arrowTool.js';
|
|
6
6
|
import { registerRotationAnchor } from '../core/ScreenSpaceControls.js';
|
|
7
|
+
import { captureFrameChildStates } from '../core/UndoRedo.js';
|
|
7
8
|
|
|
8
9
|
function getSVGCoordsFromMouse(e) {
|
|
9
10
|
const viewBox = svg.viewBox.baseVal;
|
|
@@ -434,8 +435,9 @@ move(dx, dy) {
|
|
|
434
435
|
shape.y += dy;
|
|
435
436
|
}
|
|
436
437
|
|
|
437
|
-
//
|
|
438
|
-
|
|
438
|
+
// Line.move() batches its own render to one animation frame.
|
|
439
|
+
// Calling draw() here would regenerate it immediately as well.
|
|
440
|
+
if (shape.shapeName !== 'line' && typeof shape.draw === 'function') {
|
|
439
441
|
shape.draw();
|
|
440
442
|
}
|
|
441
443
|
|
|
@@ -768,6 +770,7 @@ startLabelEdit(labelElement) {
|
|
|
768
770
|
|
|
769
771
|
const padding = 20;
|
|
770
772
|
const oldState = { x: this.x, y: this.y, width: this.width, height: this.height, rotation: this.rotation };
|
|
773
|
+
oldState.containedShapes = captureFrameChildStates(this);
|
|
771
774
|
|
|
772
775
|
this.x = minX - padding;
|
|
773
776
|
this.y = minY - padding;
|
|
@@ -795,7 +798,7 @@ startLabelEdit(labelElement) {
|
|
|
795
798
|
|
|
796
799
|
addAnchors() {
|
|
797
800
|
const anchorSize = 8 / currentZoom;
|
|
798
|
-
const anchorStrokeWidth = 2
|
|
801
|
+
const anchorStrokeWidth = 2;
|
|
799
802
|
|
|
800
803
|
// Calculate anchor positions (corners + midpoints + rotation handle)
|
|
801
804
|
const anchorPositions = [
|
|
@@ -821,6 +824,7 @@ startLabelEdit(labelElement) {
|
|
|
821
824
|
|
|
822
825
|
// Create selection outline first (behind anchors)
|
|
823
826
|
const outline = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
827
|
+
outline.setAttribute('class', 'selection-outline');
|
|
824
828
|
outline.setAttribute('x', this.x);
|
|
825
829
|
outline.setAttribute('y', this.y);
|
|
826
830
|
outline.setAttribute('width', this.width);
|
|
@@ -855,6 +859,7 @@ startLabelEdit(labelElement) {
|
|
|
855
859
|
anchor.setAttribute("stroke", "#5B57D1");
|
|
856
860
|
anchor.setAttribute("stroke-width", anchorStrokeWidth);
|
|
857
861
|
anchor.setAttribute("vector-effect", "non-scaling-stroke");
|
|
862
|
+
anchor.setAttribute("class", "rotate-anchor");
|
|
858
863
|
|
|
859
864
|
// Add rotation line
|
|
860
865
|
const rotationLine = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
|
@@ -891,6 +896,7 @@ startLabelEdit(labelElement) {
|
|
|
891
896
|
anchor.setAttribute("stroke", "#5B57D1");
|
|
892
897
|
anchor.setAttribute("stroke-width", anchorStrokeWidth);
|
|
893
898
|
anchor.setAttribute("vector-effect", "non-scaling-stroke");
|
|
899
|
+
anchor.setAttribute("class", "anchor");
|
|
894
900
|
}
|
|
895
901
|
|
|
896
902
|
// Apply rotation to anchor if needed
|
|
@@ -918,7 +924,8 @@ startLabelEdit(labelElement) {
|
|
|
918
924
|
y: this.y,
|
|
919
925
|
width: this.width,
|
|
920
926
|
height: this.height,
|
|
921
|
-
rotation: this.rotation
|
|
927
|
+
rotation: this.rotation,
|
|
928
|
+
containedShapes: captureFrameChildStates(this),
|
|
922
929
|
};
|
|
923
930
|
|
|
924
931
|
const startMousePos = getSVGCoordsFromMouse(e);
|
|
@@ -573,7 +573,7 @@ class FreehandStroke {
|
|
|
573
573
|
|
|
574
574
|
addAnchors() {
|
|
575
575
|
const anchorSize = 10 / currentZoom;
|
|
576
|
-
const anchorStrokeWidth = 2
|
|
576
|
+
const anchorStrokeWidth = 2;
|
|
577
577
|
|
|
578
578
|
const expandedX = this.boundingBox.x - this.selectionPadding;
|
|
579
579
|
const expandedY = this.boundingBox.y - this.selectionPadding;
|
|
@@ -645,6 +645,7 @@ class FreehandStroke {
|
|
|
645
645
|
];
|
|
646
646
|
|
|
647
647
|
const outline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
|
|
648
|
+
outline.setAttribute('class', 'selection-outline');
|
|
648
649
|
outline.setAttribute('points', outlinePoints.map(p => p.join(',')).join(' '));
|
|
649
650
|
outline.setAttribute('fill', 'none');
|
|
650
651
|
outline.setAttribute('stroke', '#5B57D1');
|
package/src/shapes/Line.js
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
// Line shape class - extracted from lineTool.js
|
|
3
3
|
// Depends on globals: svg, shapes, rough, currentShape, currentZoom
|
|
4
4
|
import { updateAttachedArrows as updateArrowsForShape } from '../tools/arrowTool.js';
|
|
5
|
+
import { resolveThemeLabelColor } from '../utils/themeColors.js';
|
|
5
6
|
|
|
6
7
|
const rc = rough.svg(svg);
|
|
7
8
|
// Issue #38 follow-up: read the default stroke from the active theme
|
|
8
9
|
// instead of pinning to a single colour. White was invisible on the new
|
|
9
|
-
// light canvas,
|
|
10
|
+
// light canvas, black is invisible if the user toggles dark.
|
|
10
11
|
function getThemeStroke() {
|
|
11
12
|
if (typeof document === 'undefined') return '#fff';
|
|
12
|
-
return document.body && document.body.classList.contains('theme-dark') ? '#fff' : '#
|
|
13
|
+
return document.body && document.body.classList.contains('theme-dark') ? '#fff' : '#000000';
|
|
13
14
|
}
|
|
14
15
|
const lineStrokeWidth = 2;
|
|
15
16
|
let hoveredFrameLine = null;
|
|
@@ -19,6 +20,12 @@ class Line {
|
|
|
19
20
|
this.startPoint = startPoint;
|
|
20
21
|
this.endPoint = endPoint;
|
|
21
22
|
this.options = { ...options };
|
|
23
|
+
// RoughJS otherwise chooses a fresh random stroke on every redraw.
|
|
24
|
+
// A stable per-line seed keeps the hand-drawn contour visually fixed
|
|
25
|
+
// while its endpoints move.
|
|
26
|
+
if (!Number.isInteger(this.options.seed) || this.options.seed <= 0) {
|
|
27
|
+
this.options.seed = Math.floor(Math.random() * 0x7fffffff) + 1;
|
|
28
|
+
}
|
|
22
29
|
this.group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
23
30
|
this.isSelected = false;
|
|
24
31
|
this.anchors = [];
|
|
@@ -37,11 +44,12 @@ class Line {
|
|
|
37
44
|
// Embedded label support
|
|
38
45
|
this.label = options.label || '';
|
|
39
46
|
this.labelElement = null;
|
|
40
|
-
this.labelColor = options.labelColor
|
|
47
|
+
this.labelColor = resolveThemeLabelColor(options.labelColor);
|
|
41
48
|
this.labelFontSize = options.labelFontSize || 12;
|
|
42
49
|
this._isEditingLabel = false;
|
|
43
50
|
this._hitArea = null;
|
|
44
51
|
this._labelBg = null;
|
|
52
|
+
this._dragRenderFrame = null;
|
|
45
53
|
|
|
46
54
|
svg.appendChild(this.group);
|
|
47
55
|
this._setupLabelDblClick();
|
|
@@ -431,7 +439,7 @@ removeSelection() {
|
|
|
431
439
|
this.anchors = [];
|
|
432
440
|
|
|
433
441
|
const anchorSize = 10 / (currentZoom || 1);
|
|
434
|
-
const anchorStrokeWidth = 2
|
|
442
|
+
const anchorStrokeWidth = 2;
|
|
435
443
|
|
|
436
444
|
// Start point anchor
|
|
437
445
|
const startAnchor = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
@@ -537,7 +545,38 @@ removeSelection() {
|
|
|
537
545
|
};
|
|
538
546
|
}
|
|
539
547
|
|
|
540
|
-
|
|
548
|
+
this.scheduleDragRender();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
scheduleDragRender() {
|
|
552
|
+
if (this._dragRenderFrame !== null) return;
|
|
553
|
+
if (typeof requestAnimationFrame !== 'function') {
|
|
554
|
+
this.renderDragGeometry();
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
this._dragRenderFrame = requestAnimationFrame(() => {
|
|
558
|
+
this._dragRenderFrame = null;
|
|
559
|
+
this.renderDragGeometry();
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
renderDragGeometry() {
|
|
564
|
+
this.updateLineElement();
|
|
565
|
+
this.updateAnchorPositions();
|
|
566
|
+
if (this._hitArea) {
|
|
567
|
+
const path = this.isCurved && this.controlPoint
|
|
568
|
+
? `M ${this.startPoint.x} ${this.startPoint.y} Q ${this.controlPoint.x} ${this.controlPoint.y} ${this.endPoint.x} ${this.endPoint.y}`
|
|
569
|
+
: `M ${this.startPoint.x} ${this.startPoint.y} L ${this.endPoint.x} ${this.endPoint.y}`;
|
|
570
|
+
this._hitArea.setAttribute('d', path);
|
|
571
|
+
}
|
|
572
|
+
this._updateLabelElement();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
finalizeMove() {
|
|
576
|
+
if (this._dragRenderFrame !== null && typeof cancelAnimationFrame === 'function') {
|
|
577
|
+
cancelAnimationFrame(this._dragRenderFrame);
|
|
578
|
+
}
|
|
579
|
+
this._dragRenderFrame = null;
|
|
541
580
|
this.draw();
|
|
542
581
|
}
|
|
543
582
|
|
|
@@ -620,21 +659,9 @@ removeSelection() {
|
|
|
620
659
|
this.controlPoint.y += dy;
|
|
621
660
|
}
|
|
622
661
|
|
|
623
|
-
//
|
|
624
|
-
|
|
625
|
-
this.
|
|
626
|
-
|
|
627
|
-
// Update hit area path
|
|
628
|
-
if (this._hitArea) {
|
|
629
|
-
if (this.isCurved && this.controlPoint) {
|
|
630
|
-
this._hitArea.setAttribute('d', `M ${this.startPoint.x} ${this.startPoint.y} Q ${this.controlPoint.x} ${this.controlPoint.y} ${this.endPoint.x} ${this.endPoint.y}`);
|
|
631
|
-
} else {
|
|
632
|
-
this._hitArea.setAttribute('d', `M ${this.startPoint.x} ${this.startPoint.y} L ${this.endPoint.x} ${this.endPoint.y}`);
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
// Update label position
|
|
637
|
-
this._updateLabelElement();
|
|
662
|
+
// Multiple pointer/snap updates in one browser frame collapse into a
|
|
663
|
+
// single DOM + RoughJS update.
|
|
664
|
+
this.scheduleDragRender();
|
|
638
665
|
|
|
639
666
|
// Only update frame containment if not being moved by a parent frame
|
|
640
667
|
if (!this.isBeingMovedByFrame) {
|