@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.
Files changed (218) hide show
  1. package/dist/react/AIRenderer-M4LMJ66J.js +1105 -0
  2. package/dist/react/AIRenderer-M4LMJ66J.js.map +7 -0
  3. package/dist/react/Arrow-BJDAXPMH.js +8 -0
  4. package/dist/react/Arrow-BJDAXPMH.js.map +7 -0
  5. package/dist/react/Circle-KPG7TKIX.js +8 -0
  6. package/dist/react/Circle-KPG7TKIX.js.map +7 -0
  7. package/dist/react/CodeShape-K4DIZAIV.js +8 -0
  8. package/dist/react/CodeShape-K4DIZAIV.js.map +7 -0
  9. package/dist/react/CopyPaste-4CKJRHJ3.js +569 -0
  10. package/dist/react/CopyPaste-4CKJRHJ3.js.map +7 -0
  11. package/dist/react/EraserTrail-PA6IQLQN.js +20 -0
  12. package/dist/react/EraserTrail-PA6IQLQN.js.map +7 -0
  13. package/dist/react/EventDispatcher-SRFH6LQZ.js +463 -0
  14. package/dist/react/EventDispatcher-SRFH6LQZ.js.map +7 -0
  15. package/dist/react/Frame-6UR5XPAX.js +10 -0
  16. package/dist/react/Frame-6UR5XPAX.js.map +7 -0
  17. package/dist/react/FreehandStroke-CK24NUT3.js +8 -0
  18. package/dist/react/FreehandStroke-CK24NUT3.js.map +7 -0
  19. package/dist/react/GraphEngine-QE5B2PMR.js +340 -0
  20. package/dist/react/GraphEngine-QE5B2PMR.js.map +7 -0
  21. package/dist/react/GraphMathParser-2WMTY4IE.js +10 -0
  22. package/dist/react/GraphMathParser-2WMTY4IE.js.map +7 -0
  23. package/dist/react/IconShape-KMIA6T2H.js +10 -0
  24. package/dist/react/IconShape-KMIA6T2H.js.map +7 -0
  25. package/dist/react/ImageShape-CHHGBXUQ.js +10 -0
  26. package/dist/react/ImageShape-CHHGBXUQ.js.map +7 -0
  27. package/dist/react/LayerOrder-XCHF4MSS.js +66 -0
  28. package/dist/react/LayerOrder-XCHF4MSS.js.map +7 -0
  29. package/dist/react/Line-2KOQEEGA.js +10 -0
  30. package/dist/react/Line-2KOQEEGA.js.map +7 -0
  31. package/dist/react/LixScriptParser-JYWJX375.js +1033 -0
  32. package/dist/react/LixScriptParser-JYWJX375.js.map +7 -0
  33. package/dist/react/MermaidFlowchartRenderer-4QBAZ5MB.js +412 -0
  34. package/dist/react/MermaidFlowchartRenderer-4QBAZ5MB.js.map +7 -0
  35. package/dist/react/MermaidSequenceParser-UKG2NSZY.js +8 -0
  36. package/dist/react/MermaidSequenceParser-UKG2NSZY.js.map +7 -0
  37. package/dist/react/MermaidSequenceRenderer-HPLHY5II.js +387 -0
  38. package/dist/react/MermaidSequenceRenderer-HPLHY5II.js.map +7 -0
  39. package/dist/react/Rectangle-6FE7RN6W.js +8 -0
  40. package/dist/react/Rectangle-6FE7RN6W.js.map +7 -0
  41. package/dist/react/ResizeCode-WAMDTKSW.js +2 -0
  42. package/dist/react/ResizeCode-WAMDTKSW.js.map +7 -0
  43. package/dist/react/ResizeShapes-LKEQTITP.js +250 -0
  44. package/dist/react/ResizeShapes-LKEQTITP.js.map +7 -0
  45. package/dist/react/SceneSerializer-4U4B24JM.js +739 -0
  46. package/dist/react/SceneSerializer-4U4B24JM.js.map +7 -0
  47. package/dist/react/Selection-VVUHY5YJ.js +34 -0
  48. package/dist/react/Selection-VVUHY5YJ.js.map +7 -0
  49. package/dist/react/SketchEngine-YA4RG3KX.js +413 -0
  50. package/dist/react/SketchEngine-YA4RG3KX.js.map +7 -0
  51. package/dist/react/TextShape-54ZJ66E3.js +8 -0
  52. package/dist/react/TextShape-54ZJ66E3.js.map +7 -0
  53. package/dist/react/UndoRedo-M2JZUBKN.js +30 -0
  54. package/dist/react/UndoRedo-M2JZUBKN.js.map +7 -0
  55. package/dist/react/ZoomPan-U3TXC5AJ.js +186 -0
  56. package/dist/react/ZoomPan-U3TXC5AJ.js.map +7 -0
  57. package/dist/react/arrowTool-CWEMIRXK.js +17 -0
  58. package/dist/react/arrowTool-CWEMIRXK.js.map +7 -0
  59. package/dist/react/boxicons-5JNJQPUP.woff +0 -0
  60. package/dist/react/boxicons-6NVWQSCQ.eot +0 -0
  61. package/dist/react/boxicons-AQ5N6WP7.woff2 +0 -0
  62. package/dist/react/boxicons-GDR4JU3X.ttf +0 -0
  63. package/dist/react/boxicons-NNFHLMOR.svg +1660 -0
  64. package/dist/react/chunk-2OQXI7QB.js +564 -0
  65. package/dist/react/chunk-2OQXI7QB.js.map +7 -0
  66. package/dist/react/chunk-5OUBTHHO.js +187 -0
  67. package/dist/react/chunk-5OUBTHHO.js.map +7 -0
  68. package/dist/react/chunk-6PFXZKL2.js +628 -0
  69. package/dist/react/chunk-6PFXZKL2.js.map +7 -0
  70. package/dist/react/chunk-6SFNWGGZ.js +453 -0
  71. package/dist/react/chunk-6SFNWGGZ.js.map +7 -0
  72. package/dist/react/chunk-7TZSIR6V.js +210 -0
  73. package/dist/react/chunk-7TZSIR6V.js.map +7 -0
  74. package/dist/react/chunk-AJAZC37E.js +662 -0
  75. package/dist/react/chunk-AJAZC37E.js.map +7 -0
  76. package/dist/react/chunk-BU4D2WYQ.js +1213 -0
  77. package/dist/react/chunk-BU4D2WYQ.js.map +7 -0
  78. package/dist/react/chunk-BZE4CEBG.js +747 -0
  79. package/dist/react/chunk-BZE4CEBG.js.map +7 -0
  80. package/dist/react/chunk-CB2RANCB.js +212 -0
  81. package/dist/react/chunk-CB2RANCB.js.map +7 -0
  82. package/dist/react/chunk-DNAG7ML6.js +128 -0
  83. package/dist/react/chunk-DNAG7ML6.js.map +7 -0
  84. package/dist/react/chunk-HZWLPXFN.js +198 -0
  85. package/dist/react/chunk-HZWLPXFN.js.map +7 -0
  86. package/dist/react/chunk-I6HCCIXB.js +572 -0
  87. package/dist/react/chunk-I6HCCIXB.js.map +7 -0
  88. package/dist/react/chunk-IZ5OGWGW.js +235 -0
  89. package/dist/react/chunk-IZ5OGWGW.js.map +7 -0
  90. package/dist/react/chunk-JJCJNLNN.js +1490 -0
  91. package/dist/react/chunk-JJCJNLNN.js.map +7 -0
  92. package/dist/react/chunk-JQ3IRFDP.js +222 -0
  93. package/dist/react/chunk-JQ3IRFDP.js.map +7 -0
  94. package/dist/react/chunk-L5OARZJ7.js +1134 -0
  95. package/dist/react/chunk-L5OARZJ7.js.map +7 -0
  96. package/dist/react/chunk-LHRTXNVE.js +1600 -0
  97. package/dist/react/chunk-LHRTXNVE.js.map +7 -0
  98. package/dist/react/chunk-Q2GA4MBZ.js +1653 -0
  99. package/dist/react/chunk-Q2GA4MBZ.js.map +7 -0
  100. package/dist/react/chunk-QNSE5BAY.js +519 -0
  101. package/dist/react/chunk-QNSE5BAY.js.map +7 -0
  102. package/dist/react/chunk-QXFBG7OM.js +191 -0
  103. package/dist/react/chunk-QXFBG7OM.js.map +7 -0
  104. package/dist/react/chunk-T6ZVV5ZO.js +714 -0
  105. package/dist/react/chunk-T6ZVV5ZO.js.map +7 -0
  106. package/dist/react/chunk-URB6MY46.js +974 -0
  107. package/dist/react/chunk-URB6MY46.js.map +7 -0
  108. package/dist/react/chunk-VN62KLOC.js +931 -0
  109. package/dist/react/chunk-VN62KLOC.js.map +7 -0
  110. package/dist/react/chunk-VQVEPJSA.js +158 -0
  111. package/dist/react/chunk-VQVEPJSA.js.map +7 -0
  112. package/dist/react/chunk-XISEAGBV.js +1632 -0
  113. package/dist/react/chunk-XISEAGBV.js.map +7 -0
  114. package/dist/react/chunk-YKJUBFHE.js +575 -0
  115. package/dist/react/chunk-YKJUBFHE.js.map +7 -0
  116. package/dist/react/circleTool-NUATSEI2.js +14 -0
  117. package/dist/react/circleTool-NUATSEI2.js.map +7 -0
  118. package/dist/react/codeTool-MHCB2FNB.js +38 -0
  119. package/dist/react/codeTool-MHCB2FNB.js.map +7 -0
  120. package/dist/react/eraserTool-2GS2LQCS.js +103 -0
  121. package/dist/react/eraserTool-2GS2LQCS.js.map +7 -0
  122. package/dist/react/frameTool-PWVKKXSP.js +14 -0
  123. package/dist/react/frameTool-PWVKKXSP.js.map +7 -0
  124. package/dist/react/freehandTool-SLYGASVP.js +14 -0
  125. package/dist/react/freehandTool-SLYGASVP.js.map +7 -0
  126. package/dist/react/iconTool-GPZ7O7NH.js +18 -0
  127. package/dist/react/iconTool-GPZ7O7NH.js.map +7 -0
  128. package/dist/react/imageTool-D5R4XGEU.js +15 -0
  129. package/dist/react/imageTool-D5R4XGEU.js.map +7 -0
  130. package/dist/react/index.js +5349 -0
  131. package/dist/react/index.js.map +7 -0
  132. package/dist/react/laserTool-XVMV63AV.js +239 -0
  133. package/dist/react/laserTool-XVMV63AV.js.map +7 -0
  134. package/dist/react/lineTool-WNI5EMZ5.js +14 -0
  135. package/dist/react/lineTool-WNI5EMZ5.js.map +7 -0
  136. package/dist/react/lixCode-OJMVUXXI.ttf +0 -0
  137. package/dist/react/lixDefault-JEAPAUJL.ttf +0 -0
  138. package/dist/react/lixFancy-BPXYM5CQ.ttf +0 -0
  139. package/dist/react/lixFont-CPKEUDVM.woff2 +0 -0
  140. package/dist/react/rectangleTool-U7DZ6ZTA.js +14 -0
  141. package/dist/react/rectangleTool-U7DZ6ZTA.js.map +7 -0
  142. package/dist/react/styles.css +6627 -0
  143. package/dist/react/textTool-M7S6F3VC.js +21 -0
  144. package/dist/react/textTool-M7S6F3VC.js.map +7 -0
  145. package/package.json +39 -5
  146. package/src/EngineShortcuts.js +83 -27
  147. package/src/core/AIRenderer.js +30 -18
  148. package/src/core/GraphEngine.js +64 -41
  149. package/src/core/MermaidFlowchartRenderer.js +153 -88
  150. package/src/core/MermaidSequenceRenderer.js +168 -93
  151. package/src/core/SceneSerializer.js +86 -2
  152. package/src/core/Selection.js +119 -19
  153. package/src/core/UndoRedo.js +82 -15
  154. package/src/core/ZoomPan.js +15 -5
  155. package/src/index.js +14 -1
  156. package/src/react/LixSketchCanvas.jsx +347 -0
  157. package/src/react/components/AppMenu.jsx +482 -0
  158. package/src/react/components/Footer.jsx +85 -0
  159. package/src/react/components/Toolbar.jsx +140 -0
  160. package/src/react/components/canvas/CanvasLoadingOverlay.jsx +76 -0
  161. package/src/react/components/canvas/ContextMenu.jsx +466 -0
  162. package/src/react/components/canvas/FindBar.jsx +177 -0
  163. package/src/react/components/canvas/ImageSourcePicker.jsx +133 -0
  164. package/src/react/components/canvas/MultiSelectActions.jsx +120 -0
  165. package/src/react/components/canvas/SVGCanvas.jsx +145 -0
  166. package/src/react/components/canvas/SaveStatusIndicator.jsx +29 -0
  167. package/src/react/components/modals/CanvasPropertiesModal.jsx +358 -0
  168. package/src/react/components/modals/CommandPalette.jsx +307 -0
  169. package/src/react/components/modals/ExportImageModal.jsx +287 -0
  170. package/src/react/components/modals/GraphModal.jsx +174 -0
  171. package/src/react/components/modals/HelpModal.jsx +208 -0
  172. package/src/react/components/modals/LixScriptModal.jsx +130 -0
  173. package/src/react/components/modals/ShortcutsModal.jsx +125 -0
  174. package/src/react/components/sidebars/ArrowSidebar.jsx +173 -0
  175. package/src/react/components/sidebars/CircleSidebar.jsx +134 -0
  176. package/src/react/components/sidebars/FrameSidebar.jsx +263 -0
  177. package/src/react/components/sidebars/IconSidebar.jsx +279 -0
  178. package/src/react/components/sidebars/ImageSidebar.jsx +69 -0
  179. package/src/react/components/sidebars/LineSidebar.jsx +109 -0
  180. package/src/react/components/sidebars/PaintbrushSidebar.jsx +119 -0
  181. package/src/react/components/sidebars/RectangleSidebar.jsx +165 -0
  182. package/src/react/components/sidebars/ShapeSidebar.jsx +130 -0
  183. package/src/react/components/sidebars/TextSidebar.jsx +174 -0
  184. package/src/react/hooks/inertStores.js +58 -0
  185. package/src/react/hooks/useSketchEngine.js +76 -0
  186. package/src/react/hooks/useTranslation.js +92 -0
  187. package/src/react/index.js +41 -0
  188. package/src/react/store/useSketchStore.js +234 -0
  189. package/src/react/store/useUIStore.js +196 -0
  190. package/src/react/styles/arrowSidebar.css +263 -0
  191. package/src/react/styles/canvasStroke.css +47 -0
  192. package/src/react/styles/canvasTools.css +769 -0
  193. package/src/react/styles/circleSidebar.css +206 -0
  194. package/src/react/styles/fonts/fonts.css +29 -0
  195. package/src/react/styles/fonts/lixCode.ttf +0 -0
  196. package/src/react/styles/fonts/lixDefault.ttf +0 -0
  197. package/src/react/styles/fonts/lixDocs.ttf +0 -0
  198. package/src/react/styles/fonts/lixFancy.ttf +0 -0
  199. package/src/react/styles/fonts/lixFont.woff2 +0 -0
  200. package/src/react/styles/lineSidebar.css +207 -0
  201. package/src/react/styles/paintbrushSidebar.css +247 -0
  202. package/src/react/styles/squareSidebar.css +206 -0
  203. package/src/react/styles/writeText.css +149 -0
  204. package/src/react/styles.css +175 -0
  205. package/src/react/toolsEnum.js +20 -0
  206. package/src/react/utils/toast.js +18 -0
  207. package/src/shapes/Frame.js +7 -1
  208. package/src/shapes/Line.js +11 -5
  209. package/src/tools/arrowTool.js +30 -4
  210. package/src/tools/circleTool.js +33 -9
  211. package/src/tools/codeTool.js +5 -3
  212. package/src/tools/frameTool.js +47 -7
  213. package/src/tools/freehandTool.js +12 -4
  214. package/src/tools/iconTool.js +7 -1
  215. package/src/tools/imageTool.js +7 -1
  216. package/src/tools/lineTool.js +27 -3
  217. package/src/tools/rectangleTool.js +45 -8
  218. package/src/tools/textTool.js +23 -6
@@ -0,0 +1,130 @@
1
+ "use client"
2
+
3
+ import { useCallback, useEffect, useRef, useState } from 'react'
4
+ import useUIStore from '../../store/useUIStore'
5
+
6
+ const SAMPLE = `# LixScript — paste shapes / arrows in a simple DSL.
7
+ # Hit Cmd/Ctrl+Enter to render onto the canvas.
8
+
9
+ rect A "Idea" 100 100 220 90
10
+ rect B "Plan" 400 100 220 90
11
+ rect C "Ship" 700 100 220 90
12
+
13
+ arrow A -> B
14
+ arrow B -> C
15
+ `
16
+
17
+ /**
18
+ * LixScript modal — takes a script the user types and parses it directly
19
+ * into engine shapes via @elixpo/lixsketch's LixScriptParser. No AI
20
+ * inference, no worker round-trip. Strictly client-side parsing.
21
+ */
22
+ export default function LixScriptModal() {
23
+ const open = useUIStore((s) => s.aiModalOpen)
24
+ const toggle = useUIStore((s) => s.toggleAIModal)
25
+ const [source, setSource] = useState(SAMPLE)
26
+ const [busy, setBusy] = useState(false)
27
+ const [status, setStatus] = useState(null) // { tone, message }
28
+ const taRef = useRef(null)
29
+
30
+ useEffect(() => {
31
+ if (!open) return
32
+ requestAnimationFrame(() => taRef.current?.focus())
33
+ const onKey = (e) => {
34
+ if (e.key === 'Escape') toggle()
35
+ }
36
+ document.addEventListener('keydown', onKey)
37
+ return () => document.removeEventListener('keydown', onKey)
38
+ }, [open, toggle])
39
+
40
+ const render = useCallback(async () => {
41
+ if (busy) return
42
+ setBusy(true)
43
+ setStatus(null)
44
+ try {
45
+ // Lazy-import the parser. Pulling it eagerly would bring the whole
46
+ // shape graph into the entry chunk (see SceneSerializer comment
47
+ // in LixSketchCanvas) and crash with "svg is not defined" before
48
+ // engine.init() has wired the engine globals.
49
+ const mod = await import('../../../core/LixScriptParser.js')
50
+ const parsed = mod.parseLixScript(source)
51
+ const resolved = mod.resolveShapeRefs ? mod.resolveShapeRefs(parsed) : parsed
52
+ mod.renderLixScript(resolved)
53
+ setStatus({ tone: 'success', message: `Rendered ${Array.isArray(resolved) ? resolved.length : '?'} elements.` })
54
+ // Auto-close shortly after a successful render.
55
+ setTimeout(() => { toggle() }, 700)
56
+ } catch (err) {
57
+ console.warn('[LixScriptModal] parse failed:', err)
58
+ setStatus({ tone: 'error', message: err?.message || 'Parse failed.' })
59
+ } finally {
60
+ setBusy(false)
61
+ }
62
+ }, [source, busy, toggle])
63
+
64
+ const handleKeyDown = useCallback((e) => {
65
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
66
+ e.preventDefault()
67
+ render()
68
+ }
69
+ }, [render])
70
+
71
+ if (!open) return null
72
+
73
+ return (
74
+ <div
75
+ className="fixed inset-0 z-[1100] flex items-center justify-center bg-black/60 backdrop-blur-sm font-[lixFont]"
76
+ onClick={toggle}
77
+ role="dialog"
78
+ aria-modal="true"
79
+ >
80
+ <div
81
+ onClick={(e) => e.stopPropagation()}
82
+ className="relative w-[560px] max-w-[94vw] bg-surface border border-border-light rounded-2xl p-5 shadow-2xl flex flex-col gap-3"
83
+ >
84
+ <button
85
+ onClick={toggle}
86
+ className="absolute top-3 right-3 w-7 h-7 flex items-center justify-center rounded-md text-text-muted hover:text-text-primary hover:bg-surface-hover transition-colors"
87
+ title="Close (Esc)"
88
+ >
89
+ <i className="bx bx-x text-lg" />
90
+ </button>
91
+
92
+ <div className="flex items-center gap-2">
93
+ <div className="w-8 h-8 rounded-lg bg-accent-blue/10 border border-accent-blue/30 flex items-center justify-center">
94
+ <i className="bx bx-code-alt text-accent-blue" />
95
+ </div>
96
+ <div className="flex-1">
97
+ <h2 className="text-text-primary text-sm font-medium">LixScript</h2>
98
+ <p className="text-text-dim text-[11px]">Type shapes + arrows, render directly. No AI involved.</p>
99
+ </div>
100
+ </div>
101
+
102
+ <textarea
103
+ ref={taRef}
104
+ value={source}
105
+ onChange={(e) => setSource(e.target.value)}
106
+ onKeyDown={handleKeyDown}
107
+ spellCheck={false}
108
+ className="w-full h-[260px] resize-none bg-surface-card border border-border-light rounded-lg px-3 py-2 text-[12px] text-text-primary font-[lixCode] outline-none focus:border-accent-blue/50"
109
+ />
110
+
111
+ <div className="flex items-center justify-between">
112
+ {status ? (
113
+ <span className={`text-[11px] ${status.tone === 'error' ? 'text-red-400' : 'text-green-400'}`}>
114
+ {status.message}
115
+ </span>
116
+ ) : (
117
+ <span className="text-text-dim text-[11px]">Cmd/Ctrl+Enter to render · Esc to close</span>
118
+ )}
119
+ <button
120
+ onClick={render}
121
+ disabled={busy}
122
+ className="px-3 py-1.5 rounded-lg bg-accent-blue text-text-primary text-[12px] font-medium hover:bg-accent-blue-hover transition-colors disabled:opacity-50 disabled:pointer-events-none"
123
+ >
124
+ {busy ? 'Rendering…' : 'Render'}
125
+ </button>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ )
130
+ }
@@ -0,0 +1,125 @@
1
+ "use client"
2
+
3
+ import useUIStore from '../../store/useUIStore'
4
+ import { useTranslation } from '../../hooks/useTranslation'
5
+ import { useMemo } from 'react'
6
+
7
+ function ShortcutRow({ keys, action }) {
8
+ return (
9
+ <div className="flex items-center justify-between py-1.5">
10
+ <span className="text-text-secondary text-xs">{action}</span>
11
+ <div className="flex items-center gap-1">
12
+ {keys.split('+').map((key, i) => (
13
+ <span key={i}>
14
+ {i > 0 && <span className="text-text-dim text-xs mx-0.5">+</span>}
15
+ <kbd className="px-1.5 py-0.5 bg-surface-dark rounded text-text-muted text-xs border border-border font-[lixFont]">
16
+ {key.trim()}
17
+ </kbd>
18
+ </span>
19
+ ))}
20
+ </div>
21
+ </div>
22
+ )
23
+ }
24
+
25
+ function ShortcutSection({ title, shortcuts }) {
26
+ return (
27
+ <div>
28
+ <h3 className="text-text-dim text-xs uppercase tracking-wider mb-2">{title}</h3>
29
+ {shortcuts.map((s) => (
30
+ <ShortcutRow key={s.action} keys={s.keys} action={s.action} />
31
+ ))}
32
+ </div>
33
+ )
34
+ }
35
+
36
+ export default function ShortcutsModal() {
37
+ const { t } = useTranslation()
38
+ const shortcutsModalOpen = useUIStore((s) => s.shortcutsModalOpen)
39
+ const toggleShortcutsModal = useUIStore((s) => s.toggleShortcutsModal)
40
+
41
+ const TOOL_SHORTCUTS = useMemo(() => [
42
+ { keys: 'H', action: t('shortcuts.pan') },
43
+ { keys: 'V / 1', action: t('shortcuts.selection') },
44
+ { keys: 'R / 2', action: t('shortcuts.rectangle') },
45
+ { keys: '3', action: t('shortcuts.diamond') },
46
+ { keys: 'O / 4', action: t('shortcuts.circle') },
47
+ { keys: 'A / 5', action: t('shortcuts.arrow') },
48
+ { keys: 'L / 6', action: t('shortcuts.line') },
49
+ { keys: 'P / 7', action: t('shortcuts.freehand') },
50
+ { keys: 'T / 8', action: t('shortcuts.text') },
51
+ { keys: '9', action: t('shortcuts.image') },
52
+ { keys: 'E / 0', action: t('shortcuts.eraser') },
53
+ { keys: 'F', action: t('shortcuts.frame') },
54
+ { keys: 'K', action: t('shortcuts.laser') },
55
+ ], [t])
56
+
57
+ const ACTION_SHORTCUTS = useMemo(() => [
58
+ { keys: 'Ctrl+A', action: t('shortcuts.selectAll') },
59
+ { keys: 'Ctrl+G', action: t('shortcuts.group') },
60
+ { keys: 'Ctrl+Shift+G', action: t('shortcuts.ungroup') },
61
+ { keys: 'Ctrl+D', action: t('shortcuts.duplicate') },
62
+ { keys: 'Ctrl+S', action: t('shortcuts.quickSave') },
63
+ { keys: 'Ctrl+Shift+S', action: t('shortcuts.saveShare') },
64
+ { keys: 'Ctrl+F', action: t('shortcuts.findOnCanvas') },
65
+ { keys: 'Ctrl+C', action: t('shortcuts.copy') },
66
+ { keys: 'Ctrl+V', action: t('shortcuts.paste') },
67
+ { keys: 'Ctrl+Z', action: t('shortcuts.undo') },
68
+ { keys: 'Ctrl+Shift+Z', action: t('shortcuts.redo') },
69
+ { keys: 'Esc', action: t('shortcuts.deselect') },
70
+ { keys: 'Del', action: t('shortcuts.delete') },
71
+ { keys: 'Space', action: t('shortcuts.holdToPan') },
72
+ { keys: 'Shift', action: t('shortcuts.straightDraw') },
73
+ ], [t])
74
+
75
+ const VIEW_SHORTCUTS = useMemo(() => [
76
+ { keys: 'Ctrl++', action: t('shortcuts.zoomIn') },
77
+ { keys: 'Ctrl+-', action: t('shortcuts.zoomOut') },
78
+ { keys: 'Ctrl+0', action: t('shortcuts.resetZoom') },
79
+ { keys: "Ctrl+'", action: t('shortcuts.toggleGrid') },
80
+ { keys: 'Ctrl+/', action: t('shortcuts.shortcutsHelp') },
81
+ ], [t])
82
+
83
+ if (!shortcutsModalOpen) return null
84
+
85
+ return (
86
+ <div
87
+ className="fixed inset-0 z-[9999] flex items-center justify-center font-[lixFont]"
88
+ onClick={toggleShortcutsModal}
89
+ >
90
+ {/* Backdrop */}
91
+ <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
92
+
93
+ {/* Modal */}
94
+ <div
95
+ className="relative bg-surface-card border border-border-light rounded-2xl p-6 max-w-[600px] w-full mx-4 max-h-[80vh] overflow-y-auto no-scrollbar"
96
+ onClick={(e) => e.stopPropagation()}
97
+ >
98
+ {/* Header */}
99
+ <div className="flex items-center justify-between mb-5">
100
+ <h2 className="text-text-primary text-base font-medium">
101
+ {t('shortcuts.title')}
102
+ </h2>
103
+ <button
104
+ onClick={toggleShortcutsModal}
105
+ className="w-7 h-7 flex items-center justify-center rounded-lg text-text-muted hover:text-text-primary hover:bg-surface-hover transition-all duration-200"
106
+ >
107
+ <i className="bx bx-x text-xl" />
108
+ </button>
109
+ </div>
110
+
111
+ {/* Two columns */}
112
+ <div className="grid grid-cols-2 gap-6">
113
+ {/* Left column - Tools */}
114
+ <ShortcutSection title={t('shortcuts.tools')} shortcuts={TOOL_SHORTCUTS} />
115
+
116
+ {/* Right column - Actions + View */}
117
+ <div className="flex flex-col gap-4">
118
+ <ShortcutSection title={t('shortcuts.actions')} shortcuts={ACTION_SHORTCUTS} />
119
+ <ShortcutSection title={t('shortcuts.view')} shortcuts={VIEW_SHORTCUTS} />
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ )
125
+ }
@@ -0,0 +1,173 @@
1
+ "use client"
2
+
3
+ import useSketchStore, { TOOLS } from '../../store/useSketchStore'
4
+ import ShapeSidebar, { ToolbarButton, Divider, LayerControls } from './ShapeSidebar'
5
+ import { useState, useCallback } from 'react'
6
+ import { useTranslation } from '../../hooks/useTranslation'
7
+
8
+ const STROKE_COLORS = ['#1a1a20', '#ffffff', '#FF8383', '#3A994C', '#56A2E8', '#FFD700', '#FF69B4', '#A855F7']
9
+
10
+ const HEAD_STYLES = [
11
+ { value: 'default', svg: '<svg width="22" height="12" viewBox="0 0 24 14"><line x1="2" y1="7" x2="16" y2="7" stroke="#fff" stroke-width="2"/><polyline points="13,2 19,7 13,12" fill="none" stroke="#fff" stroke-width="2" stroke-linejoin="round"/></svg>' },
12
+ { value: 'square', svg: '<svg width="22" height="12" viewBox="0 0 24 14"><line x1="2" y1="7" x2="14" y2="7" stroke="#fff" stroke-width="2"/><rect x="14" y="3" width="6" height="8" fill="none" stroke="#fff" stroke-width="1.5"/></svg>' },
13
+ { value: 'outline', svg: '<svg width="22" height="12" viewBox="0 0 24 14"><line x1="2" y1="7" x2="13" y2="7" stroke="#fff" stroke-width="2"/><polygon points="13,2 21,7 13,12" fill="none" stroke="#fff" stroke-width="1.5"/></svg>' },
14
+ { value: 'solid', svg: '<svg width="22" height="12" viewBox="0 0 24 14"><line x1="2" y1="7" x2="13" y2="7" stroke="#fff" stroke-width="2"/><polygon points="13,2 21,7 13,12" fill="#fff"/></svg>' },
15
+ ]
16
+
17
+ function SvgIcon({ svg }) {
18
+ return <span dangerouslySetInnerHTML={{ __html: svg }} />
19
+ }
20
+
21
+ function ColorGrid({ colors, selected, onSelect }) {
22
+ return (
23
+ <div className="grid grid-cols-4 gap-1.5">
24
+ {colors.map((c) => (
25
+ <button key={c} onClick={() => onSelect(c)}
26
+ className={`w-7 h-7 rounded-md border-[1.5px] transition-all duration-100 ${selected === c ? 'border-[#5B57D1] scale-110' : 'border-white/[0.08] hover:border-white/20'}`}
27
+ style={{ backgroundColor: c }}
28
+ />
29
+ ))}
30
+ </div>
31
+ )
32
+ }
33
+
34
+ export default function ArrowSidebar() {
35
+ const { t } = useTranslation()
36
+ const activeTool = useSketchStore((s) => s.activeTool)
37
+ const selectedShapeSidebar = useSketchStore((s) => s.selectedShapeSidebar)
38
+ const [headStyle, setHeadStyle] = useState('default')
39
+ const [strokeColor, setStrokeColor] = useState('#fff')
40
+ const [thickness, setThickness] = useState(2)
41
+ const [outlineStyle, setOutlineStyle] = useState('solid')
42
+ const [arrowType, setArrowType] = useState('straight')
43
+ const [curvature, setCurvature] = useState(20)
44
+
45
+ // Bridge helpers: update React state + engine state + selected arrow
46
+ const updateHead = useCallback((v) => {
47
+ setHeadStyle(v)
48
+ if (window.arrowToolSettings) window.arrowToolSettings.headStyle = v
49
+ if (window.updateSelectedArrowStyle) window.updateSelectedArrowStyle({ arrowHeadStyle: v })
50
+ }, [])
51
+
52
+ const updateStroke = useCallback((v) => {
53
+ setStrokeColor(v)
54
+ if (window.arrowToolSettings) window.arrowToolSettings.strokeColor = v
55
+ if (window.updateSelectedArrowStyle) window.updateSelectedArrowStyle({ stroke: v })
56
+ }, [])
57
+
58
+ const updateThickness = useCallback((v) => {
59
+ setThickness(v)
60
+ if (window.arrowToolSettings) window.arrowToolSettings.strokeWidth = v
61
+ if (window.updateSelectedArrowStyle) window.updateSelectedArrowStyle({ strokeWidth: v })
62
+ }, [])
63
+
64
+ const updateOutline = useCallback((v) => {
65
+ setOutlineStyle(v)
66
+ if (window.arrowToolSettings) window.arrowToolSettings.outlineStyle = v
67
+ if (window.updateSelectedArrowStyle) window.updateSelectedArrowStyle({ arrowOutlineStyle: v })
68
+ }, [])
69
+
70
+ const updateType = useCallback((v) => {
71
+ setArrowType(v)
72
+ if (window.arrowToolSettings) window.arrowToolSettings.arrowCurved = v
73
+ if (window.updateSelectedArrowStyle) window.updateSelectedArrowStyle({ arrowCurved: v })
74
+ }, [])
75
+
76
+ const updateCurvature = useCallback((v) => {
77
+ setCurvature(v)
78
+ if (window.arrowToolSettings) window.arrowToolSettings.curveAmount = v
79
+ if (window.updateSelectedArrowStyle) window.updateSelectedArrowStyle({ arrowCurveAmount: v })
80
+ }, [])
81
+
82
+ return (
83
+ <ShapeSidebar visible={activeTool === TOOLS.ARROW || selectedShapeSidebar === 'arrow'}>
84
+ {/* Head style */}
85
+ <ToolbarButton icon="bxs-right-arrow" tooltip="Arrow head">
86
+ <p className="text-xs text-text-secondary uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.head')}</p>
87
+ <div className="flex items-center gap-1">
88
+ {HEAD_STYLES.map((h) => (
89
+ <button key={h.value} onClick={() => updateHead(h.value)}
90
+ className={`w-10 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${headStyle === h.value ? 'bg-[#5B57D1]/20 text-[#5B57D1]' : 'text-text-secondary hover:bg-white/[0.06]'}`}
91
+ >
92
+ <SvgIcon svg={h.svg} />
93
+ </button>
94
+ ))}
95
+ </div>
96
+ </ToolbarButton>
97
+
98
+ <Divider />
99
+
100
+ <ToolbarButton tooltip={t('sidebar.strokeColor')}
101
+ preview={<span className="w-4 h-4 rounded-md border border-white/20" style={{ backgroundColor: strokeColor }} />}
102
+ >
103
+ <p className="text-xs text-text-secondary uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.stroke')}</p>
104
+ <ColorGrid colors={STROKE_COLORS} selected={strokeColor} onSelect={updateStroke} />
105
+ </ToolbarButton>
106
+
107
+ <Divider />
108
+
109
+ <ToolbarButton icon="bxs-edit-alt" tooltip={t('sidebar.strokeWidth')}>
110
+ <p className="text-xs text-text-secondary uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.width')}</p>
111
+ <div className="flex items-center gap-1">
112
+ {[1, 2, 4, 7].map((w) => (
113
+ <button key={w} onClick={() => updateThickness(w)}
114
+ className={`w-9 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${thickness === w ? 'bg-[#5B57D1]/20 text-[#5B57D1]' : 'text-text-secondary hover:bg-white/[0.06]'}`}
115
+ >
116
+ <div className="w-5 rounded-full bg-current" style={{ height: Math.max(1, w) }} />
117
+ </button>
118
+ ))}
119
+ </div>
120
+ </ToolbarButton>
121
+
122
+ <Divider />
123
+
124
+ <ToolbarButton icon="bxs-minus-circle" tooltip="Stroke style">
125
+ <p className="text-xs text-text-secondary uppercase tracking-wider mb-2">{t('sidebar.style')}</p>
126
+ <div className="flex items-center gap-1">
127
+ {[{ v: 'solid', d: '' }, { v: 'dashed', d: '6 4' }, { v: 'dotted', d: '2 3' }].map((s) => (
128
+ <button key={s.v} onClick={() => updateOutline(s.v)}
129
+ className={`w-11 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${outlineStyle === s.v ? 'bg-[#5B57D1]/20' : 'hover:bg-white/[0.06]'}`}
130
+ >
131
+ <svg width="28" height="4" viewBox="0 0 28 4"><line x1="0" y1="2" x2="28" y2="2" stroke="#fff" strokeWidth="2" strokeDasharray={s.d} strokeLinecap="round" /></svg>
132
+ </button>
133
+ ))}
134
+ </div>
135
+ </ToolbarButton>
136
+
137
+ <Divider />
138
+
139
+ {/* Arrow type */}
140
+ <ToolbarButton icon="bxs-share-alt" tooltip="Arrow type">
141
+ <p className="text-xs text-text-secondary uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.type')}</p>
142
+ <div className="flex flex-col gap-0.5">
143
+ {[
144
+ { v: 'straight', i: 'bxs-right-arrow-alt', l: 'Straight' },
145
+ { v: 'curved', i: 'bxs-analyse', l: 'Curved' },
146
+ { v: 'elbow', i: 'bxs-network-chart', l: 'Elbow' },
147
+ ].map((a) => (
148
+ <button key={a.v} onClick={() => updateType(a.v)}
149
+ className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 ${arrowType === a.v ? 'bg-[#5B57D1] text-white' : 'text-text-secondary hover:bg-white/[0.06]'}`}
150
+ >
151
+ <i className={`bx ${a.i} text-sm`} /> {a.l}
152
+ </button>
153
+ ))}
154
+ {arrowType === 'curved' && (
155
+ <>
156
+ <div className="w-full h-px bg-white/[0.08] my-1" />
157
+ <p className="text-[9px] text-text-dim uppercase tracking-wider mb-1">Curvature</p>
158
+ <div className="flex items-center gap-1">
159
+ {[{ v: 8, l: 'Lo' }, { v: 20, l: 'Md' }, { v: 40, l: 'Hi' }].map((c) => (
160
+ <button key={c.v} onClick={() => updateCurvature(c.v)}
161
+ className={`flex-1 py-1 rounded-md text-xs text-center transition-all duration-100 ${curvature === c.v ? 'bg-[#5B57D1]/20 text-[#5B57D1]' : 'text-text-secondary hover:bg-white/[0.06]'}`}
162
+ >{c.l}</button>
163
+ ))}
164
+ </div>
165
+ </>
166
+ )}
167
+ </div>
168
+ </ToolbarButton>
169
+ <Divider />
170
+ <LayerControls />
171
+ </ShapeSidebar>
172
+ )
173
+ }
@@ -0,0 +1,134 @@
1
+ "use client"
2
+
3
+ import useSketchStore, { TOOLS } from '../../store/useSketchStore'
4
+ import ShapeSidebar, { ToolbarButton, Divider, LayerControls } from './ShapeSidebar'
5
+ import { useState, useCallback } from 'react'
6
+ import { useTranslation } from '../../hooks/useTranslation'
7
+
8
+ const STROKE_COLORS = ['#1a1a20', '#ffffff', '#FF8383', '#3A994C', '#56A2E8', '#FFD700', '#FF69B4', '#A855F7']
9
+ const BG_COLORS = ['transparent', '#f0f0f0', '#ffcccb', '#90ee90', '#add8e6', '#FFE4B5', '#DDA0DD', '#2d2d2d']
10
+
11
+ const FILLS = [
12
+ { value: 'hachure', label: 'sidebar.fill.hachure' },
13
+ { value: 'solid', label: 'sidebar.fill.solid' },
14
+ { value: 'dots', label: 'sidebar.fill.dots' },
15
+ { value: 'cross-hatch', label: 'sidebar.fill.cross' },
16
+ { value: 'transparent', label: 'sidebar.fill.none' },
17
+ ]
18
+
19
+ function ColorGrid({ colors, selected, onSelect }) {
20
+ return (
21
+ <div className="grid grid-cols-4 gap-1.5">
22
+ {colors.map((c) => {
23
+ const isTrans = c === 'transparent'
24
+ return (
25
+ <button
26
+ key={c}
27
+ onClick={() => onSelect(c)}
28
+ className={`w-7 h-7 rounded-md border-[1.5px] transition-all duration-100 ${
29
+ selected === c ? 'border-accent-blue scale-110' : 'border-white/[0.08] hover:border-white/20'
30
+ }`}
31
+ style={!isTrans ? { backgroundColor: c } : undefined}
32
+ >
33
+ {isTrans && (
34
+ <svg className="w-full h-full text-text-dim" viewBox="0 0 20 20">
35
+ <line x1="4" y1="16" x2="16" y2="4" stroke="currentColor" strokeWidth="2" />
36
+ </svg>
37
+ )}
38
+ </button>
39
+ )
40
+ })}
41
+ </div>
42
+ )
43
+ }
44
+
45
+ export default function CircleSidebar() {
46
+ const { t } = useTranslation()
47
+ const activeTool = useSketchStore((s) => s.activeTool)
48
+ const selectedShapeSidebar = useSketchStore((s) => s.selectedShapeSidebar)
49
+ const [strokeColor, setStrokeColor] = useState('#fff')
50
+ const [bgColor, setBgColor] = useState('transparent')
51
+ const [thickness, setThickness] = useState(2)
52
+ const [lineStyle, setLineStyle] = useState('solid')
53
+ const [fillStyle, setFillStyle] = useState('hachure')
54
+
55
+ const updateStroke = useCallback((v) => { setStrokeColor(v); if (window.updateSelectedCircleStyle) window.updateSelectedCircleStyle({ stroke: v }) }, [])
56
+ const updateBg = useCallback((v) => { setBgColor(v); if (window.updateSelectedCircleStyle) window.updateSelectedCircleStyle({ fill: v }) }, [])
57
+ const updateThickness = useCallback((v) => { setThickness(v); if (window.updateSelectedCircleStyle) window.updateSelectedCircleStyle({ strokeWidth: v }) }, [])
58
+ const updateStyle = useCallback((v) => { setLineStyle(v); if (window.updateSelectedCircleStyle) window.updateSelectedCircleStyle({ outlineStyle: v }) }, [])
59
+ const updateFill = useCallback((v) => { setFillStyle(v); if (window.updateSelectedCircleStyle) window.updateSelectedCircleStyle({ fillStyle: v }) }, [])
60
+
61
+ return (
62
+ <ShapeSidebar visible={activeTool === TOOLS.CIRCLE || selectedShapeSidebar === 'circle'}>
63
+ <ToolbarButton
64
+ tooltip={t('sidebar.strokeColor')}
65
+ preview={<span className="w-4 h-4 rounded-md border border-white/20" style={{ backgroundColor: strokeColor }} />}
66
+ >
67
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.stroke')}</p>
68
+ <ColorGrid colors={STROKE_COLORS} selected={strokeColor} onSelect={updateStroke} />
69
+ </ToolbarButton>
70
+
71
+ <Divider />
72
+
73
+ <ToolbarButton
74
+ tooltip={t('sidebar.fillColor')}
75
+ preview={
76
+ <span className="w-4 h-4 rounded-md border border-white/20" style={{ backgroundColor: bgColor === 'transparent' ? 'transparent' : bgColor }}>
77
+ {bgColor === 'transparent' && <svg className="w-full h-full text-text-dim" viewBox="0 0 16 16"><line x1="2" y1="14" x2="14" y2="2" stroke="currentColor" strokeWidth="1.5" /></svg>}
78
+ </span>
79
+ }
80
+ >
81
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.background')}</p>
82
+ <ColorGrid colors={BG_COLORS} selected={bgColor} onSelect={updateBg} />
83
+ </ToolbarButton>
84
+
85
+ <Divider />
86
+
87
+ <ToolbarButton icon="bxs-edit-alt" tooltip={t('sidebar.strokeWidth')}>
88
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.width')}</p>
89
+ <div className="flex items-center gap-1">
90
+ {[1, 2, 4, 7].map((w) => (
91
+ <button key={w} onClick={() => updateThickness(w)}
92
+ className={`w-9 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${thickness === w ? 'bg-[#5B57D1]/20 text-[#5B57D1]' : 'text-text-muted hover:bg-white/[0.06]'}`}
93
+ >
94
+ <div className="w-5 rounded-full bg-current" style={{ height: Math.max(1, w) }} />
95
+ </button>
96
+ ))}
97
+ </div>
98
+ </ToolbarButton>
99
+
100
+ <Divider />
101
+
102
+ <ToolbarButton icon="bxs-minus-circle" tooltip="Stroke style">
103
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.style')}</p>
104
+ <div className="flex items-center gap-1">
105
+ {[{ v: 'solid', d: '' }, { v: 'dashed', d: '6 4' }, { v: 'dotted', d: '2 3' }].map((s) => (
106
+ <button key={s.v} onClick={() => updateStyle(s.v)}
107
+ className={`w-11 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${lineStyle === s.v ? 'bg-[#5B57D1]/20' : 'hover:bg-white/[0.06]'}`}
108
+ >
109
+ <svg width="28" height="4" viewBox="0 0 28 4"><line x1="0" y1="2" x2="28" y2="2" stroke="#fff" strokeWidth="2" strokeDasharray={s.d} strokeLinecap="round" /></svg>
110
+ </button>
111
+ ))}
112
+ </div>
113
+ </ToolbarButton>
114
+
115
+ <Divider />
116
+
117
+ <ToolbarButton icon="bxs-brush" tooltip="Fill style">
118
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.fill')}</p>
119
+ <div className="flex flex-col gap-0.5">
120
+ {FILLS.map((f) => (
121
+ <button key={f.value} onClick={() => updateFill(f.value)}
122
+ className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 ${fillStyle === f.value ? 'bg-[#5B57D1] text-white' : 'text-text-secondary hover:bg-white/[0.06]'}`}
123
+ >
124
+ <span className="w-1.5 h-1.5 rounded-full bg-current" />
125
+ {t(f.label)}
126
+ </button>
127
+ ))}
128
+ </div>
129
+ </ToolbarButton>
130
+ <Divider />
131
+ <LayerControls />
132
+ </ShapeSidebar>
133
+ )
134
+ }