makersclaw 0.1.0 → 0.3.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 (478) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -1
  3. package/THIRD-PARTY-NOTICES.md +180 -0
  4. package/package.json +15 -8
  5. package/r/annotate.json +62 -0
  6. package/r/approval-request.json +52 -0
  7. package/r/audio-player.json +74 -0
  8. package/r/auth-form.json +55 -0
  9. package/r/board.json +55 -0
  10. package/r/calendar-view.json +105 -0
  11. package/r/cart.json +62 -0
  12. package/r/chart.json +83 -0
  13. package/r/chat.json +95 -0
  14. package/r/code-editor.json +94 -0
  15. package/r/comment-box.json +64 -0
  16. package/r/contact-section.json +53 -0
  17. package/r/core.json +341 -0
  18. package/r/csv-import.json +63 -0
  19. package/r/data-table.json +69 -0
  20. package/r/diagram.json +74 -0
  21. package/r/doc-editor.json +82 -0
  22. package/r/doc-page.json +61 -0
  23. package/r/email-render.json +73 -0
  24. package/r/faq.json +50 -0
  25. package/r/file-upload.json +54 -0
  26. package/r/flow-canvas.json +93 -0
  27. package/r/footer.json +52 -0
  28. package/r/gantt.json +70 -0
  29. package/r/job-panel.json +56 -0
  30. package/r/json-editor.json +66 -0
  31. package/r/map.json +89 -0
  32. package/r/menubar.json +52 -0
  33. package/r/mind-map.json +67 -0
  34. package/r/navigation-menu.json +52 -0
  35. package/r/order-detail.json +54 -0
  36. package/r/org-chart.json +66 -0
  37. package/r/page-builder.json +105 -0
  38. package/r/payment-methods.json +51 -0
  39. package/r/pdf-document.json +79 -0
  40. package/r/pdf-viewer.json +88 -0
  41. package/r/product-detail.json +76 -0
  42. package/r/product-grid.json +65 -0
  43. package/r/record-form.json +67 -0
  44. package/r/record-notes.json +55 -0
  45. package/r/registry.json +3612 -0
  46. package/r/search-dialog.json +51 -0
  47. package/r/settings-list.json +66 -0
  48. package/r/setup-checklist.json +51 -0
  49. package/r/share-dialog.json +52 -0
  50. package/r/sidebar.json +69 -0
  51. package/r/signature-pad.json +72 -0
  52. package/r/sketch-canvas.json +99 -0
  53. package/r/spreadsheet.json +90 -0
  54. package/r/stat-cards.json +66 -0
  55. package/r/task-list.json +63 -0
  56. package/r/task-model.json +98 -0
  57. package/r/testimonials.json +52 -0
  58. package/r/transaction-list.json +53 -0
  59. package/r/whiteboard.json +66 -0
  60. package/registry/blocks/annotate/README.md +179 -0
  61. package/registry/blocks/annotate/annotate-data.js +250 -0
  62. package/registry/blocks/annotate/annotate.jsx +646 -0
  63. package/registry/blocks/annotate/block.json +10 -0
  64. package/registry/blocks/annotate/demo.jsx +135 -0
  65. package/registry/blocks/annotate/index.js +15 -0
  66. package/registry/blocks/approval-request/README.md +116 -0
  67. package/registry/blocks/approval-request/approval-request.jsx +386 -0
  68. package/registry/blocks/approval-request/block.json +10 -0
  69. package/registry/blocks/approval-request/demo.jsx +127 -0
  70. package/registry/blocks/approval-request/index.js +4 -0
  71. package/registry/blocks/audio-player/README.md +167 -0
  72. package/registry/blocks/audio-player/audio-player.jsx +623 -0
  73. package/registry/blocks/audio-player/block.json +14 -0
  74. package/registry/blocks/audio-player/colors.js +109 -0
  75. package/registry/blocks/audio-player/demo.jsx +209 -0
  76. package/registry/blocks/audio-player/index.js +6 -0
  77. package/registry/blocks/audio-player/time.js +67 -0
  78. package/registry/blocks/audio-player/wavesurfer.js +9 -0
  79. package/registry/blocks/auth-form/README.md +126 -0
  80. package/registry/blocks/auth-form/auth-form.jsx +568 -0
  81. package/registry/blocks/auth-form/block.json +10 -0
  82. package/registry/blocks/auth-form/demo.jsx +131 -0
  83. package/registry/blocks/auth-form/index.js +3 -0
  84. package/registry/blocks/board/README.md +64 -13
  85. package/registry/blocks/board/block.json +2 -2
  86. package/registry/blocks/board/board.jsx +237 -94
  87. package/registry/blocks/board/demo.jsx +103 -24
  88. package/registry/blocks/calendar-view/README.md +180 -0
  89. package/registry/blocks/calendar-view/agenda.jsx +147 -0
  90. package/registry/blocks/calendar-view/block.json +76 -0
  91. package/registry/blocks/calendar-view/calendar-model.js +442 -0
  92. package/registry/blocks/calendar-view/calendar.jsx +627 -0
  93. package/registry/blocks/calendar-view/demo.jsx +195 -0
  94. package/registry/blocks/calendar-view/draft.jsx +49 -0
  95. package/registry/blocks/calendar-view/index.js +39 -0
  96. package/registry/blocks/calendar-view/month.jsx +346 -0
  97. package/registry/blocks/calendar-view/paint.js +69 -0
  98. package/registry/blocks/calendar-view/time-grid.jsx +527 -0
  99. package/registry/blocks/calendar-view/tray.jsx +87 -0
  100. package/registry/blocks/cart/README.md +146 -0
  101. package/registry/blocks/cart/block.json +10 -0
  102. package/registry/blocks/cart/cart.jsx +597 -0
  103. package/registry/blocks/cart/demo.jsx +98 -0
  104. package/registry/blocks/cart/index.js +4 -0
  105. package/registry/blocks/cart/money.js +28 -0
  106. package/registry/blocks/chart/README.md +316 -0
  107. package/registry/blocks/chart/block.json +10 -0
  108. package/registry/blocks/chart/cartesian.jsx +642 -0
  109. package/registry/blocks/chart/core.jsx +262 -0
  110. package/registry/blocks/chart/demo.jsx +357 -0
  111. package/registry/blocks/chart/heatmap.jsx +336 -0
  112. package/registry/blocks/chart/index.js +8 -0
  113. package/registry/blocks/chart/pie.jsx +245 -0
  114. package/registry/blocks/chart/sparkline.jsx +88 -0
  115. package/registry/blocks/chat/README.md +273 -0
  116. package/registry/blocks/chat/attachment.jsx +95 -0
  117. package/registry/blocks/chat/block.json +10 -0
  118. package/registry/blocks/chat/chat.jsx +132 -0
  119. package/registry/blocks/chat/composer.jsx +364 -0
  120. package/registry/blocks/chat/demo.jsx +578 -0
  121. package/registry/blocks/chat/header.jsx +34 -0
  122. package/registry/blocks/chat/index.js +9 -0
  123. package/registry/blocks/chat/message.jsx +450 -0
  124. package/registry/blocks/chat/scroller.jsx +247 -0
  125. package/registry/blocks/chat/text.jsx +104 -0
  126. package/registry/blocks/code-editor/README.md +210 -0
  127. package/registry/blocks/code-editor/block.json +22 -0
  128. package/registry/blocks/code-editor/code-editor.jsx +534 -0
  129. package/registry/blocks/code-editor/codemirror.js +38 -0
  130. package/registry/blocks/code-editor/demo.jsx +205 -0
  131. package/registry/blocks/code-editor/index.js +8 -0
  132. package/registry/blocks/code-editor/json.js +317 -0
  133. package/registry/blocks/code-editor/sql.js +136 -0
  134. package/registry/blocks/code-editor/text.js +101 -0
  135. package/registry/blocks/code-editor/theme.js +186 -0
  136. package/registry/blocks/comment-box/README.md +123 -0
  137. package/registry/blocks/comment-box/block.json +10 -0
  138. package/registry/blocks/comment-box/comment-body.jsx +217 -0
  139. package/registry/blocks/comment-box/comment-box.jsx +198 -0
  140. package/registry/blocks/comment-box/comment-time.jsx +77 -0
  141. package/registry/blocks/comment-box/demo.jsx +141 -0
  142. package/registry/blocks/comment-box/index.js +8 -0
  143. package/registry/blocks/contact-section/README.md +152 -0
  144. package/registry/blocks/contact-section/block.json +11 -0
  145. package/registry/blocks/contact-section/contact-section.jsx +509 -0
  146. package/registry/blocks/contact-section/demo.jsx +93 -0
  147. package/registry/blocks/contact-section/index.js +4 -0
  148. package/registry/blocks/csv-import/README.md +133 -0
  149. package/registry/blocks/csv-import/block.json +13 -0
  150. package/registry/blocks/csv-import/csv-import.jsx +479 -0
  151. package/registry/blocks/csv-import/demo.jsx +106 -0
  152. package/registry/blocks/csv-import/import.js +200 -0
  153. package/registry/blocks/csv-import/index.js +7 -0
  154. package/registry/blocks/data-table/README.md +386 -0
  155. package/registry/blocks/data-table/block.json +10 -0
  156. package/registry/blocks/data-table/data-table.jsx +1164 -0
  157. package/registry/blocks/data-table/demo.jsx +588 -0
  158. package/registry/blocks/data-table/index.js +5 -0
  159. package/registry/blocks/data-table/model.js +169 -0
  160. package/registry/blocks/diagram/README.md +108 -0
  161. package/registry/blocks/diagram/block.json +13 -0
  162. package/registry/blocks/diagram/demo.jsx +113 -0
  163. package/registry/blocks/diagram/diagram.jsx +481 -0
  164. package/registry/blocks/diagram/index.js +7 -0
  165. package/registry/blocks/diagram/mermaid.js +52 -0
  166. package/registry/blocks/diagram/palette.js +313 -0
  167. package/registry/blocks/diagram/source.js +98 -0
  168. package/registry/blocks/doc-editor/README.md +215 -0
  169. package/registry/blocks/doc-editor/block.json +15 -0
  170. package/registry/blocks/doc-editor/demo.jsx +225 -0
  171. package/registry/blocks/doc-editor/doc-editor.jsx +291 -0
  172. package/registry/blocks/doc-editor/doc.js +309 -0
  173. package/registry/blocks/doc-editor/index.js +7 -0
  174. package/registry/blocks/doc-editor/schema.jsx +56 -0
  175. package/registry/blocks/doc-editor/shadcn-classes.js +110 -0
  176. package/registry/blocks/doc-editor/theme.js +82 -0
  177. package/registry/blocks/doc-page/README.md +119 -0
  178. package/registry/blocks/doc-page/autosave.js +104 -0
  179. package/registry/blocks/doc-page/block.json +10 -0
  180. package/registry/blocks/doc-page/demo.jsx +137 -0
  181. package/registry/blocks/doc-page/doc-page.jsx +276 -0
  182. package/registry/blocks/doc-page/index.js +4 -0
  183. package/registry/blocks/email-render/README.md +151 -0
  184. package/registry/blocks/email-render/block.json +14 -0
  185. package/registry/blocks/email-render/demo.jsx +153 -0
  186. package/registry/blocks/email-render/doc-email.jsx +210 -0
  187. package/registry/blocks/email-render/email.jsx +221 -0
  188. package/registry/blocks/email-render/index.js +10 -0
  189. package/registry/blocks/email-render/render.js +50 -0
  190. package/registry/blocks/email-render/theme.js +215 -0
  191. package/registry/blocks/faq/README.md +101 -0
  192. package/registry/blocks/faq/block.json +11 -0
  193. package/registry/blocks/faq/demo.jsx +89 -0
  194. package/registry/blocks/faq/faq.jsx +220 -0
  195. package/registry/blocks/faq/index.js +4 -0
  196. package/registry/blocks/file-upload/README.md +138 -0
  197. package/registry/blocks/file-upload/block.json +10 -0
  198. package/registry/blocks/file-upload/demo.jsx +204 -0
  199. package/registry/blocks/file-upload/file-upload.jsx +635 -0
  200. package/registry/blocks/file-upload/index.js +4 -0
  201. package/registry/blocks/flow-canvas/README.md +420 -0
  202. package/registry/blocks/flow-canvas/block.json +13 -0
  203. package/registry/blocks/flow-canvas/board.js +819 -0
  204. package/registry/blocks/flow-canvas/chrome.jsx +649 -0
  205. package/registry/blocks/flow-canvas/context.jsx +35 -0
  206. package/registry/blocks/flow-canvas/demo.jsx +249 -0
  207. package/registry/blocks/flow-canvas/edges.jsx +107 -0
  208. package/registry/blocks/flow-canvas/flow-canvas.jsx +1329 -0
  209. package/registry/blocks/flow-canvas/index.js +41 -0
  210. package/registry/blocks/flow-canvas/nodes.jsx +392 -0
  211. package/registry/blocks/flow-canvas/theme.js +174 -0
  212. package/registry/blocks/footer/README.md +133 -0
  213. package/registry/blocks/footer/block.json +11 -0
  214. package/registry/blocks/footer/demo.jsx +119 -0
  215. package/registry/blocks/footer/footer.jsx +406 -0
  216. package/registry/blocks/footer/index.js +4 -0
  217. package/registry/blocks/gantt/README.md +220 -0
  218. package/registry/blocks/gantt/block.json +10 -0
  219. package/registry/blocks/gantt/demo.jsx +148 -0
  220. package/registry/blocks/gantt/gantt.jsx +923 -0
  221. package/registry/blocks/gantt/index.js +7 -0
  222. package/registry/blocks/gantt/rows.jsx +468 -0
  223. package/registry/blocks/gantt/schedule.js +278 -0
  224. package/registry/blocks/job-panel/README.md +86 -2
  225. package/registry/blocks/job-panel/block.json +3 -3
  226. package/registry/blocks/job-panel/demo.jsx +230 -3
  227. package/registry/blocks/job-panel/job-panel.jsx +169 -3
  228. package/registry/blocks/json-editor/README.md +133 -0
  229. package/registry/blocks/json-editor/block.json +10 -0
  230. package/registry/blocks/json-editor/demo.jsx +92 -0
  231. package/registry/blocks/json-editor/index.js +7 -0
  232. package/registry/blocks/json-editor/json-editor.jsx +305 -0
  233. package/registry/blocks/json-editor/json-tree.jsx +393 -0
  234. package/registry/blocks/json-editor/tree.js +225 -0
  235. package/registry/blocks/map/README.md +221 -0
  236. package/registry/blocks/map/block.json +14 -0
  237. package/registry/blocks/map/demo.jsx +201 -0
  238. package/registry/blocks/map/geo.js +198 -0
  239. package/registry/blocks/map/index.js +8 -0
  240. package/registry/blocks/map/list.jsx +128 -0
  241. package/registry/blocks/map/map-view.jsx +715 -0
  242. package/registry/blocks/map/maplibre.js +30 -0
  243. package/registry/blocks/map/marks.jsx +146 -0
  244. package/registry/blocks/map/theme.js +142 -0
  245. package/registry/blocks/menubar/README.md +109 -0
  246. package/registry/blocks/menubar/block.json +10 -0
  247. package/registry/blocks/menubar/demo.jsx +128 -0
  248. package/registry/blocks/menubar/index.js +4 -0
  249. package/registry/blocks/menubar/menubar.jsx +199 -0
  250. package/registry/blocks/mind-map/README.md +206 -0
  251. package/registry/blocks/mind-map/block.json +10 -0
  252. package/registry/blocks/mind-map/demo.jsx +103 -0
  253. package/registry/blocks/mind-map/index.js +31 -0
  254. package/registry/blocks/mind-map/mind-map.jsx +1125 -0
  255. package/registry/blocks/mind-map/topic.jsx +143 -0
  256. package/registry/blocks/mind-map/tree.js +645 -0
  257. package/registry/blocks/navigation-menu/README.md +124 -0
  258. package/registry/blocks/navigation-menu/block.json +10 -0
  259. package/registry/blocks/navigation-menu/demo.jsx +84 -0
  260. package/registry/blocks/navigation-menu/index.js +4 -0
  261. package/registry/blocks/navigation-menu/navigation-menu.jsx +448 -0
  262. package/registry/blocks/order-detail/README.md +109 -0
  263. package/registry/blocks/order-detail/block.json +10 -0
  264. package/registry/blocks/order-detail/demo.jsx +88 -0
  265. package/registry/blocks/order-detail/index.js +4 -0
  266. package/registry/blocks/order-detail/order-detail.jsx +354 -0
  267. package/registry/blocks/org-chart/README.md +145 -0
  268. package/registry/blocks/org-chart/block.json +10 -0
  269. package/registry/blocks/org-chart/demo.jsx +94 -0
  270. package/registry/blocks/org-chart/index.js +7 -0
  271. package/registry/blocks/org-chart/org-chart.jsx +631 -0
  272. package/registry/blocks/org-chart/org.js +245 -0
  273. package/registry/blocks/org-chart/person.jsx +96 -0
  274. package/registry/blocks/page-builder/README.md +257 -0
  275. package/registry/blocks/page-builder/block.json +14 -0
  276. package/registry/blocks/page-builder/demo.jsx +145 -0
  277. package/registry/blocks/page-builder/grapes.js +18 -0
  278. package/registry/blocks/page-builder/index.js +11 -0
  279. package/registry/blocks/page-builder/page-builder.jsx +704 -0
  280. package/registry/blocks/page-builder/page-view.jsx +33 -0
  281. package/registry/blocks/page-builder/page.js +118 -0
  282. package/registry/blocks/page-builder/panels.jsx +588 -0
  283. package/registry/blocks/page-builder/sections.js +209 -0
  284. package/registry/blocks/page-builder/theme.js +87 -0
  285. package/registry/blocks/page-builder/thumbs.jsx +189 -0
  286. package/registry/blocks/page-builder/tokens.js +329 -0
  287. package/registry/blocks/payment-methods/README.md +104 -0
  288. package/registry/blocks/payment-methods/block.json +10 -0
  289. package/registry/blocks/payment-methods/demo.jsx +71 -0
  290. package/registry/blocks/payment-methods/index.js +4 -0
  291. package/registry/blocks/payment-methods/payment-methods.jsx +302 -0
  292. package/registry/blocks/pdf-document/README.md +213 -0
  293. package/registry/blocks/pdf-document/block.json +13 -0
  294. package/registry/blocks/pdf-document/demo.jsx +136 -0
  295. package/registry/blocks/pdf-document/index.js +33 -0
  296. package/registry/blocks/pdf-document/money.js +83 -0
  297. package/registry/blocks/pdf-document/pdf-document.jsx +144 -0
  298. package/registry/blocks/pdf-document/primitives.jsx +271 -0
  299. package/registry/blocks/pdf-document/quote.jsx +179 -0
  300. package/registry/blocks/pdf-document/theme.js +193 -0
  301. package/registry/blocks/pdf-viewer/README.md +191 -0
  302. package/registry/blocks/pdf-viewer/block.json +14 -0
  303. package/registry/blocks/pdf-viewer/demo.jsx +252 -0
  304. package/registry/blocks/pdf-viewer/helpers.js +270 -0
  305. package/registry/blocks/pdf-viewer/index.js +8 -0
  306. package/registry/blocks/pdf-viewer/notes.jsx +195 -0
  307. package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1368 -0
  308. package/registry/blocks/pdf-viewer/pdfjs.js +37 -0
  309. package/registry/blocks/pdf-viewer/ssr-shim-end.js +6 -0
  310. package/registry/blocks/pdf-viewer/ssr-shim.js +44 -0
  311. package/registry/blocks/product-detail/README.md +217 -0
  312. package/registry/blocks/product-detail/block.json +10 -0
  313. package/registry/blocks/product-detail/demo.jsx +233 -0
  314. package/registry/blocks/product-detail/index.js +7 -0
  315. package/registry/blocks/product-detail/price.jsx +115 -0
  316. package/registry/blocks/product-detail/product-detail.jsx +348 -0
  317. package/registry/blocks/product-detail/product-media.jsx +102 -0
  318. package/registry/blocks/product-detail/product-options.jsx +144 -0
  319. package/registry/blocks/product-grid/README.md +162 -0
  320. package/registry/blocks/product-grid/block.json +10 -0
  321. package/registry/blocks/product-grid/demo.jsx +255 -0
  322. package/registry/blocks/product-grid/index.js +5 -0
  323. package/registry/blocks/product-grid/product-card.jsx +232 -0
  324. package/registry/blocks/product-grid/product-grid.jsx +235 -0
  325. package/registry/blocks/record-form/README.md +196 -0
  326. package/registry/blocks/record-form/block.json +10 -0
  327. package/registry/blocks/record-form/demo.jsx +525 -0
  328. package/registry/blocks/record-form/index.js +4 -0
  329. package/registry/blocks/record-form/record-fields.jsx +457 -0
  330. package/registry/blocks/record-form/record-form.jsx +415 -0
  331. package/registry/blocks/record-form/rules.js +81 -0
  332. package/registry/blocks/record-notes/README.md +139 -0
  333. package/registry/blocks/record-notes/block.json +10 -0
  334. package/registry/blocks/record-notes/demo.jsx +179 -0
  335. package/registry/blocks/record-notes/index.js +4 -0
  336. package/registry/blocks/record-notes/record-notes.jsx +266 -0
  337. package/registry/blocks/search-dialog/README.md +108 -0
  338. package/registry/blocks/search-dialog/block.json +10 -0
  339. package/registry/blocks/search-dialog/demo.jsx +116 -0
  340. package/registry/blocks/search-dialog/index.js +3 -0
  341. package/registry/blocks/search-dialog/search-dialog.jsx +235 -0
  342. package/registry/blocks/settings-list/README.md +171 -0
  343. package/registry/blocks/settings-list/block.json +10 -0
  344. package/registry/blocks/settings-list/demo.jsx +333 -0
  345. package/registry/blocks/settings-list/index.js +4 -0
  346. package/registry/blocks/settings-list/row-state.jsx +93 -0
  347. package/registry/blocks/settings-list/settings-list.jsx +349 -0
  348. package/registry/blocks/settings-list/settings-matrix.jsx +118 -0
  349. package/registry/blocks/setup-checklist/README.md +142 -0
  350. package/registry/blocks/setup-checklist/block.json +10 -0
  351. package/registry/blocks/setup-checklist/demo.jsx +159 -0
  352. package/registry/blocks/setup-checklist/index.js +4 -0
  353. package/registry/blocks/setup-checklist/setup-checklist.jsx +446 -0
  354. package/registry/blocks/share-dialog/README.md +113 -0
  355. package/registry/blocks/share-dialog/block.json +10 -0
  356. package/registry/blocks/share-dialog/demo.jsx +140 -0
  357. package/registry/blocks/share-dialog/index.js +3 -0
  358. package/registry/blocks/share-dialog/share-dialog.jsx +384 -0
  359. package/registry/blocks/sidebar/README.md +190 -0
  360. package/registry/blocks/sidebar/block.json +43 -0
  361. package/registry/blocks/sidebar/demo.jsx +577 -0
  362. package/registry/blocks/sidebar/index.js +4 -0
  363. package/registry/blocks/sidebar/sidebar-panel.jsx +149 -0
  364. package/registry/blocks/sidebar/sidebar.jsx +864 -0
  365. package/registry/blocks/signature-pad/README.md +137 -0
  366. package/registry/blocks/signature-pad/block.json +13 -0
  367. package/registry/blocks/signature-pad/colors.js +115 -0
  368. package/registry/blocks/signature-pad/demo.jsx +94 -0
  369. package/registry/blocks/signature-pad/index.js +6 -0
  370. package/registry/blocks/signature-pad/pad.js +7 -0
  371. package/registry/blocks/signature-pad/signature-pad.jsx +434 -0
  372. package/registry/blocks/signature-pad/signature.js +68 -0
  373. package/registry/blocks/sketch-canvas/README.md +280 -0
  374. package/registry/blocks/sketch-canvas/block.json +13 -0
  375. package/registry/blocks/sketch-canvas/demo.jsx +159 -0
  376. package/registry/blocks/sketch-canvas/excalidraw.js +40 -0
  377. package/registry/blocks/sketch-canvas/index.js +9 -0
  378. package/registry/blocks/sketch-canvas/palette.js +226 -0
  379. package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1188 -0
  380. package/registry/blocks/sketch-canvas/sketch.js +179 -0
  381. package/registry/blocks/sketch-canvas/ssr-shim-end.js +6 -0
  382. package/registry/blocks/sketch-canvas/ssr-shim.js +45 -0
  383. package/registry/blocks/sketch-canvas/stamps.js +104 -0
  384. package/registry/blocks/sketch-canvas/theme.js +131 -0
  385. package/registry/blocks/spreadsheet/README.md +307 -0
  386. package/registry/blocks/spreadsheet/block.json +13 -0
  387. package/registry/blocks/spreadsheet/cells.jsx +455 -0
  388. package/registry/blocks/spreadsheet/chrome.jsx +325 -0
  389. package/registry/blocks/spreadsheet/demo.jsx +177 -0
  390. package/registry/blocks/spreadsheet/grid.js +387 -0
  391. package/registry/blocks/spreadsheet/index.js +34 -0
  392. package/registry/blocks/spreadsheet/spreadsheet.jsx +1732 -0
  393. package/registry/blocks/spreadsheet/types.js +473 -0
  394. package/registry/blocks/stat-cards/README.md +173 -0
  395. package/registry/blocks/stat-cards/block.json +10 -0
  396. package/registry/blocks/stat-cards/demo.jsx +257 -0
  397. package/registry/blocks/stat-cards/index.js +4 -0
  398. package/registry/blocks/stat-cards/stat-cards.jsx +307 -0
  399. package/registry/blocks/stat-cards/usage.jsx +243 -0
  400. package/registry/blocks/task-list/README.md +176 -0
  401. package/registry/blocks/task-list/block.json +10 -0
  402. package/registry/blocks/task-list/demo.jsx +134 -0
  403. package/registry/blocks/task-list/index.js +5 -0
  404. package/registry/blocks/task-list/task-list.jsx +907 -0
  405. package/registry/blocks/task-model/README.md +455 -0
  406. package/registry/blocks/task-model/block.json +167 -0
  407. package/registry/blocks/task-model/changes.js +89 -0
  408. package/registry/blocks/task-model/dates.js +371 -0
  409. package/registry/blocks/task-model/demo.jsx +215 -0
  410. package/registry/blocks/task-model/dnd.jsx +240 -0
  411. package/registry/blocks/task-model/index.js +97 -0
  412. package/registry/blocks/task-model/model.js +919 -0
  413. package/registry/blocks/task-model/parts.jsx +449 -0
  414. package/registry/blocks/task-model/toolbar.jsx +271 -0
  415. package/registry/blocks/testimonials/README.md +139 -0
  416. package/registry/blocks/testimonials/block.json +11 -0
  417. package/registry/blocks/testimonials/demo.jsx +137 -0
  418. package/registry/blocks/testimonials/index.js +4 -0
  419. package/registry/blocks/testimonials/testimonials.jsx +430 -0
  420. package/registry/blocks/transaction-list/README.md +126 -0
  421. package/registry/blocks/transaction-list/block.json +10 -0
  422. package/registry/blocks/transaction-list/demo.jsx +79 -0
  423. package/registry/blocks/transaction-list/index.js +4 -0
  424. package/registry/blocks/transaction-list/transaction-list.jsx +383 -0
  425. package/registry/blocks/whiteboard/README.md +179 -0
  426. package/registry/blocks/whiteboard/block.json +10 -0
  427. package/registry/blocks/whiteboard/demo.jsx +169 -0
  428. package/registry/blocks/whiteboard/index.js +6 -0
  429. package/registry/blocks/whiteboard/templates.js +150 -0
  430. package/registry/blocks/whiteboard/whiteboard.jsx +721 -0
  431. package/src/install.mjs +18 -0
  432. package/src/shadcn.mjs +171 -0
  433. package/templates/next/files/README.md +145 -11
  434. package/templates/next/files/app/globals.css +55 -4
  435. package/templates/next/files/check/run.mjs +3 -3
  436. package/templates/next/files/components/parts/fixture.jsx +1 -1
  437. package/templates/next/files/components/parts/sample.js +15 -0
  438. package/templates/next/files/components/ui/avatar.jsx +141 -0
  439. package/templates/next/files/components/ui/breadcrumb.jsx +149 -0
  440. package/templates/next/files/components/ui/button-group.jsx +70 -0
  441. package/templates/next/files/components/ui/button.jsx +132 -12
  442. package/templates/next/files/components/ui/calendar.jsx +609 -0
  443. package/templates/next/files/components/ui/carousel.jsx +281 -0
  444. package/templates/next/files/components/ui/collapsible.jsx +208 -0
  445. package/templates/next/files/components/ui/combobox.jsx +608 -0
  446. package/templates/next/files/components/ui/command.jsx +577 -0
  447. package/templates/next/files/components/ui/context-menu.jsx +191 -0
  448. package/templates/next/files/components/ui/date-picker.jsx +360 -0
  449. package/templates/next/files/components/ui/distribution.jsx +117 -0
  450. package/templates/next/files/components/ui/feedback.jsx +143 -0
  451. package/templates/next/files/components/ui/field.jsx +115 -3
  452. package/templates/next/files/components/ui/file-input.jsx +234 -0
  453. package/templates/next/files/components/ui/icon.jsx +130 -8
  454. package/templates/next/files/components/ui/image.jsx +143 -0
  455. package/templates/next/files/components/ui/index.js +47 -2
  456. package/templates/next/files/components/ui/input-group.jsx +213 -0
  457. package/templates/next/files/components/ui/input-otp.jsx +170 -0
  458. package/templates/next/files/components/ui/item.jsx +206 -0
  459. package/templates/next/files/components/ui/menu.jsx +454 -82
  460. package/templates/next/files/components/ui/notice.jsx +126 -14
  461. package/templates/next/files/components/ui/pagination.jsx +156 -0
  462. package/templates/next/files/components/ui/popover.jsx +284 -0
  463. package/templates/next/files/components/ui/radio.jsx +193 -0
  464. package/templates/next/files/components/ui/rating.jsx +131 -0
  465. package/templates/next/files/components/ui/resizable.jsx +338 -0
  466. package/templates/next/files/components/ui/scroll-area.jsx +123 -0
  467. package/templates/next/files/components/ui/separator.jsx +119 -0
  468. package/templates/next/files/components/ui/sheet.jsx +96 -11
  469. package/templates/next/files/components/ui/slider.jsx +174 -0
  470. package/templates/next/files/components/ui/stat.jsx +574 -0
  471. package/templates/next/files/components/ui/steps.jsx +338 -0
  472. package/templates/next/files/components/ui/swatch.jsx +33 -0
  473. package/templates/next/files/components/ui/table.jsx +130 -0
  474. package/templates/next/files/components/ui/toast.jsx +440 -0
  475. package/templates/next/files/components/ui/toggle.jsx +195 -0
  476. package/templates/next/files/components/ui/tooltip.jsx +270 -0
  477. package/templates/next/files/lib/sample.js +4 -12
  478. package/templates/next/template.json +264 -8
@@ -0,0 +1,1329 @@
1
+ "use client";
2
+
3
+ // FlowCanvas — a Miro-like board on React Flow, in the workspace theme,
4
+ // behind one set of props (README.md beside this file).
5
+ //
6
+ // Client only, one static bundle: the modules load with the page (the
7
+ // app's check refuses import()), and the board itself mounts after the
8
+ // first effect (the mount guard). Until then — on the server and in the
9
+ // first client paint — a box of the same size holds the board as text, so
10
+ // the server HTML already says what is on it.
11
+ //
12
+ // State: nodes and edges live here (React Flow's shape). A change that
13
+ // should be undoable COMMITS: a drag ends, a card is placed, words stop
14
+ // being edited, a colour is picked. A commit runs the block's constraints
15
+ // (`constrain`, `beforeChange`), pushes a snapshot onto the history
16
+ // (board.js historyReducer) and schedules one onChange/onSave after
17
+ // `changeDelay` ms. Selection and the view never commit.
18
+ import "@xyflow/react/dist/style.css";
19
+ import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
20
+ import {
21
+ Background,
22
+ BackgroundVariant,
23
+ ConnectionMode,
24
+ MiniMap,
25
+ ReactFlow,
26
+ ReactFlowProvider,
27
+ SelectionMode,
28
+ ViewportPortal,
29
+ applyEdgeChanges,
30
+ applyNodeChanges,
31
+ useConnection,
32
+ useReactFlow,
33
+ useStore,
34
+ } from "@xyflow/react";
35
+ import { ContextMenu, cx } from "../../ui";
36
+ import {
37
+ AUTO_HEIGHT,
38
+ EDITABLE,
39
+ GRID,
40
+ SHAPE_NAMES,
41
+ STICKY_COLORS,
42
+ alignNodes,
43
+ arrangeNodes,
44
+ autoLayout,
45
+ boardBounds,
46
+ boardKey,
47
+ boardToMarkdown,
48
+ boardToText,
49
+ cleanBoard,
50
+ createEdge,
51
+ createNode,
52
+ distributeNodes,
53
+ duplicateNodes,
54
+ emptyBoard,
55
+ facingSides,
56
+ frameContents,
57
+ historyInit,
58
+ historyReducer,
59
+ makeId,
60
+ nodeRect,
61
+ readBoard,
62
+ sidePoint,
63
+ starterTemplates,
64
+ } from "./board";
65
+ import { ConnectMenu, EdgeToolbar, EmptyState, FloatAt, HistoryBar, ListView, PresentBar, SelectionToolbar, ShortcutsSheet, ToolFlyout, ToolRail, ZoomBar, modKey } from "./chrome";
66
+ import { CanvasContext } from "./context";
67
+ import { BUILT_IN_EDGES, connectorGeometry } from "./edges";
68
+ import { BUILT_IN_NODES } from "./nodes";
69
+ import { THEME } from "./theme";
70
+
71
+ /** A ref for the canvas handle: `const board = useFlowCanvas()`, then
72
+ * `<FlowCanvas canvasRef={board} />` and `board.current?.fitView()`. */
73
+ export function useFlowCanvas() {
74
+ return useRef(null);
75
+ }
76
+
77
+ // The built-in tools. `place` names the node type a click puts down;
78
+ // `sizable` ones can be dragged out to a size.
79
+ const TOOLS = {
80
+ select: { id: "select", label: "Select", icon: "mouse-pointer", shortcut: "V", kind: "mode" },
81
+ hand: { id: "hand", label: "Hand", icon: "hand", shortcut: "H", kind: "mode" },
82
+ sticky: { id: "sticky", label: "Sticky note", icon: "sticky-note", shortcut: "N", kind: "mode", place: "sticky" },
83
+ shape: { id: "shape", label: "Shape", icon: "square", shortcut: "S", kind: "mode", place: "shape", sizable: true },
84
+ text: { id: "text", label: "Text", icon: "type", shortcut: "T", kind: "mode", place: "text", sizable: true },
85
+ connector: { id: "connector", label: "Connector", icon: "connector", shortcut: "L", kind: "mode" },
86
+ frame: { id: "frame", label: "Frame", icon: "frame", shortcut: "F", kind: "mode", place: "frame", sizable: true },
87
+ image: { id: "image", label: "Image", icon: "image", shortcut: "I", kind: "action" },
88
+ };
89
+ const SHAPE_ICON = { rect: "square", ellipse: "circle", diamond: "diamond", pill: "minus" };
90
+ const DEFAULT_RAIL = ["select", "hand", "|", "sticky", "shape", "text", "connector", "frame"];
91
+ // Props React Flow compares by identity: made once, not every render.
92
+ const EDGE_DEFAULTS = { type: "flow" };
93
+ const MULTI_KEYS = ["Shift", "Meta", "Control"];
94
+ const MIDDLE_BUTTON = [1];
95
+ const FIT_OPTIONS = { padding: 0.2, maxZoom: 1 };
96
+ const EDITING_TARGET = '[contenteditable="true"],[contenteditable="plaintext-only"],input,textarea,select,[role="menu"],[role="dialog"],[popover]';
97
+
98
+ /** Anything board-shaped → React Flow's nodes and edges. */
99
+ function prepare(board) {
100
+ const { nodes, edges } = readBoard(board);
101
+ return {
102
+ nodes: nodes.map((n) => ({ ...n, position: { x: Number(n.position?.x) || 0, y: Number(n.position?.y) || 0 }, data: n.data ?? {}, ...(n.type === "frame" && n.zIndex === undefined ? { zIndex: -1 } : null) })),
103
+ edges: edges.map((e) => ({ ...e, type: e.type ?? "flow", data: e.data ?? {} })),
104
+ };
105
+ }
106
+
107
+ /**
108
+ * A floating toolbar over something on the board, placed in SCREEN space
109
+ * with left/top and no CSS transform (FloatAt): a kit Menu or Tooltip
110
+ * inside it anchors to its button, and CSS anchor positioning is not told
111
+ * about a transformed ancestor such as React Flow's viewport. Only this
112
+ * re-renders as the view pans and zooms.
113
+ */
114
+ function Floating({ at, children }) {
115
+ const [tx, ty, z] = useStore((s) => s.transform);
116
+ return (
117
+ <FloatAt x={at.x * z + tx} top={at.top * z + ty - at.lift} bottom={at.bottom * z + ty + 14}>
118
+ {children}
119
+ </FloatAt>
120
+ );
121
+ }
122
+
123
+ /** The zoom as a percentage; re-renders alone as the view zooms. */
124
+ function ZoomLevel() {
125
+ const z = useStore((s) => s.transform[2]);
126
+ return <>{Math.round((z || 1) * 100)}%</>;
127
+ }
128
+
129
+ function usePrefersReducedMotion() {
130
+ const [reduce, setReduce] = useState(false);
131
+ useEffect(() => {
132
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
133
+ const on = () => setReduce(mq.matches);
134
+ on();
135
+ mq.addEventListener("change", on);
136
+ return () => mq.removeEventListener("change", on);
137
+ }, []);
138
+ return reduce;
139
+ }
140
+
141
+ export function FlowCanvas(props) {
142
+ const [mounted, setMounted] = useState(false);
143
+ useEffect(() => setMounted(true), []);
144
+ if (!mounted) return <Placeholder {...props} />;
145
+ return (
146
+ <ReactFlowProvider>
147
+ <LiveCanvas {...props} />
148
+ </ReactFlowProvider>
149
+ );
150
+ }
151
+
152
+ /** The server's (and the first paint's) board: its box, and its words for
153
+ * a reader. Not interactive. */
154
+ function Placeholder({ value, defaultValue, height = 560, bare = false, className, label = "Board", describeNode }) {
155
+ const text = boardToText(value ?? defaultValue ?? emptyBoard(), { describe: describeNode });
156
+ return (
157
+ <div role="region" aria-label={label} aria-busy="true" className={cx("relative overflow-hidden bg-background", !bare && "rounded-lg border border-border", className)} style={{ height }}>
158
+ <p className="sr-only whitespace-pre-line">{text}</p>
159
+ </div>
160
+ );
161
+ }
162
+
163
+ function LiveCanvas(props) {
164
+ const {
165
+ value,
166
+ defaultValue,
167
+ readOnly: readOnlyProp = false,
168
+ presentation = false,
169
+ onExitPresentation,
170
+ nodeTypes: ownNodeTypes,
171
+ tools: ownTools,
172
+ rail,
173
+ topBar,
174
+ toolbarExtra,
175
+ contextMenuItems,
176
+ emptyState,
177
+ templates: templatesProp,
178
+ renderCard,
179
+ describeNode,
180
+ palette = STICKY_COLORS,
181
+ snap: snapProp = true,
182
+ grid = GRID,
183
+ minimap: minimapProp = false,
184
+ height = 560,
185
+ bare = false,
186
+ label = "Board",
187
+ hideAttribution = false,
188
+ className,
189
+ canvasRef,
190
+ } = props;
191
+ const readOnly = readOnlyProp || presentation;
192
+ const rf = useReactFlow();
193
+ const reduce = usePrefersReducedMotion();
194
+ const duration = reduce ? 0 : 220;
195
+ const propsRef = useRef(props);
196
+ propsRef.current = props;
197
+
198
+ // ── the board ──────────────────────────────────────────────────────────
199
+ const initial = useRef(null);
200
+ if (!initial.current) initial.current = prepare(value ?? defaultValue ?? emptyBoard());
201
+ const [nodes, setNodes] = useState(initial.current.nodes);
202
+ const [edges, setEdges] = useState(initial.current.edges);
203
+ const nodesRef = useRef(nodes);
204
+ const edgesRef = useRef(edges);
205
+ nodesRef.current = nodes;
206
+ edgesRef.current = edges;
207
+ const [history, dispatch] = useReducer(historyReducer, initial.current, historyInit);
208
+ const historyRef = useRef(history);
209
+ historyRef.current = history;
210
+ const lastKey = useRef(boardKey(initial.current));
211
+ const viewportRef = useRef(readBoard(value ?? defaultValue).viewport);
212
+
213
+ // ── ui state ───────────────────────────────────────────────────────────
214
+ const [tool, setTool] = useState("select");
215
+ const [stickyColor, setStickyColor] = useState(palette[0] ?? "warning");
216
+ const [shapeKind, setShapeKind] = useState("rect");
217
+ const [editing, setEditing] = useState({ node: null, edge: null });
218
+ const [connectMenu, setConnectMenu] = useState(null);
219
+ const [listView, setListView] = useState(false);
220
+ const [minimap, setMinimap] = useState(minimapProp);
221
+ const [snap, setSnap] = useState(snapProp);
222
+ const [sheet, setSheet] = useState(false);
223
+ const [save, setSave] = useState({ state: "idle" });
224
+ const [draft, setDraft] = useState(null);
225
+ const [narrow, setNarrow] = useState(false);
226
+ const [announce, setAnnounce] = useState("");
227
+ const [slide, setSlide] = useState(0);
228
+ const [ctxOn, setCtxOn] = useState("pane");
229
+ const wrapRef = useRef(null);
230
+ const frameMembers = useRef(new Map());
231
+ const clip = useRef(null);
232
+ const pasteCount = useRef(0);
233
+ const pointer = useRef(null);
234
+ const suppressClick = useRef(false);
235
+ const ctxPoint = useRef(null);
236
+ const connecting = useConnection((c) => c.inProgress);
237
+ const marquee = useStore((s) => s.userSelectionActive);
238
+ const say = (msg) => setAnnounce((m) => (m === msg ? `${msg} ` : msg));
239
+
240
+ useEffect(() => setSnap(snapProp), [snapProp]);
241
+ useEffect(() => setMinimap(minimapProp), [minimapProp]);
242
+
243
+ // The rail moves to the bottom when the CANVAS is narrow (not the window).
244
+ useEffect(() => {
245
+ const el = wrapRef.current;
246
+ if (!el || typeof ResizeObserver === "undefined") return;
247
+ const ro = new ResizeObserver(([e]) => setNarrow(e.contentRect.width < 768));
248
+ ro.observe(el);
249
+ return () => ro.disconnect();
250
+ }, []);
251
+
252
+ // ── committing, history, onChange / onSave ─────────────────────────────
253
+ const emitTimer = useRef(0);
254
+ const dirty = useRef(false);
255
+ const saveSeq = useRef(0);
256
+ const currentBoard = () => cleanBoard({ nodes: nodesRef.current, edges: edgesRef.current, viewport: rf.getViewport?.() ?? viewportRef.current });
257
+
258
+ const runSave = async (board) => {
259
+ const onSave = propsRef.current.onSave;
260
+ if (!onSave) return;
261
+ const seq = ++saveSeq.current;
262
+ setSave({ state: "saving" });
263
+ let res;
264
+ try {
265
+ res = await onSave(board);
266
+ } catch (e) {
267
+ res = { ok: false, error: e?.message };
268
+ }
269
+ if (seq !== saveSeq.current) return;
270
+ setSave(res && res.ok === false ? { state: "error", error: res.error || "Not saved" } : { state: "saved" });
271
+ };
272
+ const emitNow = () => {
273
+ clearTimeout(emitTimer.current);
274
+ if (!dirty.current) return;
275
+ dirty.current = false;
276
+ const board = currentBoard();
277
+ lastKey.current = boardKey(board);
278
+ propsRef.current.onChange?.(board);
279
+ runSave(board);
280
+ };
281
+ const scheduleEmit = () => {
282
+ dirty.current = true;
283
+ clearTimeout(emitTimer.current);
284
+ emitTimer.current = setTimeout(emitNow, propsRef.current.changeDelay ?? 400);
285
+ };
286
+ // Leaving with a change not yet sent: send it now.
287
+ useEffect(
288
+ () => () => {
289
+ if (dirty.current) emitNow();
290
+ },
291
+ // eslint-disable-next-line react-hooks/exhaustive-deps
292
+ [],
293
+ );
294
+
295
+ const [tick, setTick] = useState(0);
296
+ const pending = useRef(null);
297
+ const commit = useCallback((reason = "edit", ids) => {
298
+ const prev = pending.current;
299
+ pending.current = { reason, ids: [...(prev?.ids ?? []), ...(ids ?? [])] };
300
+ setTick((t) => t + 1);
301
+ }, []);
302
+
303
+ useEffect(() => {
304
+ const p = pending.current;
305
+ if (!p) return;
306
+ pending.current = null;
307
+ const { constrain, beforeChange } = propsRef.current;
308
+ let ns = nodesRef.current;
309
+ const touched = new Set(p.ids);
310
+ if (touched.size) {
311
+ ns = ns.map((n) => {
312
+ if (!touched.has(n.id)) return n;
313
+ let next = n;
314
+ // A card whose height follows its words drops the height a resize gave it.
315
+ if (p.reason === "resize" && AUTO_HEIGHT.has(n.type) && next.height !== undefined) {
316
+ const { height: _h, ...rest } = next;
317
+ next = rest;
318
+ }
319
+ if (constrain) {
320
+ const out = constrain(next, { reason: p.reason, board: { nodes: ns, edges: edgesRef.current } });
321
+ if (out && typeof out === "object") next = { ...next, ...out };
322
+ }
323
+ return next;
324
+ });
325
+ }
326
+ let next = { nodes: ns, edges: edgesRef.current };
327
+ if (beforeChange) {
328
+ const out = beforeChange(cleanBoard(next, { viewport: false }), historyRef.current.present, p.reason);
329
+ if (out === false) {
330
+ const back = prepare(historyRef.current.present);
331
+ const sel = new Set(ns.filter((n) => n.selected).map((n) => n.id));
332
+ setNodes(back.nodes.map((n) => (sel.has(n.id) ? { ...n, selected: true } : n)));
333
+ setEdges(back.edges);
334
+ return;
335
+ }
336
+ if (out && typeof out === "object") {
337
+ const sel = new Set(ns.filter((n) => n.selected).map((n) => n.id));
338
+ const b = prepare(out);
339
+ next = { nodes: b.nodes.map((n) => (sel.has(n.id) ? { ...n, selected: true } : n)), edges: b.edges };
340
+ }
341
+ }
342
+ if (next.nodes !== nodesRef.current) setNodes(next.nodes);
343
+ if (next.edges !== edgesRef.current) setEdges(next.edges);
344
+ const before = historyRef.current.key;
345
+ dispatch({ type: "push", present: next });
346
+ if (boardKey(next) !== before) scheduleEmit();
347
+ // eslint-disable-next-line react-hooks/exhaustive-deps
348
+ }, [tick]);
349
+
350
+ const restore = (board) => {
351
+ const b = prepare(board);
352
+ setNodes(b.nodes);
353
+ setEdges(b.edges);
354
+ setEditing({ node: null, edge: null });
355
+ };
356
+ const undo = () => {
357
+ const h = historyRef.current;
358
+ if (!h.past.length) return;
359
+ restore(h.past[h.past.length - 1]);
360
+ dispatch({ type: "undo" });
361
+ scheduleEmit();
362
+ say("Undone");
363
+ };
364
+ const redo = () => {
365
+ const h = historyRef.current;
366
+ if (!h.future.length) return;
367
+ restore(h.future[0]);
368
+ dispatch({ type: "redo" });
369
+ scheduleEmit();
370
+ say("Redone");
371
+ };
372
+
373
+ // Controlled: a `value` that is not what we last sent replaces the board
374
+ // (and can be undone).
375
+ useEffect(() => {
376
+ if (value === undefined) return;
377
+ const k = boardKey(value);
378
+ if (k === lastKey.current) return;
379
+ lastKey.current = k;
380
+ const b = prepare(value);
381
+ const sel = new Set(nodesRef.current.filter((n) => n.selected).map((n) => n.id));
382
+ setNodes(b.nodes.map((n) => (sel.has(n.id) ? { ...n, selected: true } : n)));
383
+ setEdges(b.edges);
384
+ dispatch({ type: "push", present: b });
385
+ }, [value]);
386
+
387
+ // ── changing the board ─────────────────────────────────────────────────
388
+ const selectOnly = (ids = [], edgeIds = []) => {
389
+ const want = new Set(ids);
390
+ const wantE = new Set(edgeIds);
391
+ setNodes((ns) => ns.map((n) => (Boolean(n.selected) === want.has(n.id) ? n : { ...n, selected: want.has(n.id) })));
392
+ setEdges((es) => es.map((e) => (Boolean(e.selected) === wantE.has(e.id) ? e : { ...e, selected: wantE.has(e.id) })));
393
+ };
394
+
395
+ const addNodes = (list, { select = true, edit = false, reason = "create", edges: newEdges } = {}) => {
396
+ const arr = (Array.isArray(list) ? list : [list]).filter(Boolean).map((n) => ({
397
+ ...n,
398
+ id: n.id ?? makeId(n.type ?? "n"),
399
+ position: n.position ?? { x: 0, y: 0 },
400
+ data: n.data ?? {},
401
+ ...(n.type === "frame" && n.zIndex === undefined ? { zIndex: -1 } : null),
402
+ selected: select,
403
+ }));
404
+ if (!arr.length) return [];
405
+ setNodes((ns) => [...(select ? ns.map((n) => (n.selected ? { ...n, selected: false } : n)) : ns), ...arr]);
406
+ if (newEdges?.length) setEdges((es) => [...es.map((e) => (e.selected ? { ...e, selected: false } : e)), ...newEdges.map((e) => ({ ...e, type: e.type ?? "flow", data: e.data ?? {} }))]);
407
+ commit(reason, arr.map((n) => n.id));
408
+ if (edit && arr.length === 1) setEditing({ node: arr[0].id, edge: null });
409
+ return arr.map((n) => n.id);
410
+ };
411
+
412
+ const updateNodes = (ids, fn, { commit: doCommit = true, reason = "edit" } = {}) => {
413
+ const set = new Set(ids);
414
+ setNodes((ns) => ns.map((n) => (set.has(n.id) ? fn(n) : n)));
415
+ if (doCommit) commit(reason);
416
+ };
417
+
418
+ const removeNodes = (ids) => {
419
+ const gone = new Set(ids);
420
+ if (!gone.size) return;
421
+ setNodes((ns) => ns.filter((n) => !gone.has(n.id)));
422
+ setEdges((es) => es.filter((e) => !gone.has(e.source) && !gone.has(e.target)));
423
+ setEditing({ node: null, edge: null });
424
+ commit("delete");
425
+ say(`${gone.size} item${gone.size === 1 ? "" : "s"} deleted`);
426
+ };
427
+
428
+ const removeEdges = (ids) => {
429
+ const gone = new Set(ids);
430
+ setEdges((es) => es.filter((e) => !gone.has(e.id)));
431
+ setEditing((ed) => (gone.has(ed.edge) ? { node: null, edge: null } : ed));
432
+ commit("delete");
433
+ };
434
+
435
+ const deleteSelection = () => {
436
+ const ids = nodesRef.current.filter((n) => n.selected && n.deletable !== false && !n.data?.locked).map((n) => n.id);
437
+ const eids = edgesRef.current.filter((e) => e.selected && e.deletable !== false).map((e) => e.id);
438
+ if (!ids.length && !eids.length) return;
439
+ const gone = new Set(ids);
440
+ const goneE = new Set(eids);
441
+ setNodes((ns) => ns.filter((n) => !gone.has(n.id)));
442
+ setEdges((es) => es.filter((e) => !goneE.has(e.id) && !gone.has(e.source) && !gone.has(e.target)));
443
+ commit("delete");
444
+ say(`${ids.length + eids.length} deleted`);
445
+ };
446
+
447
+ const duplicate = (ids) => {
448
+ const src = { nodes: nodesRef.current, edges: edgesRef.current };
449
+ const copy = duplicateNodes(src, ids.filter((id) => nodesRef.current.some((n) => n.id === id)), { offset: { x: grid * 2, y: grid * 2 } });
450
+ if (!copy.nodes.length) return;
451
+ addNodes(copy.nodes, { reason: "paste", edges: copy.edges });
452
+ say(`${copy.nodes.length} duplicated`);
453
+ };
454
+
455
+ const pasteBoard = (data, at) => {
456
+ const b = readBoard(data);
457
+ if (!b.nodes.length) return;
458
+ const bounds = boardBounds(b.nodes);
459
+ pasteCount.current += 1;
460
+ const offset = at
461
+ ? { x: Math.round((at.x - bounds.x - bounds.width / 2) / grid) * grid, y: Math.round((at.y - bounds.y - bounds.height / 2) / grid) * grid }
462
+ : { x: grid * 2 * pasteCount.current, y: grid * 2 * pasteCount.current };
463
+ const copy = duplicateNodes(b, b.nodes.map((n) => n.id), { offset });
464
+ addNodes(copy.nodes, { reason: "paste", edges: copy.edges });
465
+ say(`${copy.nodes.length} pasted`);
466
+ };
467
+
468
+ const lock = (ids, on = true) => {
469
+ updateNodes(ids, (n) => {
470
+ const data = { ...n.data };
471
+ if (on) data.locked = true;
472
+ else delete data.locked;
473
+ const next = { ...n, data };
474
+ if (on) {
475
+ next.draggable = false;
476
+ next.deletable = false;
477
+ } else {
478
+ delete next.draggable;
479
+ delete next.deletable;
480
+ }
481
+ return next;
482
+ });
483
+ say(on ? "Locked" : "Unlocked");
484
+ };
485
+
486
+ const frameSelection = (ids) => {
487
+ const pick = nodesRef.current.filter((n) => ids.includes(n.id));
488
+ const b = boardBounds(pick);
489
+ if (!b) return;
490
+ const pad = grid * 2;
491
+ const f = createNode("frame", null, { rect: { x: b.x - pad, y: b.y - pad, width: b.width + pad * 2, height: b.height + pad * 2 }, grid, data: { title: "Frame" } });
492
+ addNodes(f, { edit: true });
493
+ };
494
+
495
+ const fitView = (opts) => rf.fitView({ padding: 0.2, maxZoom: 1.25, duration, ...opts });
496
+ const focusNode = (id) => {
497
+ selectOnly([id]);
498
+ const n = nodesRef.current.find((x) => x.id === id);
499
+ if (!n) return;
500
+ const r = nodeRect(n);
501
+ rf.setCenter(r.x + r.width / 2, r.y + r.height / 2, { zoom: Math.max(rf.getZoom(), 0.8), duration });
502
+ requestAnimationFrame(() => wrapRef.current?.querySelector(`.react-flow__node[data-id="${CSS.escape(id)}"]`)?.focus({ preventScroll: true }));
503
+ };
504
+ const centerPoint = () => {
505
+ const r = wrapRef.current?.getBoundingClientRect();
506
+ if (!r) return { x: 0, y: 0 };
507
+ return rf.screenToFlowPosition({ x: r.left + r.width / 2, y: r.top + r.height / 2 });
508
+ };
509
+
510
+ // The handle. One object for the canvas's life: every method reads the
511
+ // latest state through refs, so a block may keep it.
512
+ const apiRef = useRef(null);
513
+ if (!apiRef.current) apiRef.current = {};
514
+ const api = apiRef.current;
515
+ Object.assign(api, {
516
+ getBoard: currentBoard,
517
+ setBoard: (board, { fit = true } = {}) => {
518
+ restore(board);
519
+ commit("load");
520
+ if (fit) setTimeout(() => fitView(), 60);
521
+ },
522
+ getSelection: () => ({ nodes: nodesRef.current.filter((n) => n.selected), edges: edgesRef.current.filter((e) => e.selected) }),
523
+ addNodes,
524
+ addEdge: (edge) => {
525
+ const e = edge.id ? { type: "flow", data: {}, ...edge } : createEdge(edge.source, edge.target, edge);
526
+ setEdges((es) => [...es, e]);
527
+ commit("connect");
528
+ return e.id;
529
+ },
530
+ updateNode: (id, patch, opts) => updateNodes([id], (n) => ({ ...n, ...(typeof patch === "function" ? patch(n) : patch) }), opts),
531
+ updateNodeData: (id, patch, opts) => updateNodes([id], (n) => ({ ...n, data: { ...n.data, ...(typeof patch === "function" ? patch(n.data) : patch) } }), opts),
532
+ updateNodesData: (ids, patch, opts) => updateNodes(ids, (n) => ({ ...n, data: { ...n.data, ...patch } }), opts),
533
+ removeNodes,
534
+ updateEdge: (id, patch, { commit: doCommit = true } = {}) => {
535
+ setEdges((es) => es.map((e) => (e.id === id ? { ...e, ...(typeof patch === "function" ? patch(e) : patch) } : e)));
536
+ if (doCommit) commit("edit");
537
+ },
538
+ removeEdges,
539
+ select: (ids, edgeIds) => selectOnly(ids, edgeIds),
540
+ duplicate,
541
+ lock,
542
+ arrange: (ids, where) => {
543
+ setNodes((ns) => arrangeNodes(ns, ids, where));
544
+ commit("arrange");
545
+ },
546
+ align: (ids, how) => {
547
+ setNodes((ns) => alignNodes(ns, ids, how));
548
+ commit("align", ids);
549
+ },
550
+ distribute: (ids, axis) => {
551
+ setNodes((ns) => distributeNodes(ns, ids, axis));
552
+ commit("align", ids);
553
+ },
554
+ autoLayout: (opts = {}) => {
555
+ setNodes((ns) => autoLayout(ns, edgesRef.current, { grid, ...opts }));
556
+ commit("layout", opts.ids ?? nodesRef.current.map((n) => n.id));
557
+ if (!opts.ids) setTimeout(() => fitView(), 60);
558
+ },
559
+ frameSelection,
560
+ edit: (id) => setEditing({ node: id, edge: null }),
561
+ editEdge: (id) => setEditing({ node: null, edge: id }),
562
+ stopEditing: () => {
563
+ setEditing({ node: null, edge: null });
564
+ // Words emptied on a connector label go away with it.
565
+ setEdges((es) => es.map((e) => (typeof e.label === "string" && !e.label.trim() ? { ...e, label: undefined } : e)));
566
+ commit("edit");
567
+ requestAnimationFrame(() => {
568
+ if (!wrapRef.current?.contains(document.activeElement)) wrapRef.current?.focus({ preventScroll: true });
569
+ });
570
+ },
571
+ commit,
572
+ undo,
573
+ redo,
574
+ canUndo: () => historyRef.current.past.length > 0,
575
+ canRedo: () => historyRef.current.future.length > 0,
576
+ setTool: (id) => setTool(id),
577
+ getTool: () => tool,
578
+ fitView,
579
+ focusNode,
580
+ zoomIn: () => rf.zoomIn({ duration }),
581
+ zoomOut: () => rf.zoomOut({ duration }),
582
+ zoomTo: (z) => rf.zoomTo(z, { duration }),
583
+ centerPoint,
584
+ screenToFlow: (p) => rf.screenToFlowPosition(p),
585
+ toText: () => boardToText(currentBoard(), { describe: propsRef.current.describeNode }),
586
+ toMarkdown: (opts) => boardToMarkdown(currentBoard(), { describe: propsRef.current.describeNode, ...opts }),
587
+ save: () => {
588
+ dirty.current = true;
589
+ emitNow();
590
+ },
591
+ readOnly,
592
+ });
593
+
594
+ useEffect(() => {
595
+ if (!canvasRef) return;
596
+ canvasRef.current = api;
597
+ return () => {
598
+ if (canvasRef.current === api) canvasRef.current = null;
599
+ };
600
+ }, [canvasRef, api]);
601
+
602
+ // ── tools ──────────────────────────────────────────────────────────────
603
+ const customs = useMemo(
604
+ () =>
605
+ (ownTools ?? []).map((t) => ({ ...t, kind: t.onSelect && !t.create ? "action" : "mode", custom: true })),
606
+ [ownTools],
607
+ );
608
+ const toolById = useMemo(() => {
609
+ const m = new Map(Object.values(TOOLS).map((t) => [t.id, t]));
610
+ for (const t of customs) m.set(t.id, t);
611
+ return m;
612
+ }, [customs]);
613
+ const railItems = useMemo(() => {
614
+ const ids = rail ?? [...DEFAULT_RAIL, ...customs.map((t) => t.id), ...(props.pickImage ? ["image"] : [])];
615
+ return ids.map((id) => (id === "|" ? "|" : toolById.get(id))).filter(Boolean);
616
+ }, [rail, customs, toolById, props.pickImage]);
617
+ const active = toolById.get(tool) ?? TOOLS.select;
618
+ const placing = Boolean(active.place || active.create);
619
+ const cursorTool = tool === "hand" ? "hand" : tool === "connector" ? "connector" : tool === "text" ? "text" : placing ? "place" : "select";
620
+
621
+ const pickTool = (t) => {
622
+ if (!t || readOnly) return;
623
+ setConnectMenu(null);
624
+ if (t.id === "image") {
625
+ Promise.resolve(propsRef.current.pickImage?.())
626
+ .then((img) => {
627
+ if (!img?.src) return;
628
+ addNodes(createNode("image", centerPoint(), { grid, data: { src: img.src, alt: img.alt ?? "" }, width: img.width, height: img.height }));
629
+ })
630
+ .catch(() => {});
631
+ return;
632
+ }
633
+ if (t.kind === "action") {
634
+ t.onSelect?.(api);
635
+ return;
636
+ }
637
+ setTool((cur) => (cur === t.id && t.id !== "select" ? "select" : t.id));
638
+ if (t.id !== "select") selectOnly([]);
639
+ };
640
+
641
+ /** Put down what a tool makes, at a board point (or filling a rect). */
642
+ const place = (t, point, rect) => {
643
+ let made;
644
+ if (t.create) {
645
+ made = t.create(point, { rect, api, grid });
646
+ } else if (t.place) {
647
+ const data = t.place === "sticky" ? { color: stickyColor } : t.place === "shape" ? { shape: shapeKind } : undefined;
648
+ made = createNode(t.place, point, { grid: snap ? grid : 0, data, rect: t.place === "text" && rect ? { ...rect, height: undefined } : rect });
649
+ if (t.place === "text" && rect) delete made.height;
650
+ }
651
+ const list = (Array.isArray(made) ? made : [made]).filter(Boolean);
652
+ if (!list.length) return;
653
+ const one = list.length === 1 ? list[0] : null;
654
+ addNodes(list, { edit: Boolean(one && EDITABLE.has(one.type)) });
655
+ setTool("select");
656
+ say(`${t.label} added`);
657
+ };
658
+
659
+ // ── keyboard ───────────────────────────────────────────────────────────
660
+ const nudge = useRef(0);
661
+ const selectedNodes = nodes.filter((n) => n.selected);
662
+ const selectedEdges = edges.filter((e) => e.selected);
663
+
664
+ const frames = useMemo(
665
+ () =>
666
+ nodes
667
+ .filter((n) => n.type === "frame")
668
+ .sort((a, b) => {
669
+ const ra = nodeRect(a);
670
+ const rb = nodeRect(b);
671
+ return Math.abs(ra.y - rb.y) > 40 ? ra.y - rb.y : ra.x - rb.x;
672
+ }),
673
+ [nodes],
674
+ );
675
+ const goSlide = (i) => {
676
+ const f = frames[Math.max(0, Math.min(frames.length - 1, i))];
677
+ if (!f) return;
678
+ setSlide(frames.indexOf(f));
679
+ rf.fitView({ nodes: [{ id: f.id }], padding: 0.08, duration: reduce ? 0 : 400 });
680
+ };
681
+ useEffect(() => {
682
+ if (!presentation) return;
683
+ const t = setTimeout(() => (frames.length ? goSlide(0) : fitView()), 80);
684
+ return () => clearTimeout(t);
685
+ // eslint-disable-next-line react-hooks/exhaustive-deps
686
+ }, [presentation]);
687
+
688
+ const onKeyDownCapture = (e) => {
689
+ const t = e.target;
690
+ if (t?.closest?.(EDITING_TARGET)) return;
691
+ const mod = e.metaKey || e.ctrlKey;
692
+ const k = e.key;
693
+ const low = k.length === 1 ? k.toLowerCase() : k;
694
+ const onControl = Boolean(t?.closest?.("button, a[href]"));
695
+ const done = () => {
696
+ e.preventDefault();
697
+ e.stopPropagation();
698
+ };
699
+
700
+ // View keys work everywhere, read-only too.
701
+ if (e.shiftKey && !mod && (e.code === "Digit1" || k === "!")) return done(), fitView();
702
+ if (e.shiftKey && !mod && (e.code === "Digit2" || k === "@")) {
703
+ done();
704
+ const sel = nodesRef.current.filter((n) => n.selected);
705
+ return sel.length ? fitView({ nodes: sel.map((n) => ({ id: n.id })), maxZoom: 2 }) : fitView();
706
+ }
707
+ if ((mod && (k === "=" || k === "+")) || (!mod && !onControl && (k === "+" || k === "="))) return done(), rf.zoomIn({ duration });
708
+ if ((mod && k === "-") || (!mod && !onControl && k === "-")) return done(), rf.zoomOut({ duration });
709
+ if (mod && k === "0") return done(), rf.zoomTo(1, { duration });
710
+ if (k === "?" && !mod) return done(), setSheet(true);
711
+ if (presentation) {
712
+ if (k === "ArrowRight" || k === "PageDown" || (k === " " && !onControl)) return done(), goSlide(slide + 1);
713
+ if (k === "ArrowLeft" || k === "PageUp") return done(), goSlide(slide - 1);
714
+ if (k === "Escape") return done(), propsRef.current.onExitPresentation?.();
715
+ return;
716
+ }
717
+ if (k === "Escape") {
718
+ if (editing.node || editing.edge) return done(), setEditing({ node: null, edge: null });
719
+ if (connectMenu) return done(), setConnectMenu(null);
720
+ if (listView) return done(), setListView(false);
721
+ if (tool !== "select") return done(), setTool("select");
722
+ if (selectedNodes.length || selectedEdges.length) return done(), selectOnly([]);
723
+ return;
724
+ }
725
+ if (readOnly) return;
726
+
727
+ if (mod && low === "z") return done(), e.shiftKey ? redo() : undo();
728
+ if (mod && low === "y") return done(), redo();
729
+ if (mod && low === "s") return done(), api.save();
730
+ if (mod && low === "a") return done(), selectOnly(nodesRef.current.filter((n) => n.selectable !== false).map((n) => n.id));
731
+ const ids = nodesRef.current.filter((n) => n.selected).map((n) => n.id);
732
+ if (mod && low === "d") return done(), ids.length && duplicate(ids);
733
+ if (mod && e.shiftKey && low === "l") return done(), ids.length && lock(ids, !nodesRef.current.some((n) => n.selected && n.data?.locked));
734
+ if (mod && k === "]") return done(), ids.length && api.arrange(ids, "front");
735
+ if (mod && k === "[") return done(), ids.length && api.arrange(ids, "back");
736
+ if (mod) return;
737
+
738
+ if (!onControl && (k === "Delete" || k === "Backspace")) return done(), deleteSelection();
739
+ if (!onControl && k === "Enter") {
740
+ const focused = t?.classList?.contains("react-flow__node") ? t.dataset.id : null;
741
+ if (focused && !nodesRef.current.find((n) => n.id === focused)?.selected) return done(), selectOnly([focused]);
742
+ if (ids.length === 1) {
743
+ const n = nodesRef.current.find((x) => x.id === ids[0]);
744
+ if (n && !n.data?.locked) return done(), openNode(n);
745
+ }
746
+ const eid = edgesRef.current.filter((x) => x.selected).map((x) => x.id);
747
+ if (eid.length === 1) return done(), setEditing({ node: null, edge: eid[0] });
748
+ return;
749
+ }
750
+ if (!onControl && k.startsWith("Arrow") && ids.length) {
751
+ done();
752
+ const step = e.shiftKey ? 10 : 1;
753
+ const dx = k === "ArrowLeft" ? -step : k === "ArrowRight" ? step : 0;
754
+ const dy = k === "ArrowUp" ? -step : k === "ArrowDown" ? step : 0;
755
+ const movable = nodesRef.current.filter((n) => n.selected && n.draggable !== false && !n.data?.locked).map((n) => n.id);
756
+ updateNodes(movable, (n) => ({ ...n, position: { x: n.position.x + dx, y: n.position.y + dy } }), { commit: false });
757
+ clearTimeout(nudge.current);
758
+ nudge.current = setTimeout(() => commit("move", movable), 400);
759
+ return;
760
+ }
761
+ if (!e.altKey && k.length === 1) {
762
+ const hit = railItems.find((x) => x !== "|" && x.shortcut && x.shortcut.toLowerCase() === low);
763
+ if (hit) return done(), pickTool(hit);
764
+ }
765
+ };
766
+
767
+ // Tab onto a card selects it, so what the keys do is what is shown.
768
+ const onFocusCapture = (e) => {
769
+ const el = e.target;
770
+ if (!el?.classList?.contains("react-flow__node") || !el.matches(":focus-visible")) return;
771
+ const id = el.dataset.id;
772
+ const n = nodesRef.current.find((x) => x.id === id);
773
+ if (n && !n.selected && n.selectable !== false) selectOnly([id]);
774
+ };
775
+
776
+ // Copy, cut, paste: the board's own clipboard, with the text on the
777
+ // system one too (paste a list of lines from elsewhere = one sticky each).
778
+ useEffect(() => {
779
+ const MIME = "application/x-flow-canvas+json";
780
+ const activeHere = () => {
781
+ const a = document.activeElement;
782
+ return wrapRef.current?.contains(a) && !a?.closest?.(EDITING_TARGET);
783
+ };
784
+ const copy = (e) => {
785
+ if (!activeHere()) return false;
786
+ const ids = nodesRef.current.filter((n) => n.selected).map((n) => n.id);
787
+ if (!ids.length) return false;
788
+ const all = new Set(ids);
789
+ for (const id of ids) if (nodesRef.current.find((n) => n.id === id)?.type === "frame") for (const c of frameContents({ nodes: nodesRef.current }, id)) all.add(c);
790
+ const data = cleanBoard({ nodes: nodesRef.current.filter((n) => all.has(n.id)), edges: edgesRef.current.filter((x) => all.has(x.source) && all.has(x.target)) }, { viewport: false });
791
+ clip.current = data;
792
+ pasteCount.current = 0;
793
+ const text = boardToText(data, { describe: propsRef.current.describeNode });
794
+ clip.current.text = text;
795
+ e.clipboardData?.setData("text/plain", text);
796
+ try {
797
+ e.clipboardData?.setData(MIME, JSON.stringify(data));
798
+ } catch {}
799
+ e.preventDefault();
800
+ return true;
801
+ };
802
+ const onCopy = (e) => copy(e);
803
+ const onCut = (e) => {
804
+ if (propsRef.current.readOnly || propsRef.current.presentation) return;
805
+ if (copy(e)) deleteSelection();
806
+ };
807
+ const onPaste = (e) => {
808
+ if (!activeHere() || propsRef.current.readOnly || propsRef.current.presentation) return;
809
+ let data = null;
810
+ try {
811
+ const raw = e.clipboardData?.getData(MIME);
812
+ if (raw) data = JSON.parse(raw);
813
+ } catch {}
814
+ const text = e.clipboardData?.getData("text/plain") ?? "";
815
+ if (!data && clip.current && text === clip.current.text) data = clip.current;
816
+ e.preventDefault();
817
+ if (data) return pasteBoard(data, pointer.current);
818
+ const lines = text
819
+ .split(/\r?\n/)
820
+ .map((s) => s.replace(/^\s*(?:[-*•]|\d+[.)])\s+/, "").trim())
821
+ .filter(Boolean)
822
+ .slice(0, 24);
823
+ if (!lines.length) return;
824
+ const at = pointer.current ?? centerPoint();
825
+ const cols = Math.min(4, lines.length);
826
+ const made = lines.map((line, i) =>
827
+ createNode("sticky", { x: at.x + (i % cols) * 220 - ((cols - 1) * 220) / 2, y: at.y + Math.floor(i / cols) * 220 }, { grid, data: { text: line, color: stickyColor } }),
828
+ );
829
+ addNodes(made, { reason: "paste" });
830
+ };
831
+ document.addEventListener("copy", onCopy);
832
+ document.addEventListener("cut", onCut);
833
+ document.addEventListener("paste", onPaste);
834
+ return () => {
835
+ document.removeEventListener("copy", onCopy);
836
+ document.removeEventListener("cut", onCut);
837
+ document.removeEventListener("paste", onPaste);
838
+ };
839
+ // eslint-disable-next-line react-hooks/exhaustive-deps
840
+ }, [stickyColor]);
841
+
842
+ // ── pointer: focus, double-click, drag-to-size ─────────────────────────
843
+ const isPane = (el) => el?.classList?.contains("react-flow__pane");
844
+ const onPointerDownCapture = (e) => {
845
+ const a = document.activeElement;
846
+ if (!e.target.closest?.(EDITING_TARGET) && !wrapRef.current?.contains(a)) wrapRef.current?.focus({ preventScroll: true });
847
+ if (!isPane(e.target) || e.button !== 0 || readOnly) return;
848
+ if (connectMenu) setConnectMenu(null);
849
+ if (!active.sizable) return;
850
+ const start = rf.screenToFlowPosition({ x: e.clientX, y: e.clientY });
851
+ const origin = { x: e.clientX, y: e.clientY };
852
+ let moved = false;
853
+ const rectFrom = (ev) => {
854
+ const p = rf.screenToFlowPosition({ x: ev.clientX, y: ev.clientY });
855
+ const s = snap ? grid : 1;
856
+ const x1 = Math.round(Math.min(start.x, p.x) / s) * s;
857
+ const y1 = Math.round(Math.min(start.y, p.y) / s) * s;
858
+ const x2 = Math.round(Math.max(start.x, p.x) / s) * s;
859
+ const y2 = Math.round(Math.max(start.y, p.y) / s) * s;
860
+ return { x: x1, y: y1, width: Math.max(s, x2 - x1), height: Math.max(s, y2 - y1) };
861
+ };
862
+ const move = (ev) => {
863
+ if (!moved && Math.hypot(ev.clientX - origin.x, ev.clientY - origin.y) < 5) return;
864
+ moved = true;
865
+ setDraft(rectFrom(ev));
866
+ };
867
+ const up = (ev) => {
868
+ window.removeEventListener("pointermove", move);
869
+ window.removeEventListener("pointerup", up);
870
+ setDraft(null);
871
+ if (!moved) return;
872
+ suppressClick.current = true;
873
+ const r = rectFrom(ev);
874
+ if (r.width < grid * 2 && r.height < grid * 2) return;
875
+ place(active, { x: r.x + r.width / 2, y: r.y + r.height / 2 }, r);
876
+ };
877
+ window.addEventListener("pointermove", move);
878
+ window.addEventListener("pointerup", up);
879
+ };
880
+
881
+ const onPaneClick = (e) => {
882
+ setConnectMenu(null);
883
+ if (editing.node || editing.edge) setEditing({ node: null, edge: null });
884
+ if (suppressClick.current) {
885
+ suppressClick.current = false;
886
+ return;
887
+ }
888
+ if (readOnly || !placing) return;
889
+ place(active, rf.screenToFlowPosition({ x: e.clientX, y: e.clientY }));
890
+ };
891
+
892
+ const onDoubleClick = (e) => {
893
+ if (readOnly || tool !== "select" || !isPane(e.target)) return;
894
+ const p = rf.screenToFlowPosition({ x: e.clientX, y: e.clientY });
895
+ const n = createNode("sticky", p, { grid: snap ? grid : 0, data: { color: stickyColor } });
896
+ addNodes(n, { edit: true });
897
+ say("Sticky added");
898
+ };
899
+
900
+ const openNode = (n) => {
901
+ const { onOpenNode } = propsRef.current;
902
+ const custom = !BUILT_IN_NODES[n.type];
903
+ if (onOpenNode && (custom || (n.type === "card" && renderCard) || readOnly)) return onOpenNode(n);
904
+ if (readOnly || n.data?.locked) return;
905
+ if (EDITABLE.has(n.type) || custom) setEditing({ node: n.id, edge: null });
906
+ };
907
+
908
+ // ── React Flow wiring ──────────────────────────────────────────────────
909
+ const onNodesChange = useCallback((changes) => {
910
+ const real = changes.filter((c) => c.type !== "remove");
911
+ setNodes((nds) => {
912
+ const members = frameMembers.current;
913
+ if (!members.size) return applyNodeChanges(real, nds);
914
+ const byId = new Map(nds.map((n) => [n.id, n]));
915
+ const moving = new Set(real.filter((c) => c.type === "position").map((c) => c.id));
916
+ const extra = [];
917
+ for (const c of real) {
918
+ if (c.type !== "position" || !c.position || !members.has(c.id)) continue;
919
+ const prev = byId.get(c.id);
920
+ if (!prev) continue;
921
+ const dx = c.position.x - prev.position.x;
922
+ const dy = c.position.y - prev.position.y;
923
+ if (!dx && !dy) continue;
924
+ for (const m of members.get(c.id)) {
925
+ const mn = byId.get(m);
926
+ if (!mn || moving.has(m)) continue;
927
+ moving.add(m);
928
+ extra.push({ type: "position", id: m, position: { x: mn.position.x + dx, y: mn.position.y + dy } });
929
+ }
930
+ }
931
+ return applyNodeChanges(extra.length ? [...real, ...extra] : real, nds);
932
+ });
933
+ }, []);
934
+ const onEdgesChange = useCallback((changes) => setEdges((es) => applyEdgeChanges(changes.filter((c) => c.type !== "remove"), es)), []);
935
+
936
+ const dragStart = (dragged) => {
937
+ setConnectMenu(null);
938
+ const ids = new Set((dragged ?? []).map((n) => n.id));
939
+ const m = new Map();
940
+ for (const n of dragged ?? []) {
941
+ if (n.type !== "frame") continue;
942
+ const inside = frameContents({ nodes: nodesRef.current }, n.id).filter((id) => !ids.has(id));
943
+ if (inside.length) m.set(n.id, inside);
944
+ }
945
+ frameMembers.current = m;
946
+ };
947
+ const dragStop = (dragged) => {
948
+ const ids = (dragged ?? []).map((n) => n.id);
949
+ for (const list of frameMembers.current.values()) ids.push(...list);
950
+ frameMembers.current = new Map();
951
+ commit("move", ids);
952
+ };
953
+
954
+ const isValidConnection = (c) => {
955
+ if (c.source === c.target) return false;
956
+ const can = propsRef.current.canConnect;
957
+ return can ? can(c, { nodes: nodesRef.current, edges: edgesRef.current }) !== false : true;
958
+ };
959
+ const onConnect = (c) => {
960
+ if (!isValidConnection(c)) return;
961
+ if (edgesRef.current.some((e) => e.source === c.source && e.target === c.target)) return;
962
+ const e = createEdge(c.source, c.target);
963
+ setEdges((es) => [...es, e]);
964
+ commit("connect");
965
+ say("Connected");
966
+ };
967
+ const onConnectEnd = (event, state) => {
968
+ if (readOnly || state?.isValid || state?.toNode || !state?.fromNode) return;
969
+ const pt = "changedTouches" in event && event.changedTouches?.length ? event.changedTouches[0] : event;
970
+ const box = wrapRef.current?.getBoundingClientRect();
971
+ if (!box) return;
972
+ const flow = rf.screenToFlowPosition({ x: pt.clientX, y: pt.clientY });
973
+ setConnectMenu({ at: { x: Math.min(pt.clientX - box.left, box.width - 200), y: Math.min(pt.clientY - box.top, box.height - 240) }, flow, from: state.fromNode.id });
974
+ };
975
+
976
+ const connectItems = useMemo(() => {
977
+ if (!connectMenu) return [];
978
+ const { flow, from } = connectMenu;
979
+ const make = (node) => {
980
+ if (!node) return;
981
+ const list = Array.isArray(node) ? node : [node];
982
+ const first = list[0];
983
+ addNodes(list, {
984
+ edges: first ? [createEdge(from, first.id)] : [],
985
+ edit: list.length === 1 && EDITABLE.has(first?.type),
986
+ });
987
+ setConnectMenu(null);
988
+ };
989
+ const built = [
990
+ { label: "Sticky note", icon: "sticky-note", onSelect: () => make(createNode("sticky", flow, { grid, data: { color: stickyColor } })) },
991
+ ...Object.entries(SHAPE_ICON).map(([k, icon]) => ({ label: SHAPE_NAMES[k], icon, onSelect: () => make(createNode("shape", flow, { grid, data: { shape: k } })) })),
992
+ { label: "Text", icon: "type", onSelect: () => make(createNode("text", flow, { grid })) },
993
+ ];
994
+ const own = customs.filter((t) => t.create && t.connectable !== false).map((t) => ({ label: t.label, icon: t.icon, onSelect: () => make(t.create(flow, { api, grid })) }));
995
+ return [...built, own.length ? { type: "separator" } : null, ...own];
996
+ // eslint-disable-next-line react-hooks/exhaustive-deps
997
+ }, [connectMenu, customs, stickyColor]);
998
+
999
+ // ── the context menu (right-click, long press, Shift+F10) ───────────────
1000
+ const ctxItems = (() => {
1001
+ const m = modKey();
1002
+ if (readOnly) {
1003
+ return [
1004
+ { label: "Fit to screen", icon: "maximize", shortcut: "⇧1", onSelect: () => fitView() },
1005
+ { label: "List view", icon: "list", onSelect: () => setListView(true) },
1006
+ ];
1007
+ }
1008
+ const sel = selectedNodes;
1009
+ if (ctxOn === "edge" && selectedEdges.length) {
1010
+ const ids = selectedEdges.map((x) => x.id);
1011
+ return [
1012
+ ids.length === 1 && { label: selectedEdges[0].label ? "Edit label" : "Add label", icon: "pencil", shortcut: "↵", onSelect: () => api.editEdge(ids[0]) },
1013
+ { label: "Delete", icon: "trash", tone: "danger", shortcut: "⌫", onSelect: () => removeEdges(ids) },
1014
+ ];
1015
+ }
1016
+ if (ctxOn === "selection" && sel.length) {
1017
+ const ids = sel.map((n) => n.id);
1018
+ const anyLocked = sel.some((n) => n.data?.locked);
1019
+ const single = sel.length === 1 ? sel[0] : null;
1020
+ const extra = typeof contextMenuItems === "function" ? contextMenuItems({ nodes: sel, edges: selectedEdges }, api) : null;
1021
+ return [
1022
+ single && !anyLocked && { label: "Edit", icon: "pencil", shortcut: "↵", onSelect: () => openNode(single) },
1023
+ { label: "Duplicate", icon: "copy", shortcut: `${m}D`, onSelect: () => duplicate(ids) },
1024
+ !sel.every((n) => n.type === "frame") && { label: "Frame selection", icon: "frame", onSelect: () => frameSelection(ids) },
1025
+ { type: "separator" },
1026
+ { label: "Bring to front", icon: "bring-front", shortcut: `${m}]`, onSelect: () => api.arrange(ids, "front") },
1027
+ { label: "Send to back", icon: "send-back", shortcut: `${m}[`, onSelect: () => api.arrange(ids, "back") },
1028
+ sel.length > 1 && {
1029
+ label: "Align",
1030
+ icon: "align-center",
1031
+ items: [
1032
+ { label: "Left", onSelect: () => api.align(ids, "left") },
1033
+ { label: "Centre", onSelect: () => api.align(ids, "center") },
1034
+ { label: "Right", onSelect: () => api.align(ids, "right") },
1035
+ { type: "separator" },
1036
+ { label: "Top", onSelect: () => api.align(ids, "top") },
1037
+ { label: "Middle", onSelect: () => api.align(ids, "middle") },
1038
+ { label: "Bottom", onSelect: () => api.align(ids, "bottom") },
1039
+ sel.length > 2 && { type: "separator" },
1040
+ sel.length > 2 && { label: "Distribute horizontally", onSelect: () => api.distribute(ids, "horizontal") },
1041
+ sel.length > 2 && { label: "Distribute vertically", onSelect: () => api.distribute(ids, "vertical") },
1042
+ { type: "separator" },
1043
+ { label: "Tidy up", onSelect: () => api.autoLayout({ ids }) },
1044
+ ],
1045
+ },
1046
+ { label: anyLocked ? "Unlock" : "Lock", icon: anyLocked ? "lock-open" : "lock", shortcut: `⇧${m}L`, onSelect: () => lock(ids, !anyLocked) },
1047
+ ...(extra?.length ? [{ type: "separator" }, ...extra] : []),
1048
+ { type: "separator" },
1049
+ { label: "Delete", icon: "trash", tone: "danger", shortcut: "⌫", disabled: sel.every((n) => n.data?.locked || n.deletable === false), onSelect: deleteSelection },
1050
+ ];
1051
+ }
1052
+ const at = () => ctxPoint.current ?? centerPoint();
1053
+ const extra = typeof contextMenuItems === "function" ? contextMenuItems({ nodes: [], edges: [] }, api) : null;
1054
+ return [
1055
+ { label: "Add sticky here", icon: "sticky-note", onSelect: () => addNodes(createNode("sticky", at(), { grid, data: { color: stickyColor } }), { edit: true }) },
1056
+ { label: "Add shape here", icon: "square", onSelect: () => addNodes(createNode("shape", at(), { grid, data: { shape: shapeKind } }), { edit: true }) },
1057
+ { label: "Add text here", icon: "type", onSelect: () => addNodes(createNode("text", at(), { grid }), { edit: true }) },
1058
+ { label: "Add frame here", icon: "frame", onSelect: () => addNodes(createNode("frame", at(), { grid }), { edit: true }) },
1059
+ { type: "separator" },
1060
+ clip.current && { label: "Paste here", icon: "copy", shortcut: `${m}V`, onSelect: () => pasteBoard(clip.current, at()) },
1061
+ nodes.length > 0 && { label: "Select all", shortcut: `${m}A`, onSelect: () => selectOnly(nodesRef.current.map((n) => n.id)) },
1062
+ nodes.length > 1 && { label: "Tidy up everything", icon: "sparkles", onSelect: () => api.autoLayout() },
1063
+ ...(extra?.length ? [{ type: "separator" }, ...extra] : []),
1064
+ { type: "separator" },
1065
+ { label: "Fit to screen", icon: "maximize", shortcut: "⇧1", onSelect: () => fitView() },
1066
+ { type: "checkbox", label: "Snap to grid", checked: snap, onCheckedChange: setSnap },
1067
+ { label: "Keyboard shortcuts", icon: "keyboard", shortcut: "?", onSelect: () => setSheet(true) },
1068
+ ];
1069
+ })();
1070
+
1071
+ // ── node and edge types, context ───────────────────────────────────────
1072
+ const nodeTypes = useMemo(() => ({ ...BUILT_IN_NODES, ...(ownNodeTypes ?? {}) }), [ownNodeTypes]);
1073
+ const soleEdge = selectedEdges.length === 1 && !selectedNodes.length ? selectedEdges[0].id : null;
1074
+ const ctx = useMemo(
1075
+ () => ({ api, tool, editingNode: editing.node, editingEdge: editing.edge, soleEdge, readOnly, palette, renderCard, connecting, grid }),
1076
+ [api, tool, editing.node, editing.edge, soleEdge, readOnly, palette, renderCard, connecting, grid],
1077
+ );
1078
+
1079
+ // React Flow's event props, one stable function each: a new function per
1080
+ // render would redraw every node on every render (a pan, a keystroke).
1081
+ const live = useRef({});
1082
+ live.current = {
1083
+ onConnect,
1084
+ onConnectEnd,
1085
+ isValidConnection,
1086
+ onNodeDragStart: (_, __, dragged) => dragStart(dragged),
1087
+ onNodeDragStop: (_, __, dragged) => dragStop(dragged),
1088
+ onSelectionDragStart: (_, dragged) => dragStart(dragged),
1089
+ onSelectionDragStop: (_, dragged) => dragStop(dragged),
1090
+ onNodeDoubleClick: (e, n) => {
1091
+ e.stopPropagation();
1092
+ openNode(n);
1093
+ },
1094
+ onEdgeDoubleClick: (e, ed) => {
1095
+ e.stopPropagation();
1096
+ if (!readOnly) setEditing({ node: null, edge: ed.id });
1097
+ },
1098
+ onPaneClick,
1099
+ onPaneContextMenu: (e) => {
1100
+ ctxPoint.current = rf.screenToFlowPosition({ x: e.clientX, y: e.clientY });
1101
+ setCtxOn("pane");
1102
+ },
1103
+ onNodeContextMenu: (_, n) => {
1104
+ if (!n.selected) selectOnly([n.id]);
1105
+ setCtxOn("selection");
1106
+ },
1107
+ onSelectionContextMenu: () => setCtxOn("selection"),
1108
+ onEdgeContextMenu: (_, ed) => {
1109
+ if (!ed.selected) selectOnly([], [ed.id]);
1110
+ setCtxOn("edge");
1111
+ },
1112
+ onSelectionChange: ({ nodes: sn, edges: se }) => propsRef.current.onSelectionChange?.(sn, se),
1113
+ onMoveEnd: (_, vp) => {
1114
+ viewportRef.current = vp;
1115
+ },
1116
+ };
1117
+ const handlers = useMemo(() => Object.fromEntries(Object.keys(live.current).map((k) => [k, (...a) => live.current[k](...a)])), []);
1118
+
1119
+ const dragging = nodes.some((n) => n.dragging) || marquee;
1120
+ const snapGrid = useMemo(() => [grid, grid], [grid]);
1121
+ const proOptions = useMemo(() => ({ hideAttribution }), [hideAttribution]);
1122
+ const showToolbar = !readOnly && tool === "select" && selectedNodes.length > 0 && !dragging && !editing.node && !connectMenu && !listView;
1123
+ const templates = templatesProp ?? starterTemplates();
1124
+
1125
+ // Where the floating toolbars hang, in board coordinates (Floating turns
1126
+ // them into screen ones as the view moves).
1127
+ const selBounds = showToolbar ? boardBounds(selectedNodes) : null;
1128
+ const selBox = selBounds
1129
+ ? {
1130
+ x: selBounds.x + selBounds.width / 2,
1131
+ top: selBounds.y,
1132
+ bottom: selBounds.y + selBounds.height,
1133
+ // A frame's title sits above its box: clear it.
1134
+ lift: selectedNodes.some((n) => n.type === "frame") ? 34 : 14,
1135
+ }
1136
+ : null;
1137
+ const soleEdgeObj = !readOnly && tool === "select" && soleEdge && !editing.edge && !connectMenu && !listView ? selectedEdges[0] : null;
1138
+ const edgeBar = (() => {
1139
+ if (!soleEdgeObj) return null;
1140
+ const s = nodes.find((n) => n.id === soleEdgeObj.source);
1141
+ const t = nodes.find((n) => n.id === soleEdgeObj.target);
1142
+ if (!s || !t) return null;
1143
+ const ra = nodeRect(s);
1144
+ const rb = nodeRect(t);
1145
+ const sides = facingSides(ra, rb);
1146
+ const g = connectorGeometry(sidePoint(ra, sides.source), sides.source, sidePoint(rb, sides.target), sides.target, soleEdgeObj.data?.line ?? "curved");
1147
+ return { at: { x: g.mid.x, top: g.mid.y, bottom: g.mid.y, lift: 26 }, edge: soleEdgeObj };
1148
+ })();
1149
+ const isEmpty = nodes.length === 0;
1150
+ const board = listView ? cleanBoard({ nodes, edges }, { viewport: false }) : null;
1151
+
1152
+ return (
1153
+ <CanvasContext.Provider value={ctx}>
1154
+ <div
1155
+ ref={wrapRef}
1156
+ role="region"
1157
+ aria-label={label}
1158
+ aria-roledescription="board"
1159
+ tabIndex={-1}
1160
+ data-tool={cursorTool}
1161
+ data-connecting={connecting ? "true" : "false"}
1162
+ className={cx("relative isolate overflow-hidden bg-background text-foreground outline-none", !bare && "rounded-lg border border-border", THEME, className)}
1163
+ style={{ height }}
1164
+ onKeyDownCapture={onKeyDownCapture}
1165
+ onFocusCapture={onFocusCapture}
1166
+ onPointerDownCapture={onPointerDownCapture}
1167
+ onPointerMove={(e) => {
1168
+ if (e.pointerType === "mouse") pointer.current = rf.screenToFlowPosition({ x: e.clientX, y: e.clientY });
1169
+ }}
1170
+ onPointerLeave={() => {
1171
+ pointer.current = null;
1172
+ }}
1173
+ onDoubleClick={onDoubleClick}
1174
+ >
1175
+ <p className="sr-only" aria-live="polite">
1176
+ {announce}
1177
+ </p>
1178
+ <ContextMenu items={ctxItems} label={`${label} actions`} className="absolute inset-0">
1179
+ <ReactFlow
1180
+ nodes={nodes}
1181
+ edges={edges}
1182
+ nodeTypes={nodeTypes}
1183
+ edgeTypes={BUILT_IN_EDGES}
1184
+ defaultEdgeOptions={EDGE_DEFAULTS}
1185
+ onNodesChange={onNodesChange}
1186
+ onEdgesChange={onEdgesChange}
1187
+ {...handlers}
1188
+ connectionMode={ConnectionMode.Loose}
1189
+ connectionRadius={28}
1190
+ snapToGrid={snap}
1191
+ snapGrid={snapGrid}
1192
+ panOnScroll
1193
+ zoomOnScroll={false}
1194
+ zoomOnPinch
1195
+ zoomOnDoubleClick={false}
1196
+ panOnDrag={tool === "hand" ? true : MIDDLE_BUTTON}
1197
+ selectionOnDrag={tool === "select" && !readOnly}
1198
+ selectionMode={SelectionMode.Full}
1199
+ multiSelectionKeyCode={MULTI_KEYS}
1200
+ deleteKeyCode={null}
1201
+ nodesDraggable={!readOnly && (tool === "select" || placing)}
1202
+ nodesConnectable={!readOnly}
1203
+ elementsSelectable={!readOnly && tool !== "hand"}
1204
+ elevateNodesOnSelect={false}
1205
+ elevateEdgesOnSelect
1206
+ minZoom={0.1}
1207
+ maxZoom={4}
1208
+ fitView={!viewportRef.current}
1209
+ fitViewOptions={FIT_OPTIONS}
1210
+ defaultViewport={viewportRef.current ?? undefined}
1211
+ proOptions={proOptions}
1212
+ attributionPosition={narrow ? "top-left" : "bottom-center"}
1213
+ colorMode="light"
1214
+ aria-label={label}
1215
+ >
1216
+ <Background variant={BackgroundVariant.Dots} gap={grid} size={1.6} />
1217
+ {minimap && !narrow ? <MiniMap pannable zoomable ariaLabel="Minimap" position="bottom-right" nodeBorderRadius={6} className="bottom-14!" /> : null}
1218
+ {draft ? (
1219
+ <ViewportPortal>
1220
+ <div
1221
+ aria-hidden="true"
1222
+ className="absolute top-0 left-0 rounded-md border-2 border-dashed border-primary bg-primary/5"
1223
+ style={{ transform: `translate(${draft.x}px, ${draft.y}px)`, width: draft.width, height: draft.height }}
1224
+ />
1225
+ </ViewportPortal>
1226
+ ) : null}
1227
+ </ReactFlow>
1228
+ </ContextMenu>
1229
+
1230
+ {isEmpty && !readOnly && emptyState !== false && !listView && tool === "select" ? (
1231
+ emptyState ? (
1232
+ <div className="pointer-events-none absolute inset-0 grid place-items-center p-6">
1233
+ <div className="fc-chrome pointer-events-auto">{emptyState}</div>
1234
+ </div>
1235
+ ) : (
1236
+ <EmptyState
1237
+ templates={templates}
1238
+ narrow={narrow}
1239
+ onTemplate={(t) => {
1240
+ const b = typeof t.board === "function" ? t.board() : t.board;
1241
+ api.setBoard(b);
1242
+ say(`${t.label} template added`);
1243
+ }}
1244
+ />
1245
+ )
1246
+ ) : null}
1247
+
1248
+ {showToolbar && selBox ? (
1249
+ <Floating at={selBox}>
1250
+ <SelectionToolbar nodes={selectedNodes} api={api} palette={palette} extra={toolbarExtra} />
1251
+ </Floating>
1252
+ ) : null}
1253
+ {edgeBar ? (
1254
+ <Floating at={edgeBar.at}>
1255
+ <EdgeToolbar id={edgeBar.edge.id} data={edgeBar.edge.data} label={edgeBar.edge.label} />
1256
+ </Floating>
1257
+ ) : null}
1258
+
1259
+ {topBar ? <div className="pointer-events-none absolute inset-x-3 top-3 z-10 flex justify-center [&>*]:pointer-events-auto">{topBar}</div> : null}
1260
+
1261
+ {!readOnly ? (
1262
+ <div className={cx("pointer-events-none absolute z-10 flex gap-2", narrow ? "inset-x-3 bottom-3 flex-col-reverse items-center" : "inset-y-3 left-3 flex-row items-center")}>
1263
+ <ToolRail items={railItems} tool={tool} onPick={pickTool} narrow={narrow} />
1264
+ <ToolFlyout tool={tool} palette={palette} stickyColor={stickyColor} onStickyColor={setStickyColor} shapeKind={shapeKind} onShapeKind={setShapeKind} />
1265
+ </div>
1266
+ ) : null}
1267
+
1268
+ {!readOnly ? (
1269
+ <div className={cx("absolute left-3 z-10", narrow ? (tool === "sticky" || tool === "shape" ? "bottom-32" : "bottom-20") : "bottom-3")}>
1270
+ <HistoryBar canUndo={history.past.length > 0} canRedo={history.future.length > 0} onUndo={undo} onRedo={redo} save={save} onRetry={() => runSave(currentBoard())} />
1271
+ </div>
1272
+ ) : null}
1273
+
1274
+ <div className={cx("absolute right-3 z-10", narrow ? (topBar ? "top-16" : "top-3") : "bottom-3")}>
1275
+ <ZoomBar
1276
+ zoom={<ZoomLevel />}
1277
+ narrow={narrow}
1278
+ presentation={presentation}
1279
+ onZoomIn={() => rf.zoomIn({ duration })}
1280
+ onZoomOut={() => rf.zoomOut({ duration })}
1281
+ onZoomTo={(z) => rf.zoomTo(z, { duration })}
1282
+ onFit={() => fitView()}
1283
+ onFitSelection={() => fitView({ nodes: selectedNodes.map((n) => ({ id: n.id })), maxZoom: 2 })}
1284
+ hasSelection={selectedNodes.length > 0}
1285
+ minimap={minimap}
1286
+ onMinimap={setMinimap}
1287
+ list={listView}
1288
+ onList={setListView}
1289
+ snap={snap}
1290
+ onSnap={setSnap}
1291
+ onShortcuts={() => setSheet(true)}
1292
+ />
1293
+ </div>
1294
+
1295
+ {presentation ? (
1296
+ <div className="pointer-events-none absolute inset-x-3 bottom-3 z-10 flex justify-center">
1297
+ <PresentBar
1298
+ index={slide}
1299
+ count={frames.length}
1300
+ title={frames[slide]?.data?.title || "Untitled frame"}
1301
+ onPrev={() => goSlide(slide - 1)}
1302
+ onNext={() => goSlide(slide + 1)}
1303
+ onExit={onExitPresentation}
1304
+ />
1305
+ </div>
1306
+ ) : null}
1307
+
1308
+ {connectMenu ? <ConnectMenu at={connectMenu.at} items={connectItems} onClose={() => setConnectMenu(null)} /> : null}
1309
+
1310
+ {listView ? (
1311
+ <ListView
1312
+ board={board}
1313
+ label={label}
1314
+ describe={describeNode}
1315
+ iconOf={(n) => toolById.get(n.type)?.icon}
1316
+ onClose={() => setListView(false)}
1317
+ onPick={(id) => {
1318
+ setListView(false);
1319
+ requestAnimationFrame(() => focusNode(id));
1320
+ }}
1321
+ />
1322
+ ) : null}
1323
+
1324
+ <ShortcutsSheet open={sheet} onClose={() => setSheet(false)} tools={railItems} />
1325
+ </div>
1326
+ </CanvasContext.Provider>
1327
+ );
1328
+ }
1329
+