@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,109 @@
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
+ function ColorGrid({ colors, selected, onSelect }) {
11
+ return (
12
+ <div className="grid grid-cols-4 gap-1.5">
13
+ {colors.map((c) => (
14
+ <button key={c} onClick={() => onSelect(c)}
15
+ 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'}`}
16
+ style={{ backgroundColor: c }}
17
+ />
18
+ ))}
19
+ </div>
20
+ )
21
+ }
22
+
23
+ export default function LineSidebar() {
24
+ const { t } = useTranslation()
25
+ const activeTool = useSketchStore((s) => s.activeTool)
26
+ const selectedShapeSidebar = useSketchStore((s) => s.selectedShapeSidebar)
27
+ const [strokeColor, setStrokeColor] = useState('#fff')
28
+ const [thickness, setThickness] = useState(2)
29
+ const [lineStyle, setLineStyle] = useState('solid')
30
+ const [sloppiness, setSloppiness] = useState(0)
31
+ const [edge, setEdge] = useState('smooth')
32
+
33
+ const updateStroke = useCallback((v) => { setStrokeColor(v); if (window.updateSelectedLineStyle) window.updateSelectedLineStyle({ stroke: v }) }, [])
34
+ const updateThickness = useCallback((v) => { setThickness(v); if (window.updateSelectedLineStyle) window.updateSelectedLineStyle({ strokeWidth: v }) }, [])
35
+ const updateStyle = useCallback((v) => { setLineStyle(v); if (window.updateSelectedLineStyle) window.updateSelectedLineStyle({ strokeStyle: v }) }, [])
36
+ const updateSloppiness = useCallback((v) => { setSloppiness(v); if (window.updateSelectedLineStyle) window.updateSelectedLineStyle({ sloppiness: v }) }, [])
37
+ const updateEdge = useCallback((v) => { setEdge(v); if (window.updateSelectedLineStyle) window.updateSelectedLineStyle({ edge: v === 'smooth' ? 1 : 5 }) }, [])
38
+
39
+ return (
40
+ <ShapeSidebar visible={activeTool === TOOLS.LINE || selectedShapeSidebar === 'line'}>
41
+ <ToolbarButton tooltip={t('sidebar.strokeColor')}
42
+ preview={<span className="w-4 h-4 rounded-md border border-white/20" style={{ backgroundColor: strokeColor }} />}
43
+ >
44
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.stroke')}</p>
45
+ <ColorGrid colors={STROKE_COLORS} selected={strokeColor} onSelect={updateStroke} />
46
+ </ToolbarButton>
47
+
48
+ <Divider />
49
+
50
+ <ToolbarButton icon="bxs-edit-alt" tooltip={t('sidebar.strokeWidth')}>
51
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.width')}</p>
52
+ <div className="flex items-center gap-1">
53
+ {[1, 2, 4, 7].map((w) => (
54
+ <button key={w} onClick={() => updateThickness(w)}
55
+ 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]'}`}
56
+ >
57
+ <div className="w-5 rounded-full bg-current" style={{ height: Math.max(1, w) }} />
58
+ </button>
59
+ ))}
60
+ </div>
61
+ </ToolbarButton>
62
+
63
+ <Divider />
64
+
65
+ <ToolbarButton icon="bxs-minus-circle" tooltip="Stroke style">
66
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.style')}</p>
67
+ <div className="flex items-center gap-1">
68
+ {[{ v: 'solid', d: '' }, { v: 'dashed', d: '6 4' }, { v: 'dotted', d: '2 3' }].map((s) => (
69
+ <button key={s.v} onClick={() => updateStyle(s.v)}
70
+ 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]'}`}
71
+ >
72
+ <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>
73
+ </button>
74
+ ))}
75
+ </div>
76
+ </ToolbarButton>
77
+
78
+ <Divider />
79
+
80
+ <ToolbarButton icon="bxs-shape-polygon" tooltip="Sloppiness">
81
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.sloppiness')}</p>
82
+ <div className="flex items-center gap-1">
83
+ {[{ v: 0, l: '0' }, { v: 2, l: '2' }, { v: 4, l: '4' }].map((s) => (
84
+ <button key={s.v} onClick={() => updateSloppiness(s.v)}
85
+ className={`w-8 h-8 flex items-center justify-center rounded-lg text-xs transition-all duration-100 ${sloppiness === s.v ? 'bg-[#5B57D1]/20 text-accent-blue' : 'text-text-muted hover:bg-white/6'}`}
86
+ >{s.l}</button>
87
+ ))}
88
+ </div>
89
+ </ToolbarButton>
90
+
91
+ <Divider />
92
+
93
+ <ToolbarButton icon="bxs-landscape" tooltip="Edge">
94
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.edge')}</p>
95
+ <div className="flex flex-col gap-0.5">
96
+ {[{ v: 'smooth', i: 'bxs-droplet', l: 'Smooth' }, { v: 'rough', i: 'bxs-bolt', l: 'Rough' }].map((e) => (
97
+ <button key={e.v} onClick={() => updateEdge(e.v)}
98
+ className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 ${edge === e.v ? 'bg-accent-blue text-white' : 'text-text-secondary hover:bg-white/6'}`}
99
+ >
100
+ <i className={`bx ${e.i} text-sm`} /> {e.l}
101
+ </button>
102
+ ))}
103
+ </div>
104
+ </ToolbarButton>
105
+ <Divider />
106
+ <LayerControls />
107
+ </ShapeSidebar>
108
+ )
109
+ }
@@ -0,0 +1,119 @@
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
+ // Map taper UI values to perfect-freehand thinning values
11
+ const TAPER_MAP = { uniform: 0, pen: 0.5, brush: 0.8 }
12
+
13
+ function ColorGrid({ colors, selected, onSelect }) {
14
+ return (
15
+ <div className="grid grid-cols-4 gap-1.5">
16
+ {colors.map((c) => (
17
+ <button key={c} onClick={() => onSelect(c)}
18
+ 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'}`}
19
+ style={{ backgroundColor: c }}
20
+ />
21
+ ))}
22
+ </div>
23
+ )
24
+ }
25
+
26
+ export default function PaintbrushSidebar() {
27
+ const { t } = useTranslation()
28
+ const activeTool = useSketchStore((s) => s.activeTool)
29
+ const selectedShapeSidebar = useSketchStore((s) => s.selectedShapeSidebar)
30
+ const [strokeColor, setStrokeColor] = useState('#fff')
31
+ const [thickness, setThickness] = useState(2)
32
+ const [lineStyle, setLineStyle] = useState('solid')
33
+ const [taper, setTaper] = useState('uniform')
34
+ const [roughness, setRoughness] = useState('smooth')
35
+ const [opacity, setOpacity] = useState(1)
36
+
37
+ const updateStroke = useCallback((v) => { setStrokeColor(v); if (window.updateSelectedFreehandStyle) window.updateSelectedFreehandStyle({ stroke: v }) }, [])
38
+ const updateThickness = useCallback((v) => { setThickness(v); if (window.updateSelectedFreehandStyle) window.updateSelectedFreehandStyle({ strokeWidth: v }) }, [])
39
+ const updateTaper = useCallback((v) => { setTaper(v); if (window.updateSelectedFreehandStyle) window.updateSelectedFreehandStyle({ thinning: TAPER_MAP[v] }) }, [])
40
+ const updateRoughness = useCallback((v) => { setRoughness(v); if (window.updateSelectedFreehandStyle) window.updateSelectedFreehandStyle({ roughness: v }) }, [])
41
+ const updateOpacity = useCallback((v) => { setOpacity(v); if (window.updateSelectedFreehandStyle) window.updateSelectedFreehandStyle({ opacity: v }) }, [])
42
+
43
+ return (
44
+ <ShapeSidebar visible={activeTool === TOOLS.FREEHAND || selectedShapeSidebar === 'paintbrush'}>
45
+ <ToolbarButton tooltip={t('sidebar.strokeColor')}
46
+ preview={<span className="w-4 h-4 rounded-md border border-white/20" style={{ backgroundColor: strokeColor }} />}
47
+ >
48
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.stroke')}</p>
49
+ <ColorGrid colors={STROKE_COLORS} selected={strokeColor} onSelect={updateStroke} />
50
+ </ToolbarButton>
51
+
52
+ <Divider />
53
+
54
+ <ToolbarButton icon="bxs-edit-alt" tooltip={t('sidebar.strokeWidth')}>
55
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.width')}</p>
56
+ <div className="flex items-center gap-1">
57
+ {[1, 2, 4, 7].map((w) => (
58
+ <button key={w} onClick={() => updateThickness(w)}
59
+ 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]'}`}
60
+ >
61
+ <div className="w-5 rounded-full bg-current" style={{ height: Math.max(1, w) }} />
62
+ </button>
63
+ ))}
64
+ </div>
65
+ </ToolbarButton>
66
+
67
+ <Divider />
68
+
69
+ <ToolbarButton icon="bxs-pen" tooltip="Taper">
70
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.taper')}</p>
71
+ <div className="flex flex-col gap-0.5">
72
+ {[
73
+ { v: 'uniform', i: 'bxs-minus-circle', l: 'Uniform' },
74
+ { v: 'pen', i: 'bxs-pen', l: 'Pen' },
75
+ { v: 'brush', i: 'bxs-brush', l: 'Brush' },
76
+ ].map((t) => (
77
+ <button key={t.v} onClick={() => updateTaper(t.v)}
78
+ className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 ${taper === t.v ? 'bg-[#5B57D1] text-white' : 'text-text-secondary hover:bg-white/[0.06]'}`}
79
+ >
80
+ <i className={`bx ${t.i} text-sm`} /> {t.l}
81
+ </button>
82
+ ))}
83
+ </div>
84
+ </ToolbarButton>
85
+
86
+ <Divider />
87
+
88
+ <ToolbarButton icon="bxs-shape-polygon" tooltip="Roughness">
89
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.roughness')}</p>
90
+ <div className="flex flex-col gap-0.5">
91
+ {[
92
+ { v: 'smooth', i: 'bxs-droplet', l: 'Smooth' },
93
+ { v: 'medium', i: 'bxs-leaf', l: 'Medium' },
94
+ { v: 'rough', i: 'bxs-bolt', l: 'Rough' },
95
+ ].map((r) => (
96
+ <button key={r.v} onClick={() => updateRoughness(r.v)}
97
+ className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 ${roughness === r.v ? 'bg-[#5B57D1] text-white' : 'text-text-secondary hover:bg-white/[0.06]'}`}
98
+ >
99
+ <i className={`bx ${r.i} text-sm`} /> {r.l}
100
+ </button>
101
+ ))}
102
+ </div>
103
+ </ToolbarButton>
104
+
105
+ <Divider />
106
+
107
+ <ToolbarButton icon="bxs-sun" tooltip={t('sidebar.opacity')}>
108
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">Opacity {Math.round(opacity * 100)}%</p>
109
+ <input
110
+ type="range" min="0" max="1" step="0.05" value={opacity}
111
+ onChange={(e) => updateOpacity(parseFloat(e.target.value))}
112
+ className="w-28 h-1 bg-white/10 rounded-full appearance-none cursor-pointer accent-[#5B57D1]"
113
+ />
114
+ </ToolbarButton>
115
+ <Divider />
116
+ <LayerControls />
117
+ </ShapeSidebar>
118
+ )
119
+ }
@@ -0,0 +1,165 @@
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-[#5B57D1] 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 RectangleSidebar() {
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 update = useCallback((changes, localSetters) => {
56
+ Object.entries(localSetters).forEach(([, fn]) => fn())
57
+ if (window.updateSelectedRectStyle) window.updateSelectedRectStyle(changes)
58
+ }, [])
59
+
60
+ const updateStroke = useCallback((v) => { setStrokeColor(v); if (window.updateSelectedRectStyle) window.updateSelectedRectStyle({ stroke: v }) }, [])
61
+ const updateBg = useCallback((v) => { setBgColor(v); if (window.updateSelectedRectStyle) window.updateSelectedRectStyle({ fill: v }) }, [])
62
+ const updateThickness = useCallback((v) => { setThickness(v); if (window.updateSelectedRectStyle) window.updateSelectedRectStyle({ strokeWidth: v }) }, [])
63
+ const updateStyle = useCallback((v) => { setLineStyle(v); if (window.updateSelectedRectStyle) window.updateSelectedRectStyle({ outlineStyle: v }) }, [])
64
+ const updateFill = useCallback((v) => { setFillStyle(v); if (window.updateSelectedRectStyle) window.updateSelectedRectStyle({ fillStyle: v }) }, [])
65
+
66
+ return (
67
+ <ShapeSidebar visible={activeTool === TOOLS.RECTANGLE || selectedShapeSidebar === 'rectangle'}>
68
+ {/* Stroke color */}
69
+ <ToolbarButton
70
+ tooltip={t('sidebar.strokeColor')}
71
+ preview={<span className="w-4 h-4 rounded-md border border-white/20" style={{ backgroundColor: strokeColor }} />}
72
+ >
73
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.stroke')}</p>
74
+ <ColorGrid colors={STROKE_COLORS} selected={strokeColor} onSelect={updateStroke} />
75
+ </ToolbarButton>
76
+
77
+ <Divider />
78
+
79
+ {/* Background */}
80
+ <ToolbarButton
81
+ tooltip={t('sidebar.fillColor')}
82
+ preview={
83
+ <span className="w-4 h-4 rounded-md border border-white/20" style={{ backgroundColor: bgColor === 'transparent' ? 'transparent' : bgColor }}>
84
+ {bgColor === 'transparent' && (
85
+ <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>
86
+ )}
87
+ </span>
88
+ }
89
+ >
90
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.background')}</p>
91
+ <ColorGrid colors={BG_COLORS} selected={bgColor} onSelect={updateBg} />
92
+ </ToolbarButton>
93
+
94
+ <Divider />
95
+
96
+ {/* Thickness */}
97
+ <ToolbarButton icon="bxs-edit-alt" tooltip={t('sidebar.strokeWidth')}>
98
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.width')}</p>
99
+ <div className="flex items-center gap-1">
100
+ {[1, 2, 4, 7].map((w) => (
101
+ <button
102
+ key={w}
103
+ onClick={() => updateThickness(w)}
104
+ className={`w-9 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${
105
+ thickness === w ? 'bg-[#5B57D1]/20 text-[#5B57D1]' : 'text-text-muted hover:bg-white/[0.06]'
106
+ }`}
107
+ >
108
+ <div className="w-5 rounded-full bg-current" style={{ height: Math.max(1, w) }} />
109
+ </button>
110
+ ))}
111
+ </div>
112
+ </ToolbarButton>
113
+
114
+ <Divider />
115
+
116
+ {/* Stroke style */}
117
+ <ToolbarButton icon="bxs-minus-circle" tooltip="Stroke style">
118
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.style')}</p>
119
+ <div className="flex items-center gap-1">
120
+ {[
121
+ { v: 'solid', d: '' },
122
+ { v: 'dashed', d: '6 4' },
123
+ { v: 'dotted', d: '2 3' },
124
+ ].map((s) => (
125
+ <button
126
+ key={s.v}
127
+ onClick={() => updateStyle(s.v)}
128
+ className={`w-11 h-8 flex items-center justify-center rounded-lg transition-all duration-100 ${
129
+ lineStyle === s.v ? 'bg-[#5B57D1]/20' : 'hover:bg-white/[0.06]'
130
+ }`}
131
+ >
132
+ <svg width="28" height="4" viewBox="0 0 28 4">
133
+ <line x1="0" y1="2" x2="28" y2="2" stroke="#fff" strokeWidth="2" strokeDasharray={s.d} strokeLinecap="round" />
134
+ </svg>
135
+ </button>
136
+ ))}
137
+ </div>
138
+ </ToolbarButton>
139
+
140
+ <Divider />
141
+
142
+ {/* Fill pattern */}
143
+ <ToolbarButton icon="bxs-brush" tooltip="Fill style">
144
+ <p className="text-xs text-text-muted uppercase tracking-wider mb-2">{t('sidebar.sectionHeader.fill')}</p>
145
+ <div className="flex flex-col gap-0.5">
146
+ {FILLS.map((f) => (
147
+ <button
148
+ key={f.value}
149
+ onClick={() => updateFill(f.value)}
150
+ className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 ${
151
+ fillStyle === f.value ? 'bg-[#5B57D1] text-white' : 'text-text-secondary hover:bg-white/[0.06]'
152
+ }`}
153
+ >
154
+ <span className="w-1.5 h-1.5 rounded-full bg-current" />
155
+ {t(f.label)}
156
+ </button>
157
+ ))}
158
+ </div>
159
+ </ToolbarButton>
160
+
161
+ <Divider />
162
+ <LayerControls />
163
+ </ShapeSidebar>
164
+ )
165
+ }
@@ -0,0 +1,130 @@
1
+ "use client"
2
+
3
+ import { useState, useRef, useEffect } from 'react'
4
+ import useSketchStore from '../../store/useSketchStore'
5
+
6
+ /**
7
+ * A toolbar button that opens a popover panel above it on click.
8
+ * Shows an icon (or custom preview) in the bar, popover shows full options.
9
+ */
10
+ export function ToolbarButton({ icon, preview, children, tooltip }) {
11
+ const [open, setOpen] = useState(false)
12
+ const ref = useRef(null)
13
+
14
+ // Close on outside click
15
+ useEffect(() => {
16
+ if (!open) return
17
+ const handler = (e) => {
18
+ if (ref.current && !ref.current.contains(e.target)) setOpen(false)
19
+ }
20
+ document.addEventListener('mousedown', handler)
21
+ return () => document.removeEventListener('mousedown', handler)
22
+ }, [open])
23
+
24
+ return (
25
+ <div ref={ref} className="relative flex items-center">
26
+ <button
27
+ onClick={() => setOpen(!open)}
28
+ title={tooltip}
29
+ className={`h-9 flex items-center gap-1.5 px-3 rounded-lg transition-all duration-100 ${
30
+ open
31
+ ? 'bg-surface-active text-text-primary'
32
+ : 'text-text-muted hover:text-text-primary hover:bg-surface-hover'
33
+ }`}
34
+ >
35
+ {preview || (icon && <i className={`bx ${icon} text-base`} />)}
36
+ <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
+ <path d="M1 1l3 3 3-3" />
38
+ </svg>
39
+ </button>
40
+
41
+ {open && (
42
+ <div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 z-20">
43
+ <div className="bg-surface-card border border-border-light rounded-xl p-3 shadow-xl shadow-black/10 min-w-max">
44
+ {children}
45
+ </div>
46
+ {/* Arrow pointer */}
47
+ <div className="absolute -bottom-[5px] left-1/2 -translate-x-1/2 w-2.5 h-2.5 rotate-45 bg-surface-card border-r border-b border-border-light" />
48
+ </div>
49
+ )}
50
+ </div>
51
+ )
52
+ }
53
+
54
+ /**
55
+ * Simple toolbar divider
56
+ */
57
+ function Divider() {
58
+ return <div className="w-px h-5 bg-surface-hover mx-0.5 shrink-0" />
59
+ }
60
+
61
+ /**
62
+ * Bottom toolbar container - appears when tool/shape is active
63
+ */
64
+ export default function ShapeSidebar({ visible, children }) {
65
+ const viewMode = useSketchStore((s) => s.viewMode)
66
+ const show = visible && !viewMode
67
+
68
+ return (
69
+ <div
70
+ className={`absolute bottom-14 left-1/2 -translate-x-1/2 bg-surface border border-border-light rounded-xl px-2 py-1.5 z-[999] font-[lixFont] transition-all duration-200 ${
71
+ show
72
+ ? 'opacity-100 pointer-events-auto translate-y-0'
73
+ : 'opacity-0 pointer-events-none translate-y-2'
74
+ }`}
75
+ >
76
+ <div className="flex items-center gap-0.5">
77
+ {children}
78
+ </div>
79
+ </div>
80
+ )
81
+ }
82
+
83
+ /**
84
+ * Layer ordering controls - add to any shape sidebar
85
+ */
86
+ import { useTranslation } from '../../hooks/useTranslation'
87
+
88
+ function LayerControls() {
89
+ const { t } = useTranslation()
90
+ const doLayer = (method) => {
91
+ const shape = window.currentShape
92
+ if (!shape || !window.__layerOrder) return
93
+ window.__layerOrder[method](shape)
94
+ }
95
+
96
+ return (
97
+ <div className="flex items-center gap-0.5">
98
+ <button
99
+ onClick={() => doLayer('sendToBack')}
100
+ title={t('sidebar.sendToBack', { defaultValue: 'Send to back' })}
101
+ 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"
102
+ >
103
+ <i className="bx bx-chevrons-down text-base" />
104
+ </button>
105
+ <button
106
+ onClick={() => doLayer('sendBackward')}
107
+ title={t('sidebar.sendBackward')}
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>
127
+ )
128
+ }
129
+
130
+ export { Divider, LayerControls }