@elixpo/lixsketch 5.4.7 → 5.5.0

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 +5342 -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 +475 -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,358 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect } from 'react'
4
+ import useUIStore from '../../store/useUIStore'
5
+ import useSketchStore from '../../store/useSketchStore'
6
+ import useAuthStore from '../../hooks/inertStores'
7
+ import useCollabStore from '../../hooks/inertStores'
8
+ import { useProfileStore } from '../../hooks/inertStores'
9
+ import { getSessionID } from '../../hooks/inertStores'
10
+ import { triggerDocCloudSync, persistLayoutMode } from '../../hooks/inertStores'
11
+
12
+ function formatBytes(bytes) {
13
+ if (!bytes || bytes === 0) return '0 B'
14
+ const k = 1024
15
+ const sizes = ['B', 'KB', 'MB']
16
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
17
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
18
+ }
19
+
20
+ function InfoRow({ label, value, icon, color }) {
21
+ return (
22
+ <div className="flex items-center justify-between py-2 px-1">
23
+ <div className="flex items-center gap-2 text-text-dim text-xs">
24
+ <i className={`bx ${icon} text-sm ${color || 'text-text-muted'}`} />
25
+ {label}
26
+ </div>
27
+ <span className="text-text-secondary text-xs font-medium">{value}</span>
28
+ </div>
29
+ )
30
+ }
31
+
32
+ export default function CanvasPropertiesModal() {
33
+ const isOpen = useUIStore((s) => s.canvasPropertiesOpen)
34
+ const toggle = useUIStore((s) => s.toggleCanvasProperties)
35
+ const workspaceName = useUIStore((s) => s.workspaceName)
36
+ const saveStatus = useUIStore((s) => s.saveStatus)
37
+
38
+ const isAuthenticated = useAuthStore((s) => s.isAuthenticated)
39
+ const authUser = useAuthStore((s) => s.user)
40
+ const guestProfile = useProfileStore((s) => s.profile)
41
+
42
+ const collabConnected = useCollabStore((s) => s.connected)
43
+ const collabUsers = useCollabStore((s) => s.users)
44
+ const adminUserId = useCollabStore((s) => s.adminUserId)
45
+ const ws = useCollabStore((s) => s.ws)
46
+
47
+ const layoutMode = useSketchStore((s) => s.layoutMode)
48
+ const setLayoutMode = useSketchStore((s) => s.setLayoutMode)
49
+ const handleSetLayout = (mode) => {
50
+ if (mode === layoutMode) return
51
+ setLayoutMode(mode)
52
+ persistLayoutMode(mode)
53
+ }
54
+
55
+ // Determine if current user is admin
56
+ const myUserId = isAuthenticated ? authUser?.id : guestProfile?.id
57
+ const isAdmin = myUserId && adminUserId && myUserId === adminUserId
58
+
59
+ const handleKickUser = (userId) => {
60
+ if (!ws || ws.readyState !== WebSocket.OPEN) return
61
+ ws.send(JSON.stringify({ type: 'kick', userId }))
62
+ }
63
+
64
+ const [stats, setStats] = useState({
65
+ shapeCount: 0,
66
+ canvasSize: '0 x 0',
67
+ zoom: 1,
68
+ dataSize: '0 B',
69
+ sessionId: '',
70
+ })
71
+
72
+ useEffect(() => {
73
+ if (!isOpen) return
74
+
75
+ const shapes = window.shapes || []
76
+ const vb = window.svg?.viewBox?.baseVal
77
+ const zoom = window.currentZoom || 1
78
+ const sessionId = getSessionID() || ''
79
+
80
+ // Estimate canvas data size from localStorage (per-session key, fall
81
+ // back to the legacy single-key entry).
82
+ const sceneSavedKey = sessionId ? `lixsketch-autosave-${sessionId}` : 'lixsketch-autosave'
83
+ const sceneSaved = localStorage.getItem(sceneSavedKey) || localStorage.getItem('lixsketch-autosave')
84
+ const dataSize = sceneSaved ? new Blob([sceneSaved]).size : 0
85
+
86
+ // Doc stats: pulled from the doc-autosave localStorage buffer.
87
+ const docKey = sessionId ? `lixsketch-doc-autosave-${sessionId}` : 'lixsketch-doc-autosave'
88
+ const docMetaKey = sessionId ? `lixsketch-doc-autosave-meta-${sessionId}` : 'lixsketch-doc-autosave-meta'
89
+ const docSaved = localStorage.getItem(docKey)
90
+ let docBlockCount = 0
91
+ let docSize = 0
92
+ let docSavedAt = null
93
+ if (docSaved) {
94
+ docSize = new Blob([docSaved]).size
95
+ try {
96
+ const parsed = JSON.parse(docSaved)
97
+ if (Array.isArray(parsed?.blocks)) docBlockCount = parsed.blocks.length
98
+ if (parsed?.savedAt) docSavedAt = parsed.savedAt
99
+ } catch {}
100
+ }
101
+ let docCloudUpdatedAt = null
102
+ try {
103
+ const meta = JSON.parse(localStorage.getItem(docMetaKey) || '{}')
104
+ docCloudUpdatedAt = meta?.lastSeenUpdatedAt || null
105
+ } catch {}
106
+
107
+ // Count shape types
108
+ const typeCounts = {}
109
+ shapes.forEach(s => {
110
+ const name = s.shapeName || 'unknown'
111
+ typeCounts[name] = (typeCounts[name] || 0) + 1
112
+ })
113
+
114
+ setStats({
115
+ shapeCount: shapes.length,
116
+ canvasSize: vb ? `${Math.round(vb.width)} x ${Math.round(vb.height)}` : 'N/A',
117
+ zoom: Math.round(zoom * 100),
118
+ dataSize: formatBytes(dataSize),
119
+ sessionId,
120
+ typeCounts,
121
+ docBlockCount,
122
+ docSize: formatBytes(docSize),
123
+ docSavedAt,
124
+ docCloudUpdatedAt,
125
+ })
126
+ }, [isOpen])
127
+
128
+ useEffect(() => {
129
+ if (!isOpen) return
130
+ const handleKey = (e) => {
131
+ if (e.key === 'Escape') toggle()
132
+ }
133
+ document.addEventListener('keydown', handleKey)
134
+ return () => document.removeEventListener('keydown', handleKey)
135
+ }, [isOpen, toggle])
136
+
137
+ if (!isOpen) return null
138
+
139
+ const ownerName = isAuthenticated
140
+ ? (authUser?.displayName || authUser?.email || 'You')
141
+ : (guestProfile?.displayName || 'Guest')
142
+
143
+ const ownerType = isAuthenticated ? 'Authenticated' : 'Guest'
144
+
145
+ return (
146
+ <div
147
+ className="fixed inset-0 z-[9999] flex items-center justify-center font-[lixFont]"
148
+ onClick={toggle}
149
+ >
150
+ <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
151
+
152
+ <div
153
+ className="relative bg-surface-card border border-border-light rounded-2xl w-[380px] mx-4 max-h-[85vh] overflow-y-auto docs-scroll"
154
+ onClick={(e) => e.stopPropagation()}
155
+ >
156
+ {/* Header */}
157
+ <div className="flex items-center justify-between px-5 pt-4 pb-2">
158
+ <div className="flex items-center gap-2">
159
+ <i className="bx bx-info-circle text-lg text-accent-blue" />
160
+ <h2 className="text-text-primary text-sm font-medium">Canvas Properties</h2>
161
+ </div>
162
+ <button
163
+ onClick={toggle}
164
+ className="w-7 h-7 flex items-center justify-center rounded-lg text-text-muted hover:text-text-primary hover:bg-surface-hover cursor-pointer transition-all duration-200"
165
+ >
166
+ <i className="bx bx-x text-xl" />
167
+ </button>
168
+ </div>
169
+
170
+ <div className="px-5 pb-5 flex flex-col gap-3">
171
+ {/* Workspace Info */}
172
+ <div className="p-3 rounded-xl border border-border-light bg-surface/50">
173
+ <p className="text-text-dim text-[10px] uppercase tracking-wider mb-2">Workspace</p>
174
+ <InfoRow label="Name" value={workspaceName || 'Untitled'} icon="bx-rename" />
175
+ <InfoRow label="Owner" value={ownerName} icon="bx-user" />
176
+ <InfoRow label="Account" value={ownerType} icon="bx-shield" color={isAuthenticated ? 'text-green-400' : 'text-yellow-400'} />
177
+ <InfoRow
178
+ label="Session ID"
179
+ value={
180
+ <span className="font-mono text-[10px] text-text-dim max-w-[140px] truncate inline-block" title={stats.sessionId}>
181
+ {stats.sessionId}
182
+ </span>
183
+ }
184
+ icon="bx-hash"
185
+ />
186
+ </div>
187
+
188
+ {/* Canvas Stats */}
189
+ <div className="p-3 rounded-xl border border-border-light bg-surface/50">
190
+ <p className="text-text-dim text-[10px] uppercase tracking-wider mb-2">Canvas</p>
191
+ <InfoRow label="Shapes" value={stats.shapeCount} icon="bx-shape-square" color="text-blue-400" />
192
+ <InfoRow label="Viewport" value={stats.canvasSize} icon="bx-expand" />
193
+ <InfoRow label="Zoom" value={`${stats.zoom}%`} icon="bx-zoom-in" />
194
+ <InfoRow label="Local data" value={stats.dataSize} icon="bx-data" />
195
+
196
+ {/* Shape breakdown */}
197
+ {stats.typeCounts && Object.keys(stats.typeCounts).length > 0 && (
198
+ <div className="mt-2 pt-2 border-t border-border-light">
199
+ <p className="text-text-dim text-[10px] uppercase tracking-wider mb-1.5">Shape Breakdown</p>
200
+ <div className="flex flex-wrap gap-1.5">
201
+ {Object.entries(stats.typeCounts).map(([type, count]) => (
202
+ <span
203
+ key={type}
204
+ className="px-2 py-0.5 rounded-md bg-surface text-text-muted text-[10px] border border-border-light"
205
+ >
206
+ {type}: {count}
207
+ </span>
208
+ ))}
209
+ </div>
210
+ </div>
211
+ )}
212
+ </div>
213
+
214
+ {/* Document */}
215
+ <div className="p-3 rounded-xl border border-border-light bg-surface/50">
216
+ <div className="flex items-center justify-between mb-2">
217
+ <p className="text-text-dim text-[10px] uppercase tracking-wider">Document</p>
218
+ <button
219
+ onClick={() => triggerDocCloudSync()}
220
+ title="Force sync the doc to cloud"
221
+ className="text-[10px] text-accent-blue hover:text-accent-blue-hover px-1.5 py-0.5 rounded hover:bg-accent-blue/10 cursor-pointer transition-all duration-200"
222
+ >
223
+ Sync now
224
+ </button>
225
+ </div>
226
+
227
+ {/* Layout mode pill toggle */}
228
+ <div className="flex items-center gap-1 bg-surface/60 border border-border-light rounded-lg p-0.5 mb-2">
229
+ {[
230
+ { key: 'canvas', icon: 'bx-pen', label: 'Canvas' },
231
+ { key: 'split', icon: 'bx-layout', label: 'Split' },
232
+ { key: 'docs', icon: 'bxs-notepad', label: 'Docs' },
233
+ ].map((m) => {
234
+ const active = layoutMode === m.key
235
+ return (
236
+ <button
237
+ key={m.key}
238
+ onClick={() => handleSetLayout(m.key)}
239
+ className={`flex-1 flex items-center justify-center gap-1 h-6 rounded-md text-[10.5px] transition-all duration-150 ${
240
+ active
241
+ ? 'bg-accent-blue text-text-primary'
242
+ : 'text-text-muted hover:text-text-primary hover:bg-surface-hover'
243
+ }`}
244
+ >
245
+ <i className={`bx ${m.icon} text-[11px]`} />
246
+ {m.label}
247
+ </button>
248
+ )
249
+ })}
250
+ </div>
251
+
252
+ <InfoRow label="Blocks" value={stats.docBlockCount || 0} icon="bx-list-ul" color="text-purple-400" />
253
+ <InfoRow label="Local size" value={stats.docSize || '0 B'} icon="bx-data" />
254
+ <InfoRow
255
+ label="Last local save"
256
+ value={
257
+ stats.docSavedAt
258
+ ? new Date(stats.docSavedAt).toLocaleTimeString()
259
+ : '—'
260
+ }
261
+ icon="bx-time"
262
+ />
263
+ <InfoRow
264
+ label="Last cloud sync"
265
+ value={
266
+ stats.docCloudUpdatedAt
267
+ ? new Date(stats.docCloudUpdatedAt).toLocaleString()
268
+ : '—'
269
+ }
270
+ icon="bx-cloud"
271
+ color={stats.docCloudUpdatedAt ? 'text-green-400' : 'text-text-muted'}
272
+ />
273
+ </div>
274
+
275
+ {/* Save & Sync Status */}
276
+ <div className="p-3 rounded-xl border border-border-light bg-surface/50">
277
+ <p className="text-text-dim text-[10px] uppercase tracking-wider mb-2">Sync Status</p>
278
+ <div className="flex items-center gap-2 py-1.5">
279
+ <span className={`w-2 h-2 rounded-full ${
280
+ saveStatus === 'cloud' ? 'bg-green-400' :
281
+ saveStatus === 'local' ? 'bg-yellow-400' :
282
+ 'bg-text-dim'
283
+ }`} />
284
+ <span className="text-text-secondary text-xs">
285
+ {saveStatus === 'cloud' ? 'Synced to cloud' :
286
+ saveStatus === 'local' ? 'Saved locally (pending cloud sync)' :
287
+ 'Not saved yet'}
288
+ </span>
289
+ </div>
290
+ </div>
291
+
292
+ {/* Collaboration Status */}
293
+ <div className="p-3 rounded-xl border border-border-light bg-surface/50">
294
+ <div className="flex items-center justify-between mb-2">
295
+ <p className="text-text-dim text-[10px] uppercase tracking-wider">Collaboration</p>
296
+ {collabConnected && collabUsers && (
297
+ <span className="text-text-dim text-[10px]">
298
+ {collabUsers.length} participant{collabUsers.length !== 1 ? 's' : ''}
299
+ </span>
300
+ )}
301
+ </div>
302
+ {collabConnected ? (
303
+ <>
304
+ <div className="flex items-center gap-2 py-1.5">
305
+ <span className="w-2 h-2 rounded-full bg-green-400 animate-pulse" />
306
+ <span className="text-green-400 text-xs">Live session active</span>
307
+ {isAdmin && (
308
+ <span className="ml-auto px-1.5 py-0.5 rounded bg-accent-blue/15 text-accent-blue text-[9px]">Admin</span>
309
+ )}
310
+ </div>
311
+ {collabUsers && collabUsers.length > 0 && (
312
+ <div className="mt-2 flex flex-col gap-1">
313
+ {collabUsers.map((user, i) => {
314
+ const isMe = user.userId === myUserId
315
+ const isUserAdmin = user.userId === adminUserId
316
+ return (
317
+ <div
318
+ key={user.userId || i}
319
+ className="flex items-center gap-2 px-2 py-1.5 rounded-lg bg-surface/80 border border-border-light"
320
+ >
321
+ <span
322
+ className="w-2.5 h-2.5 rounded-full shrink-0"
323
+ style={{ background: user.color || '#5B57D1' }}
324
+ />
325
+ <span className="text-text-secondary text-[11px] flex-1 truncate">
326
+ {user.displayName || user.name || `User ${i + 1}`}
327
+ {isMe && <span className="text-text-dim ml-1">(you)</span>}
328
+ </span>
329
+ {isUserAdmin && (
330
+ <span className="text-[9px] text-accent-blue px-1 py-0.5 rounded bg-accent-blue/10">admin</span>
331
+ )}
332
+ {isAdmin && !isMe && (
333
+ <button
334
+ onClick={() => handleKickUser(user.userId)}
335
+ className="text-red-400/60 hover:text-red-400 text-[10px] px-1.5 py-0.5 rounded hover:bg-red-500/10 transition-all duration-200 cursor-pointer"
336
+ title={`Remove ${user.displayName || 'user'} from session`}
337
+ >
338
+ <i className="bx bx-x text-sm" />
339
+ </button>
340
+ )}
341
+ </div>
342
+ )
343
+ })}
344
+ </div>
345
+ )}
346
+ </>
347
+ ) : (
348
+ <div className="flex items-center gap-2 py-1.5">
349
+ <span className="w-2 h-2 rounded-full bg-text-dim" />
350
+ <span className="text-text-dim text-xs">No active session</span>
351
+ </div>
352
+ )}
353
+ </div>
354
+ </div>
355
+ </div>
356
+ </div>
357
+ )
358
+ }
@@ -0,0 +1,307 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect, useRef } from 'react'
4
+ import useUIStore from '../../store/useUIStore'
5
+ import useSketchStore, { TOOLS } from '../../store/useSketchStore'
6
+
7
+ const COMMANDS = [
8
+ // --- App ---
9
+ { label: 'Library', icon: 'bx-library', section: 'App' },
10
+ { label: 'Find on canvas', icon: 'bx-search', section: 'App', shortcut: 'Ctrl+F', action: 'findOnCanvas' },
11
+ { label: 'Live collaboration...', icon: 'bx-group', section: 'App' },
12
+ { label: 'Share', icon: 'bx-share-alt', section: 'App' },
13
+ { label: 'Toggle theme', icon: 'bx-sun', section: 'App', action: 'toggleTheme' },
14
+
15
+ // --- Export ---
16
+ { label: 'Export image...', icon: 'bx-image', section: 'Export', shortcut: 'Ctrl+Shift+E', action: 'exportImage' },
17
+ { label: 'Save to disk', icon: 'bx-download', section: 'Export', shortcut: 'Ctrl+S', action: 'save' },
18
+ { label: 'Copy to clipboard as PNG', icon: 'bx-clipboard', section: 'Export', shortcut: 'Shift+Alt+C', action: 'copyPNG' },
19
+ { label: 'Copy to clipboard as SVG', icon: 'bx-code-alt', section: 'Export', shortcut: null, action: 'copySVG' },
20
+ { label: 'Open scene file', icon: 'bx-folder-open', section: 'Export', shortcut: 'Ctrl+O', action: 'load' },
21
+
22
+ // --- Editor ---
23
+ { label: 'Undo', icon: 'bx-undo', section: 'Editor', action: 'undo' },
24
+ { label: 'Redo', icon: 'bx-redo', section: 'Editor', action: 'redo' },
25
+ { label: 'Zoom in', icon: 'bx-plus', section: 'Editor', shortcut: 'Ctrl++' },
26
+ { label: 'Zoom out', icon: 'bx-minus', section: 'Editor', shortcut: 'Ctrl+-' },
27
+ { label: 'Reset zoom', icon: 'bx-reset', section: 'Editor', shortcut: 'Ctrl+0' },
28
+ { label: 'Zoom to fit all elements', icon: 'bx-fullscreen', section: 'Editor', shortcut: 'Shift+1' },
29
+ { label: 'Toggle grid', icon: 'bx-grid-alt', section: 'Editor', shortcut: "Ctrl+'", action: 'toggleGrid' },
30
+ { label: 'Shortcuts & help', icon: 'bx-help-circle', section: 'Editor', shortcut: '?', action: 'help' },
31
+ { label: 'Select all', icon: 'bx-select-multiple', section: 'Editor', shortcut: 'Ctrl+A', action: 'selectAll' },
32
+ { label: 'Reset canvas', icon: 'bx-trash', section: 'Editor', shortcut: 'Ctrl+Delete', action: 'resetCanvas' },
33
+ { label: 'Canvas background', icon: 'bx-palette', section: 'Editor', action: 'openMenu' },
34
+
35
+ // --- Tools ---
36
+ { label: 'Hand (panning tool)', icon: 'bx-hand', section: 'Tools', shortcut: 'H', action: 'tool:pan' },
37
+ { label: 'Selection', icon: 'bx-pointer', section: 'Tools', shortcut: 'V', action: 'tool:select' },
38
+ { label: 'Rectangle', icon: 'bx-rectangle', section: 'Tools', shortcut: 'R', action: 'tool:rectangle' },
39
+ { label: 'Diamond', icon: 'bx-diamond', section: 'Tools', shortcut: 'D', action: 'tool:diamond' },
40
+ { label: 'Circle', icon: 'bx-circle', section: 'Tools', shortcut: 'O', action: 'tool:circle' },
41
+ { label: 'Arrow', icon: 'bx-right-arrow-alt', section: 'Tools', shortcut: 'A', action: 'tool:arrow' },
42
+ { label: 'Line', icon: 'bx-minus', section: 'Tools', shortcut: 'L', action: 'tool:line' },
43
+ { label: 'Draw', icon: 'bx-pencil', section: 'Tools', shortcut: 'P', action: 'tool:freehand' },
44
+ { label: 'Text', icon: 'bx-text', section: 'Tools', shortcut: 'T', action: 'tool:text' },
45
+ { label: 'Insert image', icon: 'bx-image-add', section: 'Tools', shortcut: '9', action: 'tool:image' },
46
+ { label: 'Eraser', icon: 'bx-eraser', section: 'Tools', shortcut: 'E', action: 'tool:eraser' },
47
+ { label: 'Laser pointer', icon: 'bxs-magic-wand', section: 'Tools', shortcut: 'K', action: 'tool:laser' },
48
+ { label: 'Frame tool', icon: 'bx-border-all', section: 'Tools', shortcut: 'F', action: 'tool:frame' },
49
+ // AI entries removed — feature is hidden behind a coming-soon screen.
50
+ // Restore "Text to diagram..." / "Mermaid to diagram..." here when the
51
+ // assistant ships.
52
+
53
+ // --- Links ---
54
+ { label: 'GitHub', icon: 'bxl-github', section: 'Links', action: 'link:github' },
55
+ { label: 'Report an issue', icon: 'bx-bug', section: 'Links', action: 'link:issues' },
56
+ ]
57
+
58
+ const TOOL_ACTION_MAP = {
59
+ 'tool:pan': TOOLS.PAN,
60
+ 'tool:select': TOOLS.SELECT,
61
+ 'tool:rectangle': TOOLS.RECTANGLE,
62
+ 'tool:diamond': TOOLS.RECTANGLE, // diamond is a rectangle variant
63
+ 'tool:circle': TOOLS.CIRCLE,
64
+ 'tool:arrow': TOOLS.ARROW,
65
+ 'tool:line': TOOLS.LINE,
66
+ 'tool:freehand': TOOLS.FREEHAND,
67
+ 'tool:text': TOOLS.TEXT,
68
+ 'tool:image': TOOLS.IMAGE,
69
+ 'tool:eraser': TOOLS.ERASER,
70
+ 'tool:laser': TOOLS.LASER,
71
+ 'tool:frame': TOOLS.FRAME,
72
+ }
73
+
74
+ export default function CommandPalette() {
75
+ const open = useUIStore((s) => s.commandPaletteOpen)
76
+ const toggleCommandPalette = useUIStore((s) => s.toggleCommandPalette)
77
+ const [query, setQuery] = useState('')
78
+ const [selectedIndex, setSelectedIndex] = useState(0)
79
+ const inputRef = useRef(null)
80
+ const listRef = useRef(null)
81
+
82
+ useEffect(() => {
83
+ if (open) {
84
+ setQuery('')
85
+ setSelectedIndex(0)
86
+ setTimeout(() => inputRef.current?.focus(), 50)
87
+ }
88
+ }, [open])
89
+
90
+ // Scroll selected item into view
91
+ useEffect(() => {
92
+ if (!open) return
93
+ const el = listRef.current?.querySelector(`[data-index="${selectedIndex}"]`)
94
+ if (el) el.scrollIntoView({ block: 'nearest' })
95
+ }, [selectedIndex, open])
96
+
97
+ // Reset selection on query change
98
+ useEffect(() => {
99
+ setSelectedIndex(0)
100
+ }, [query])
101
+
102
+ if (!open) return null
103
+
104
+ const filtered = query
105
+ ? COMMANDS.filter((c) =>
106
+ c.label.toLowerCase().includes(query.toLowerCase())
107
+ )
108
+ : COMMANDS
109
+
110
+ // Group into sections preserving order
111
+ const sectionOrder = []
112
+ const sections = {}
113
+ filtered.forEach((c) => {
114
+ if (!sections[c.section]) {
115
+ sections[c.section] = []
116
+ sectionOrder.push(c.section)
117
+ }
118
+ sections[c.section].push(c)
119
+ })
120
+
121
+ const handleCommand = (cmd) => {
122
+ toggleCommandPalette()
123
+ const action = cmd.action
124
+
125
+ if (!action) return
126
+
127
+ // Tool switching
128
+ if (action.startsWith('tool:')) {
129
+ const tool = TOOL_ACTION_MAP[action]
130
+ if (tool) useSketchStore.getState().setActiveTool(tool)
131
+ return
132
+ }
133
+
134
+ // Links
135
+ if (action === 'link:github') {
136
+ window.open('https://github.com/elixpo/sketch.elixpo', '_blank')
137
+ return
138
+ }
139
+ if (action === 'link:issues') {
140
+ window.open('https://github.com/elixpo/sketch.elixpo/issues', '_blank')
141
+ return
142
+ }
143
+
144
+ const serializer = window.__sceneSerializer
145
+
146
+ switch (action) {
147
+ case 'toggleTheme': {
148
+ const store = useUIStore.getState()
149
+ store.setTheme(store.theme === 'dark' ? 'light' : 'dark')
150
+ break
151
+ }
152
+ case 'toggleGrid':
153
+ useSketchStore.getState().toggleGrid()
154
+ break
155
+ case 'save': {
156
+ const name = useUIStore.getState().workspaceName || 'Untitled'
157
+ serializer?.download(name)
158
+ break
159
+ }
160
+ case 'load':
161
+ serializer?.upload()
162
+ break
163
+ case 'exportImage':
164
+ useUIStore.getState().toggleExportImageModal()
165
+ break
166
+ case 'copyPNG':
167
+ serializer?.copyAsPNG()
168
+ break
169
+ case 'copySVG':
170
+ serializer?.copyAsSVG()
171
+ break
172
+ case 'undo':
173
+ if (typeof window.undo === 'function') window.undo()
174
+ break
175
+ case 'redo':
176
+ if (typeof window.redo === 'function') window.redo()
177
+ break
178
+ case 'selectAll':
179
+ useSketchStore.getState().setActiveTool(TOOLS.SELECT)
180
+ if (window.multiSelection && window.shapes) {
181
+ window.multiSelection.clearSelection()
182
+ window.shapes.forEach(shape => window.multiSelection.addShape(shape))
183
+ }
184
+ break
185
+ case 'resetCanvas': {
186
+ const serializer = window.__sceneSerializer
187
+ if (serializer?.resetCanvas) {
188
+ serializer.resetCanvas()
189
+ }
190
+ useSketchStore.getState().clearShapes()
191
+ useSketchStore.getState().clearHistory()
192
+ break
193
+ }
194
+ case 'findOnCanvas':
195
+ useUIStore.getState().toggleFindBar()
196
+ break
197
+ case 'help':
198
+ useUIStore.getState().toggleHelpModal()
199
+ break
200
+ case 'aiModal':
201
+ useUIStore.getState().toggleAIModal()
202
+ break
203
+ case 'openMenu':
204
+ useUIStore.getState().toggleMenu()
205
+ break
206
+ }
207
+ }
208
+
209
+ const handleKeyDown = (e) => {
210
+ if (e.key === 'Escape') {
211
+ toggleCommandPalette()
212
+ return
213
+ }
214
+ if (e.key === 'ArrowDown') {
215
+ e.preventDefault()
216
+ setSelectedIndex((i) => Math.min(i + 1, filtered.length - 1))
217
+ } else if (e.key === 'ArrowUp') {
218
+ e.preventDefault()
219
+ setSelectedIndex((i) => Math.max(i - 1, 0))
220
+ } else if (e.key === 'Enter' && filtered[selectedIndex]) {
221
+ e.preventDefault()
222
+ handleCommand(filtered[selectedIndex])
223
+ }
224
+ }
225
+
226
+ let flatIndex = -1
227
+
228
+ return (
229
+ <div
230
+ className="fixed inset-0 z-[9999] flex items-start justify-center pt-[12vh] font-[lixFont]"
231
+ onClick={toggleCommandPalette}
232
+ >
233
+ <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
234
+
235
+ <div
236
+ className="relative bg-surface-card border border-border-light rounded-2xl w-full max-w-[540px] mx-4 overflow-hidden"
237
+ onClick={(e) => e.stopPropagation()}
238
+ >
239
+ {/* Search */}
240
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-border-light">
241
+ <i className="bx bx-search text-text-muted text-lg" />
242
+ <input
243
+ ref={inputRef}
244
+ type="text"
245
+ value={query}
246
+ onChange={(e) => setQuery(e.target.value)}
247
+ placeholder="Search commands..."
248
+ className="flex-1 bg-transparent text-text-primary text-sm outline-none placeholder:text-text-dim font-[lixFont]"
249
+ onKeyDown={handleKeyDown}
250
+ />
251
+ </div>
252
+
253
+ {/* Results */}
254
+ <div ref={listRef} className="max-h-[55vh] overflow-y-auto no-scrollbar py-2">
255
+ {filtered.length === 0 && (
256
+ <div className="px-4 py-6 text-center text-text-dim text-xs">
257
+ No commands found
258
+ </div>
259
+ )}
260
+
261
+ {sectionOrder.map((section) => (
262
+ <div key={section}>
263
+ <div className="px-4 pt-3 pb-1">
264
+ <span className="text-text-dim text-xs uppercase tracking-wider">
265
+ {section}
266
+ </span>
267
+ </div>
268
+ {sections[section].map((cmd) => {
269
+ flatIndex++
270
+ const idx = flatIndex
271
+ return (
272
+ <button
273
+ key={cmd.label}
274
+ data-index={idx}
275
+ onClick={() => handleCommand(cmd)}
276
+ className={`w-full flex items-center justify-between px-4 py-2 text-text-secondary text-xs transition-all duration-100 ${
277
+ idx === selectedIndex
278
+ ? 'bg-surface-hover text-text-primary'
279
+ : 'hover:bg-surface-hover/60'
280
+ }`}
281
+ >
282
+ <span className="flex items-center gap-3">
283
+ <i className={`bx ${cmd.icon} text-base text-text-muted`} />
284
+ {cmd.label}
285
+ </span>
286
+ {cmd.shortcut && (
287
+ <div className="flex items-center gap-0.5">
288
+ {cmd.shortcut.split('+').map((key, i) => (
289
+ <span key={i}>
290
+ {i > 0 && <span className="text-text-dim text-xs mx-0.5">+</span>}
291
+ <kbd className="px-1.5 py-0.5 bg-surface-dark rounded text-text-dim text-xs border border-border">
292
+ {key.trim()}
293
+ </kbd>
294
+ </span>
295
+ ))}
296
+ </div>
297
+ )}
298
+ </button>
299
+ )
300
+ })}
301
+ </div>
302
+ ))}
303
+ </div>
304
+ </div>
305
+ </div>
306
+ )
307
+ }