@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,174 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import useSketchStore, { TOOLS } from '../../store/useSketchStore'
|
|
4
|
+
import ShapeSidebar, { ToolbarButton, Divider, LayerControls } from './ShapeSidebar'
|
|
5
|
+
import { useState, useCallback, useEffect } from 'react'
|
|
6
|
+
import { useTranslation } from '../../hooks/useTranslation'
|
|
7
|
+
|
|
8
|
+
const TEXT_COLORS = ['#1a1a20', '#ffffff', '#FF8383', '#3A994C', '#56A2E8', '#FFD700', '#FF69B4', '#A855F7']
|
|
9
|
+
|
|
10
|
+
const FONTS = [
|
|
11
|
+
{ value: 'lixFont', label: 'Lix' },
|
|
12
|
+
{ value: 'lixCode', label: 'Code' },
|
|
13
|
+
{ value: 'lixDefault', label: 'Default' },
|
|
14
|
+
{ value: 'lixFancy', label: 'Fancy' },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
const SIZE_MAP = { S: '20px', M: '30px', L: '48px', XL: '72px' }
|
|
18
|
+
|
|
19
|
+
const LANGUAGES = [
|
|
20
|
+
'javascript', 'typescript', 'python', 'java', 'c', 'cpp', 'csharp',
|
|
21
|
+
'go', 'rust', 'ruby', 'php', 'swift', 'kotlin', 'html', 'css',
|
|
22
|
+
'sql', 'bash', 'json', 'yaml', 'markdown',
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
export default function TextSidebar() {
|
|
26
|
+
const { t } = useTranslation()
|
|
27
|
+
const activeTool = useSketchStore((s) => s.activeTool)
|
|
28
|
+
const selectedShapeSidebar = useSketchStore((s) => s.selectedShapeSidebar)
|
|
29
|
+
const [textColor, setTextColor] = useState('#fff')
|
|
30
|
+
const [fontSize, setFontSize] = useState('M')
|
|
31
|
+
const [font, setFont] = useState('lixFont')
|
|
32
|
+
const [codeMode, setCodeMode] = useState(false)
|
|
33
|
+
const [language, setLanguage] = useState('javascript')
|
|
34
|
+
|
|
35
|
+
// Sync code mode state from engine when a shape is selected
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
window.__onCodeModeChanged = (isCode) => {
|
|
38
|
+
setCodeMode(isCode)
|
|
39
|
+
window.isTextInCodeMode = isCode
|
|
40
|
+
}
|
|
41
|
+
return () => { window.__onCodeModeChanged = null }
|
|
42
|
+
}, [])
|
|
43
|
+
|
|
44
|
+
const updateColor = useCallback((c) => {
|
|
45
|
+
setTextColor(c)
|
|
46
|
+
if (window.updateSelectedTextStyle) window.updateSelectedTextStyle({ color: c })
|
|
47
|
+
}, [])
|
|
48
|
+
|
|
49
|
+
const updateFont = useCallback((f) => {
|
|
50
|
+
setFont(f)
|
|
51
|
+
if (window.updateSelectedTextStyle) window.updateSelectedTextStyle({ font: f })
|
|
52
|
+
}, [])
|
|
53
|
+
|
|
54
|
+
const updateSize = useCallback((s) => {
|
|
55
|
+
setFontSize(s)
|
|
56
|
+
if (window.updateSelectedTextStyle) window.updateSelectedTextStyle({ fontSize: SIZE_MAP[s] })
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
59
|
+
const toggleCodeMode = useCallback(() => {
|
|
60
|
+
const next = !codeMode
|
|
61
|
+
setCodeMode(next)
|
|
62
|
+
window.isTextInCodeMode = next
|
|
63
|
+
if (next) {
|
|
64
|
+
// Convert selected text → code
|
|
65
|
+
if (window.__convertTextToCode) window.__convertTextToCode()
|
|
66
|
+
} else {
|
|
67
|
+
// Convert selected code → text
|
|
68
|
+
if (window.__convertCodeToText) window.__convertCodeToText()
|
|
69
|
+
}
|
|
70
|
+
}, [codeMode])
|
|
71
|
+
|
|
72
|
+
const updateLanguage = useCallback((lang) => {
|
|
73
|
+
setLanguage(lang)
|
|
74
|
+
if (window.__setCodeLanguage) window.__setCodeLanguage(lang)
|
|
75
|
+
}, [])
|
|
76
|
+
|
|
77
|
+
const visible = activeTool === TOOLS.TEXT || activeTool === TOOLS.CODE || selectedShapeSidebar === 'text'
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<>
|
|
81
|
+
<ShapeSidebar visible={visible}>
|
|
82
|
+
{/* Color */}
|
|
83
|
+
<ToolbarButton tooltip={t('sidebar.textColor')}
|
|
84
|
+
preview={<span className="w-4 h-4 rounded-md border border-white/20" style={{ backgroundColor: textColor }} />}
|
|
85
|
+
>
|
|
86
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.color')}</p>
|
|
87
|
+
<div className="grid grid-cols-4 gap-1.5">
|
|
88
|
+
{TEXT_COLORS.map((c) => (
|
|
89
|
+
<button key={c} onClick={() => updateColor(c)}
|
|
90
|
+
className={`w-7 h-7 rounded-md border-[1.5px] transition-all duration-100 ${textColor === c ? 'border-[#5B57D1] scale-110' : 'border-white/[0.08] hover:border-white/20'}`}
|
|
91
|
+
style={{ backgroundColor: c }}
|
|
92
|
+
/>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
</ToolbarButton>
|
|
96
|
+
|
|
97
|
+
{!codeMode && (
|
|
98
|
+
<>
|
|
99
|
+
<Divider />
|
|
100
|
+
|
|
101
|
+
{/* Font */}
|
|
102
|
+
<ToolbarButton icon="bxs-font-family" tooltip={t('sidebar.font')}>
|
|
103
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.font')}</p>
|
|
104
|
+
<div className="flex flex-col gap-0.5">
|
|
105
|
+
{FONTS.map((f) => (
|
|
106
|
+
<button key={f.value} onClick={() => updateFont(f.value)}
|
|
107
|
+
className={`flex items-center px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 ${font === f.value ? 'bg-[#5B57D1] text-white' : 'text-text-secondary hover:bg-white/[0.06]'}`}
|
|
108
|
+
style={{ fontFamily: f.value }}
|
|
109
|
+
>
|
|
110
|
+
{f.label}
|
|
111
|
+
</button>
|
|
112
|
+
))}
|
|
113
|
+
</div>
|
|
114
|
+
</ToolbarButton>
|
|
115
|
+
</>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
<Divider />
|
|
119
|
+
|
|
120
|
+
{/* Size */}
|
|
121
|
+
<ToolbarButton icon="bxs-chevrons-up" tooltip={t('sidebar.size')}>
|
|
122
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.size')}</p>
|
|
123
|
+
<div className="flex items-center gap-1">
|
|
124
|
+
{['S', 'M', 'L', 'XL'].map((s) => (
|
|
125
|
+
<button key={s} onClick={() => updateSize(s)}
|
|
126
|
+
className={`w-8 h-8 flex items-center justify-center rounded-lg text-xs transition-all duration-100 ${fontSize === s ? 'bg-[#5B57D1]/20 text-[#5B57D1]' : 'text-text-muted hover:bg-white/[0.06]'}`}
|
|
127
|
+
>{s}</button>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
</ToolbarButton>
|
|
131
|
+
|
|
132
|
+
<Divider />
|
|
133
|
+
|
|
134
|
+
{/* Code mode */}
|
|
135
|
+
<ToolbarButton icon="bxs-terminal" tooltip={t('sidebar.codeMode')}>
|
|
136
|
+
<p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.code')}</p>
|
|
137
|
+
<div className="flex flex-col gap-2">
|
|
138
|
+
<button onClick={toggleCodeMode}
|
|
139
|
+
className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 ${codeMode ? 'bg-[#5B57D1] text-white' : 'text-text-secondary hover:bg-white/[0.06]'}`}
|
|
140
|
+
>
|
|
141
|
+
<div className={`w-6 h-3 rounded-full transition-all duration-150 relative ${codeMode ? 'bg-white/30' : 'bg-white/10'}`}>
|
|
142
|
+
<div className={`absolute top-0.5 w-2 h-2 rounded-full bg-white transition-all duration-150 ${codeMode ? 'left-3.5' : 'left-0.5'}`} />
|
|
143
|
+
</div>
|
|
144
|
+
{codeMode ? t('sidebar.on') : t('sidebar.off')}
|
|
145
|
+
</button>
|
|
146
|
+
{codeMode && (
|
|
147
|
+
<div className="flex flex-wrap gap-1 max-w-[180px]">
|
|
148
|
+
{LANGUAGES.map((lang) => (
|
|
149
|
+
<button key={lang} onClick={() => updateLanguage(lang)}
|
|
150
|
+
className={`px-1.5 py-0.5 rounded text-[9px] transition-all duration-100 ${language === lang ? 'bg-[#5B57D1]/20 text-[#5B57D1]' : 'text-text-dim hover:bg-white/[0.06] hover:text-text-secondary'}`}
|
|
151
|
+
>{lang}</button>
|
|
152
|
+
))}
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
156
|
+
</ToolbarButton>
|
|
157
|
+
<Divider />
|
|
158
|
+
<LayerControls />
|
|
159
|
+
</ShapeSidebar>
|
|
160
|
+
|
|
161
|
+
{/* Keyboard shortcut hint — positioned below the property bar */}
|
|
162
|
+
<div
|
|
163
|
+
className={`absolute bottom-7 left-1/2 -translate-x-1/2 z-[998] font-[lixFont] transition-all duration-200 ${
|
|
164
|
+
visible ? 'opacity-100 translate-y-0' : 'opacity-0 pointer-events-none translate-y-2'
|
|
165
|
+
}`}
|
|
166
|
+
>
|
|
167
|
+
<p className="text-[10px] text-white/30 leading-relaxed text-center whitespace-nowrap">
|
|
168
|
+
<span className="text-white/50">Ctrl+Enter</span> / <span className="text-white/50">Enter</span> — {t('sidebar.renderText')}
|
|
169
|
+
<span className="text-white/50">Shift+Enter</span> — {t('sidebar.newLine')}
|
|
170
|
+
</p>
|
|
171
|
+
</div>
|
|
172
|
+
</>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Inert replacements for the standalone product's auth / cloud / collab
|
|
2
|
+
// state. These let us copy components that reference cloud-coupled hooks
|
|
3
|
+
// without hauling the whole product backend along. Each export mirrors
|
|
4
|
+
// the shape the consumer expects but with no-op implementations.
|
|
5
|
+
|
|
6
|
+
const noop = () => {};
|
|
7
|
+
|
|
8
|
+
const inertAuth = {
|
|
9
|
+
isAuthenticated: false,
|
|
10
|
+
user: null,
|
|
11
|
+
setUser: noop,
|
|
12
|
+
setAuthenticated: noop,
|
|
13
|
+
logout: noop,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const inertCollab = {
|
|
17
|
+
roomId: null,
|
|
18
|
+
connected: false,
|
|
19
|
+
setRoom: noop,
|
|
20
|
+
setConnected: noop,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const inertProfile = {
|
|
24
|
+
profile: null,
|
|
25
|
+
setProfile: noop,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function makeStore(state) {
|
|
29
|
+
// Match the zustand call signature used in selectors:
|
|
30
|
+
// const x = useStore((s) => s.foo)
|
|
31
|
+
// const x = useStore.getState().foo
|
|
32
|
+
function useStore(selector) {
|
|
33
|
+
return typeof selector === 'function' ? selector(state) : state;
|
|
34
|
+
}
|
|
35
|
+
useStore.getState = () => state;
|
|
36
|
+
useStore.setState = noop;
|
|
37
|
+
useStore.subscribe = () => () => {};
|
|
38
|
+
return useStore;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const useAuthStore = makeStore(inertAuth);
|
|
42
|
+
export const useCollabStore = makeStore(inertCollab);
|
|
43
|
+
export const useProfileStore = makeStore(inertProfile);
|
|
44
|
+
|
|
45
|
+
export const WORKER_URL = '';
|
|
46
|
+
export function getSessionID() {
|
|
47
|
+
if (typeof window !== 'undefined' && window.__sessionID) return window.__sessionID;
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function triggerDocCloudSync() { /* offline mode: no-op */ }
|
|
52
|
+
export function persistLayoutMode() { /* offline mode: no-op */ }
|
|
53
|
+
|
|
54
|
+
// Cloud-sync trigger used by the standalone product's AppMenu / keyboard
|
|
55
|
+
// shortcuts. The package's offline build should never hit a server, so we
|
|
56
|
+
// resolve immediately. The host's onSceneChange callback (passed to
|
|
57
|
+
// LixSketchCanvas) handles real persistence.
|
|
58
|
+
export async function triggerCloudSync() { return false; }
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react'
|
|
4
|
+
import useSketchStore from '../store/useSketchStore'
|
|
5
|
+
import { WORKER_URL } from './inertStores'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hook that initializes the SketchEngine on the provided SVG element
|
|
9
|
+
* and bridges Zustand activeTool changes to the engine's window flags.
|
|
10
|
+
*/
|
|
11
|
+
export default function useSketchEngine(svgRef, ready = true) {
|
|
12
|
+
const engineRef = useRef(null)
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!ready || !svgRef.current) return
|
|
16
|
+
|
|
17
|
+
let cancelled = false
|
|
18
|
+
|
|
19
|
+
async function initEngine() {
|
|
20
|
+
try {
|
|
21
|
+
// Expose Zustand store API on window BEFORE engine init
|
|
22
|
+
// so SketchEngine globals can call into React state
|
|
23
|
+
const storeState = useSketchStore.getState()
|
|
24
|
+
window.__sketchStoreApi = {
|
|
25
|
+
setSelectedShapeSidebar: (sidebar) => useSketchStore.getState().setSelectedShapeSidebar(sidebar),
|
|
26
|
+
clearSelectedShapeSidebar: () => useSketchStore.getState().clearSelectedShapeSidebar(),
|
|
27
|
+
setActiveTool: (tool, opts) => useSketchStore.getState().setActiveTool(tool, opts),
|
|
28
|
+
setZoom: (zoom) => useSketchStore.setState({ zoom }),
|
|
29
|
+
getState: () => useSketchStore.getState(),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Import the engine class directly (not via ../../index.js) so the
|
|
33
|
+
// chunk doesn't pull SceneSerializer + the shape graph in before
|
|
34
|
+
// engine.init() has had a chance to wire window.svg / window.rough.
|
|
35
|
+
const { SketchEngine } = await import('../../SketchEngine.js')
|
|
36
|
+
if (cancelled) return
|
|
37
|
+
|
|
38
|
+
const engine = new SketchEngine(svgRef.current)
|
|
39
|
+
await engine.init()
|
|
40
|
+
engineRef.current = engine
|
|
41
|
+
window.__sketchEngine = engine
|
|
42
|
+
window.__WORKER_URL = WORKER_URL
|
|
43
|
+
|
|
44
|
+
// Sync current tool immediately after init
|
|
45
|
+
const currentTool = useSketchStore.getState().activeTool
|
|
46
|
+
engine.setActiveTool(currentTool)
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error('[useSketchEngine] Failed to initialize:', err)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
initEngine()
|
|
53
|
+
|
|
54
|
+
return () => {
|
|
55
|
+
cancelled = true
|
|
56
|
+
if (engineRef.current) {
|
|
57
|
+
engineRef.current.cleanup()
|
|
58
|
+
engineRef.current = null
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}, [svgRef, ready])
|
|
62
|
+
|
|
63
|
+
// Subscribe to Zustand activeTool changes and bridge to engine
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const unsub = useSketchStore.subscribe(
|
|
66
|
+
(state, prevState) => {
|
|
67
|
+
if (state.activeTool !== prevState?.activeTool && engineRef.current) {
|
|
68
|
+
engineRef.current.setActiveTool(state.activeTool)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
return unsub
|
|
73
|
+
}, [])
|
|
74
|
+
|
|
75
|
+
return engineRef
|
|
76
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Lightweight i18n stub for the offline package. Converts dotted keys
|
|
2
|
+
// into readable labels so consumers don't have to ship a full translation
|
|
3
|
+
// table just to get nice button names. A real `t()` from the host can be
|
|
4
|
+
// passed in via `options.t` if needed (LixSketchCanvas can wire it up
|
|
5
|
+
// later).
|
|
6
|
+
//
|
|
7
|
+
// t('sidebar.sectionHeader.stroke') → 'Stroke'
|
|
8
|
+
// t('sidebar.bringForward') → 'Bring forward'
|
|
9
|
+
// t('menu.canvasBackground') → 'Canvas background'
|
|
10
|
+
// t('shortcuts.deselect', 'Deselect') → 'Deselect' (fallback wins)
|
|
11
|
+
|
|
12
|
+
// Hand-written overrides for keys that don't reduce cleanly from the
|
|
13
|
+
// last segment alone (acronyms, hyphens, multi-word phrases).
|
|
14
|
+
const OVERRIDES = {
|
|
15
|
+
'sidebar.sectionHeader.stroke': 'Stroke',
|
|
16
|
+
'sidebar.sectionHeader.fill': 'Fill',
|
|
17
|
+
'sidebar.sectionHeader.background': 'Background',
|
|
18
|
+
'sidebar.sectionHeader.backgroundImage': 'Background image',
|
|
19
|
+
'sidebar.sectionHeader.width': 'Width',
|
|
20
|
+
'sidebar.sectionHeader.edge': 'Edge style',
|
|
21
|
+
'sidebar.sectionHeader.head': 'Arrow head',
|
|
22
|
+
'sidebar.sectionHeader.type': 'Type',
|
|
23
|
+
'sidebar.sectionHeader.name': 'Name',
|
|
24
|
+
'sidebar.sectionHeader.color': 'Color',
|
|
25
|
+
'sidebar.sectionHeader.fit': 'Fit',
|
|
26
|
+
'sidebar.sectionHeader.roughness': 'Roughness',
|
|
27
|
+
'sidebar.sectionHeader.sloppiness': 'Sloppiness',
|
|
28
|
+
'sidebar.sectionHeader.taper': 'Taper',
|
|
29
|
+
'menu.canvasBackground': 'Canvas background',
|
|
30
|
+
'menu.canvasProperties': 'Canvas properties',
|
|
31
|
+
'menu.exportImage': 'Export as image',
|
|
32
|
+
'menu.findText': 'Find text',
|
|
33
|
+
'menu.quickSave': 'Quick save',
|
|
34
|
+
'menu.resetCanvas': 'Reset canvas',
|
|
35
|
+
'menu.saveShare': 'Save & share',
|
|
36
|
+
'menu.showGrid': 'Show grid',
|
|
37
|
+
'menu.signIn': 'Sign in',
|
|
38
|
+
'menu.signOut': 'Sign out',
|
|
39
|
+
'prefs.toolLock': 'Tool lock',
|
|
40
|
+
'prefs.toggleGrid': 'Toggle grid',
|
|
41
|
+
'prefs.snapObjects': 'Snap to objects',
|
|
42
|
+
'prefs.snapMidpoints': 'Snap to midpoints',
|
|
43
|
+
'prefs.zenMode': 'Zen mode',
|
|
44
|
+
'prefs.viewMode': 'View mode',
|
|
45
|
+
'prefs.canvasShapeProps': 'Canvas & shape properties',
|
|
46
|
+
'prefs.arrowBinding': 'Arrow binding',
|
|
47
|
+
'prefs.language': 'Language',
|
|
48
|
+
'sidebar.bringForward': 'Bring forward',
|
|
49
|
+
'sidebar.bringToFront': 'Bring to front',
|
|
50
|
+
'sidebar.sendBackward': 'Send backward',
|
|
51
|
+
'sidebar.sendToBack': 'Send to back',
|
|
52
|
+
'sidebar.fillColor': 'Fill color',
|
|
53
|
+
'sidebar.strokeColor': 'Stroke color',
|
|
54
|
+
'sidebar.codeMode': 'Code mode',
|
|
55
|
+
'sidebar.renderText': 'Render text',
|
|
56
|
+
'sidebar.newLine': 'New line',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function camelToWords(s) {
|
|
60
|
+
return s.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/-/g, ' ');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function autoFormat(key) {
|
|
64
|
+
const last = key.split('.').pop() || key;
|
|
65
|
+
const words = camelToWords(last);
|
|
66
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function tImpl(key, fallbackOrOpts) {
|
|
70
|
+
if (typeof key !== 'string') return fallbackOrOpts ?? '';
|
|
71
|
+
// The product calls t() with various signatures: (key), (key, fallback),
|
|
72
|
+
// (key, { defaultValue }). Handle all of them.
|
|
73
|
+
let fallback = '';
|
|
74
|
+
if (typeof fallbackOrOpts === 'string') {
|
|
75
|
+
fallback = fallbackOrOpts;
|
|
76
|
+
} else if (fallbackOrOpts && typeof fallbackOrOpts === 'object') {
|
|
77
|
+
fallback = fallbackOrOpts.defaultValue || '';
|
|
78
|
+
}
|
|
79
|
+
if (fallback) return fallback;
|
|
80
|
+
if (OVERRIDES[key]) return OVERRIDES[key];
|
|
81
|
+
return autoFormat(key);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function useTranslation() {
|
|
85
|
+
return {
|
|
86
|
+
t: tImpl,
|
|
87
|
+
locale: 'en',
|
|
88
|
+
setLocale: () => {},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default useTranslation;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @elixpo/lixsketch/react — React component bindings for the LixSketch
|
|
3
|
+
* engine. Mountable in any React app (e.g. blogs.elixpo) without running a
|
|
4
|
+
* second server.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* import { LixSketchCanvas } from '@elixpo/lixsketch/react';
|
|
8
|
+
* import '@elixpo/lixsketch/react/styles';
|
|
9
|
+
*
|
|
10
|
+
* <LixSketchCanvas
|
|
11
|
+
* initialScene={scene}
|
|
12
|
+
* onSceneChange={(scene, metadata) => save(scene, metadata)}
|
|
13
|
+
* onUploadImage={async (dataUrl) => {
|
|
14
|
+
* const r = await fetch('/api/media/upload', { … });
|
|
15
|
+
* return { url: r.url, sizeBytes: r.sizeBytes };
|
|
16
|
+
* }}
|
|
17
|
+
* onExit={() => router.back()}
|
|
18
|
+
* />
|
|
19
|
+
*
|
|
20
|
+
* The component is offline-first: it does not touch any cloud sync, AI, or
|
|
21
|
+
* auth code paths. The host owns persistence and image upload via the two
|
|
22
|
+
* callbacks above.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export { default as LixSketchCanvas } from './LixSketchCanvas.jsx';
|
|
26
|
+
|
|
27
|
+
// Re-exports — we import each from its specific source file rather than
|
|
28
|
+
// going through `../index.js`, which would pull SceneSerializer.js (and
|
|
29
|
+
// the entire shape graph that references engine globals) into the entry
|
|
30
|
+
// chunk. The engine entry stays available for hosts that explicitly want
|
|
31
|
+
// raw scene IO (`import { saveScene } from '@elixpo/lixsketch'`).
|
|
32
|
+
export { TOOLS } from './toolsEnum.js';
|
|
33
|
+
export { compressImage } from '../utils/imageCompressor.js';
|
|
34
|
+
export {
|
|
35
|
+
ALLOWED_IMAGE_MIME_TYPES,
|
|
36
|
+
IMAGE_ACCEPT_ATTR,
|
|
37
|
+
ALLOWED_IMAGE_EXTENSIONS,
|
|
38
|
+
isAllowedImage,
|
|
39
|
+
isAllowedImageDataUrl,
|
|
40
|
+
} from '../utils/allowedImageTypes.js';
|
|
41
|
+
export { installEngineShortcuts, SHORTCUT_MAP } from '../EngineShortcuts.js';
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
|
|
3
|
+
// Tool enum replaces 15 boolean flags
|
|
4
|
+
export const TOOLS = {
|
|
5
|
+
SELECT: 'select',
|
|
6
|
+
PAN: 'pan',
|
|
7
|
+
RECTANGLE: 'rectangle',
|
|
8
|
+
CIRCLE: 'circle',
|
|
9
|
+
LINE: 'line',
|
|
10
|
+
ARROW: 'arrow',
|
|
11
|
+
FREEHAND: 'freehand',
|
|
12
|
+
TEXT: 'text',
|
|
13
|
+
CODE: 'code',
|
|
14
|
+
ERASER: 'eraser',
|
|
15
|
+
LASER: 'laser',
|
|
16
|
+
IMAGE: 'image',
|
|
17
|
+
FRAME: 'frame',
|
|
18
|
+
ICON: 'icon',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Action types for undo/redo
|
|
22
|
+
export const ACTION = {
|
|
23
|
+
CREATE: 'create',
|
|
24
|
+
DELETE: 'delete',
|
|
25
|
+
MODIFY: 'modify',
|
|
26
|
+
PASTE: 'paste',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Tool -> cursor mapping
|
|
30
|
+
const TOOL_CURSORS = {
|
|
31
|
+
[TOOLS.SELECT]: 'default',
|
|
32
|
+
[TOOLS.PAN]: 'grab',
|
|
33
|
+
[TOOLS.RECTANGLE]: 'crosshair',
|
|
34
|
+
[TOOLS.CIRCLE]: 'crosshair',
|
|
35
|
+
[TOOLS.LINE]: 'crosshair',
|
|
36
|
+
[TOOLS.ARROW]: 'crosshair',
|
|
37
|
+
[TOOLS.FREEHAND]: 'crosshair',
|
|
38
|
+
[TOOLS.TEXT]: 'text',
|
|
39
|
+
[TOOLS.CODE]: 'text',
|
|
40
|
+
[TOOLS.LASER]: 'crosshair',
|
|
41
|
+
[TOOLS.IMAGE]: 'crosshair',
|
|
42
|
+
[TOOLS.FRAME]: 'crosshair',
|
|
43
|
+
[TOOLS.ICON]: 'crosshair',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Tool -> sidebar mapping
|
|
47
|
+
const TOOL_SIDEBARS = {
|
|
48
|
+
[TOOLS.FREEHAND]: 'paintbrush',
|
|
49
|
+
[TOOLS.RECTANGLE]: 'rectangle',
|
|
50
|
+
[TOOLS.CIRCLE]: 'circle',
|
|
51
|
+
[TOOLS.LINE]: 'line',
|
|
52
|
+
[TOOLS.ARROW]: 'arrow',
|
|
53
|
+
[TOOLS.TEXT]: 'text',
|
|
54
|
+
[TOOLS.CODE]: 'text',
|
|
55
|
+
[TOOLS.FRAME]: 'frame',
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Keyboard shortcut -> tool mapping
|
|
59
|
+
export const SHORTCUT_MAP = {
|
|
60
|
+
h: TOOLS.PAN,
|
|
61
|
+
v: TOOLS.SELECT,
|
|
62
|
+
1: TOOLS.SELECT,
|
|
63
|
+
r: TOOLS.RECTANGLE,
|
|
64
|
+
2: TOOLS.RECTANGLE,
|
|
65
|
+
o: TOOLS.CIRCLE,
|
|
66
|
+
4: TOOLS.CIRCLE,
|
|
67
|
+
a: TOOLS.ARROW,
|
|
68
|
+
5: TOOLS.ARROW,
|
|
69
|
+
l: TOOLS.LINE,
|
|
70
|
+
6: TOOLS.LINE,
|
|
71
|
+
p: TOOLS.FREEHAND,
|
|
72
|
+
7: TOOLS.FREEHAND,
|
|
73
|
+
t: TOOLS.TEXT,
|
|
74
|
+
8: TOOLS.TEXT,
|
|
75
|
+
9: TOOLS.IMAGE,
|
|
76
|
+
e: TOOLS.ERASER,
|
|
77
|
+
0: TOOLS.ERASER,
|
|
78
|
+
i: TOOLS.ICON,
|
|
79
|
+
f: TOOLS.FRAME,
|
|
80
|
+
k: TOOLS.LASER,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const useSketchStore = create((set, get) => ({
|
|
84
|
+
// --- Active tool (replaces 15 boolean flags) ---
|
|
85
|
+
activeTool: TOOLS.SELECT,
|
|
86
|
+
activeSidebar: null, // which sidebar to show
|
|
87
|
+
selectedShapeSidebar: null, // sidebar shown when a shape is selected (overrides activeSidebar)
|
|
88
|
+
|
|
89
|
+
setActiveTool: (tool, { afterDraw } = {}) => {
|
|
90
|
+
// In view mode, only pan is allowed
|
|
91
|
+
if (get().viewMode && tool !== TOOLS.PAN) return
|
|
92
|
+
// Tool lock: don't switch to SELECT after drawing
|
|
93
|
+
if (afterDraw && tool === TOOLS.SELECT && get().toolLock) return
|
|
94
|
+
set({
|
|
95
|
+
activeTool: tool,
|
|
96
|
+
activeSidebar: TOOL_SIDEBARS[tool] || null,
|
|
97
|
+
selectedShapeSidebar: null,
|
|
98
|
+
})
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
// Called by engine when a shape is selected/deselected
|
|
102
|
+
setSelectedShapeSidebar: (sidebar) => set({ selectedShapeSidebar: sidebar }),
|
|
103
|
+
clearSelectedShapeSidebar: () => set({ selectedShapeSidebar: null }),
|
|
104
|
+
|
|
105
|
+
getCursor: () => {
|
|
106
|
+
const tool = get().activeTool
|
|
107
|
+
if (tool === TOOLS.ERASER) {
|
|
108
|
+
return `url("data:image/svg+xml;base64,${typeof btoa !== 'undefined' ? btoa('<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="10" r="7" fill="#222" stroke="white" stroke-width="2"/></svg>') : ''}") 10 10, auto`
|
|
109
|
+
}
|
|
110
|
+
if (tool === TOOLS.LASER) {
|
|
111
|
+
return `url("data:image/svg+xml;base64,${typeof btoa !== 'undefined' ? btoa('<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1971c2" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>') : ''}") 0 0, auto`
|
|
112
|
+
}
|
|
113
|
+
return TOOL_CURSORS[tool] || 'crosshair'
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// --- Shapes ---
|
|
117
|
+
shapes: [],
|
|
118
|
+
currentShape: null,
|
|
119
|
+
lastMousePos: null,
|
|
120
|
+
|
|
121
|
+
addShape: (shape) =>
|
|
122
|
+
set((s) => ({ shapes: [...s.shapes, shape] })),
|
|
123
|
+
|
|
124
|
+
removeShape: (shape) =>
|
|
125
|
+
set((s) => ({ shapes: s.shapes.filter((sh) => sh !== shape) })),
|
|
126
|
+
|
|
127
|
+
setCurrentShape: (shape) => set({ currentShape: shape }),
|
|
128
|
+
setLastMousePos: (pos) => set({ lastMousePos: pos }),
|
|
129
|
+
|
|
130
|
+
clearShapes: () => set({ shapes: [], currentShape: null }),
|
|
131
|
+
|
|
132
|
+
// --- Undo/Redo ---
|
|
133
|
+
undoStack: [],
|
|
134
|
+
redoStack: [],
|
|
135
|
+
|
|
136
|
+
pushUndo: (action) =>
|
|
137
|
+
set((s) => ({
|
|
138
|
+
undoStack: [...s.undoStack, action],
|
|
139
|
+
redoStack: [], // clear redo on new action
|
|
140
|
+
})),
|
|
141
|
+
|
|
142
|
+
popUndo: () => {
|
|
143
|
+
const { undoStack } = get()
|
|
144
|
+
if (undoStack.length === 0) return null
|
|
145
|
+
const action = undoStack[undoStack.length - 1]
|
|
146
|
+
set((s) => ({
|
|
147
|
+
undoStack: s.undoStack.slice(0, -1),
|
|
148
|
+
redoStack: [...s.redoStack, action],
|
|
149
|
+
}))
|
|
150
|
+
return action
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
popRedo: () => {
|
|
154
|
+
const { redoStack } = get()
|
|
155
|
+
if (redoStack.length === 0) return null
|
|
156
|
+
const action = redoStack[redoStack.length - 1]
|
|
157
|
+
set((s) => ({
|
|
158
|
+
redoStack: s.redoStack.slice(0, -1),
|
|
159
|
+
undoStack: [...s.undoStack, action],
|
|
160
|
+
}))
|
|
161
|
+
return action
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
clearHistory: () => set({ undoStack: [], redoStack: [] }),
|
|
165
|
+
|
|
166
|
+
// --- Viewport / Zoom ---
|
|
167
|
+
zoom: 1,
|
|
168
|
+
viewBox: { x: 0, y: 0, width: 0, height: 0 },
|
|
169
|
+
translation: { x: 0, y: 0 },
|
|
170
|
+
isPanning: false,
|
|
171
|
+
panStart: null,
|
|
172
|
+
|
|
173
|
+
setZoom: (zoom) => set({ zoom: Math.max(0.4, Math.min(30, zoom)) }),
|
|
174
|
+
setViewBox: (vb) => set({ viewBox: vb }),
|
|
175
|
+
setTranslation: (t) => set({ translation: t }),
|
|
176
|
+
setIsPanning: (v) => set({ isPanning: v }),
|
|
177
|
+
setPanStart: (p) => set({ panStart: p }),
|
|
178
|
+
|
|
179
|
+
// --- Canvas background ---
|
|
180
|
+
canvasBackground: 'var(--lixsketch-bg, #ffffff)',
|
|
181
|
+
setCanvasBackground: (color) => set({ canvasBackground: color }),
|
|
182
|
+
|
|
183
|
+
// --- Grid ---
|
|
184
|
+
gridEnabled: false,
|
|
185
|
+
toggleGrid: () => set((s) => ({ gridEnabled: !s.gridEnabled })),
|
|
186
|
+
|
|
187
|
+
// --- Modes ---
|
|
188
|
+
viewMode: false,
|
|
189
|
+
zenMode: false,
|
|
190
|
+
toolLock: false,
|
|
191
|
+
snapToObjects: false,
|
|
192
|
+
|
|
193
|
+
toggleViewMode: () => {
|
|
194
|
+
const entering = !get().viewMode
|
|
195
|
+
if (entering) {
|
|
196
|
+
set({ viewMode: true, zenMode: false, activeTool: TOOLS.PAN, activeSidebar: null, selectedShapeSidebar: null })
|
|
197
|
+
} else {
|
|
198
|
+
set({ viewMode: false, activeTool: TOOLS.SELECT, activeSidebar: null })
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
toggleZenMode: () => set((s) => ({ zenMode: !s.zenMode, viewMode: false })),
|
|
202
|
+
|
|
203
|
+
// --- Canvas/Docs split layout ---
|
|
204
|
+
// 'canvas' = sketch only, 'split' = side-by-side, 'docs' = doc only
|
|
205
|
+
// Hydrated from localStorage on first store consumption (see
|
|
206
|
+
// hydrateLayoutMode below). Persisted on every change.
|
|
207
|
+
layoutMode: 'canvas',
|
|
208
|
+
setLayoutMode: (mode) => {
|
|
209
|
+
if (!['canvas', 'split', 'docs'].includes(mode)) return
|
|
210
|
+
set({ layoutMode: mode })
|
|
211
|
+
if (typeof window !== 'undefined') {
|
|
212
|
+
try { localStorage.setItem('lixsketch-layout-mode', mode) } catch {}
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
hydrateLayoutMode: () => {
|
|
216
|
+
if (typeof window === 'undefined') return
|
|
217
|
+
try {
|
|
218
|
+
const saved = localStorage.getItem('lixsketch-layout-mode')
|
|
219
|
+
if (saved && ['canvas', 'split', 'docs'].includes(saved)) {
|
|
220
|
+
set({ layoutMode: saved })
|
|
221
|
+
}
|
|
222
|
+
} catch {}
|
|
223
|
+
},
|
|
224
|
+
toggleToolLock: () => set((s) => ({ toolLock: !s.toolLock })),
|
|
225
|
+
toggleSnapToObjects: () => set((s) => ({ snapToObjects: !s.snapToObjects })),
|
|
226
|
+
|
|
227
|
+
// --- RoughJS refs (set once after mount) ---
|
|
228
|
+
roughCanvas: null,
|
|
229
|
+
roughGenerator: null,
|
|
230
|
+
setRoughRefs: (canvas, generator) =>
|
|
231
|
+
set({ roughCanvas: canvas, roughGenerator: generator }),
|
|
232
|
+
}))
|
|
233
|
+
|
|
234
|
+
export default useSketchStore
|