makersclaw 0.2.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 (473) hide show
  1. package/THIRD-PARTY-NOTICES.md +180 -0
  2. package/package.json +3 -2
  3. package/r/annotate.json +62 -0
  4. package/r/approval-request.json +52 -0
  5. package/r/audio-player.json +74 -0
  6. package/r/auth-form.json +55 -0
  7. package/r/board.json +10 -6
  8. package/r/calendar-view.json +105 -0
  9. package/r/cart.json +62 -0
  10. package/r/chart.json +83 -0
  11. package/r/chat.json +95 -0
  12. package/r/code-editor.json +94 -0
  13. package/r/comment-box.json +64 -0
  14. package/r/contact-section.json +53 -0
  15. package/r/core.json +205 -8
  16. package/r/csv-import.json +63 -0
  17. package/r/data-table.json +69 -0
  18. package/r/diagram.json +74 -0
  19. package/r/doc-editor.json +82 -0
  20. package/r/doc-page.json +61 -0
  21. package/r/email-render.json +73 -0
  22. package/r/faq.json +50 -0
  23. package/r/file-upload.json +54 -0
  24. package/r/flow-canvas.json +93 -0
  25. package/r/footer.json +52 -0
  26. package/r/gantt.json +70 -0
  27. package/r/job-panel.json +12 -7
  28. package/r/json-editor.json +66 -0
  29. package/r/map.json +89 -0
  30. package/r/menubar.json +52 -0
  31. package/r/mind-map.json +67 -0
  32. package/r/navigation-menu.json +52 -0
  33. package/r/order-detail.json +54 -0
  34. package/r/org-chart.json +66 -0
  35. package/r/page-builder.json +105 -0
  36. package/r/payment-methods.json +51 -0
  37. package/r/pdf-document.json +79 -0
  38. package/r/pdf-viewer.json +88 -0
  39. package/r/product-detail.json +76 -0
  40. package/r/product-grid.json +65 -0
  41. package/r/record-form.json +67 -0
  42. package/r/record-notes.json +55 -0
  43. package/r/registry.json +3411 -29
  44. package/r/search-dialog.json +51 -0
  45. package/r/settings-list.json +66 -0
  46. package/r/setup-checklist.json +51 -0
  47. package/r/share-dialog.json +52 -0
  48. package/r/sidebar.json +69 -0
  49. package/r/signature-pad.json +72 -0
  50. package/r/sketch-canvas.json +99 -0
  51. package/r/spreadsheet.json +90 -0
  52. package/r/stat-cards.json +66 -0
  53. package/r/task-list.json +63 -0
  54. package/r/task-model.json +98 -0
  55. package/r/testimonials.json +52 -0
  56. package/r/transaction-list.json +53 -0
  57. package/r/whiteboard.json +66 -0
  58. package/registry/blocks/annotate/README.md +179 -0
  59. package/registry/blocks/annotate/annotate-data.js +250 -0
  60. package/registry/blocks/annotate/annotate.jsx +646 -0
  61. package/registry/blocks/annotate/block.json +10 -0
  62. package/registry/blocks/annotate/demo.jsx +135 -0
  63. package/registry/blocks/annotate/index.js +15 -0
  64. package/registry/blocks/approval-request/README.md +116 -0
  65. package/registry/blocks/approval-request/approval-request.jsx +386 -0
  66. package/registry/blocks/approval-request/block.json +10 -0
  67. package/registry/blocks/approval-request/demo.jsx +127 -0
  68. package/registry/blocks/approval-request/index.js +4 -0
  69. package/registry/blocks/audio-player/README.md +167 -0
  70. package/registry/blocks/audio-player/audio-player.jsx +623 -0
  71. package/registry/blocks/audio-player/block.json +14 -0
  72. package/registry/blocks/audio-player/colors.js +109 -0
  73. package/registry/blocks/audio-player/demo.jsx +209 -0
  74. package/registry/blocks/audio-player/index.js +6 -0
  75. package/registry/blocks/audio-player/time.js +67 -0
  76. package/registry/blocks/audio-player/wavesurfer.js +9 -0
  77. package/registry/blocks/auth-form/README.md +126 -0
  78. package/registry/blocks/auth-form/auth-form.jsx +568 -0
  79. package/registry/blocks/auth-form/block.json +10 -0
  80. package/registry/blocks/auth-form/demo.jsx +131 -0
  81. package/registry/blocks/auth-form/index.js +3 -0
  82. package/registry/blocks/board/README.md +55 -5
  83. package/registry/blocks/board/block.json +2 -2
  84. package/registry/blocks/board/board.jsx +230 -86
  85. package/registry/blocks/board/demo.jsx +103 -24
  86. package/registry/blocks/calendar-view/README.md +180 -0
  87. package/registry/blocks/calendar-view/agenda.jsx +147 -0
  88. package/registry/blocks/calendar-view/block.json +76 -0
  89. package/registry/blocks/calendar-view/calendar-model.js +442 -0
  90. package/registry/blocks/calendar-view/calendar.jsx +627 -0
  91. package/registry/blocks/calendar-view/demo.jsx +195 -0
  92. package/registry/blocks/calendar-view/draft.jsx +49 -0
  93. package/registry/blocks/calendar-view/index.js +39 -0
  94. package/registry/blocks/calendar-view/month.jsx +346 -0
  95. package/registry/blocks/calendar-view/paint.js +69 -0
  96. package/registry/blocks/calendar-view/time-grid.jsx +527 -0
  97. package/registry/blocks/calendar-view/tray.jsx +87 -0
  98. package/registry/blocks/cart/README.md +146 -0
  99. package/registry/blocks/cart/block.json +10 -0
  100. package/registry/blocks/cart/cart.jsx +597 -0
  101. package/registry/blocks/cart/demo.jsx +98 -0
  102. package/registry/blocks/cart/index.js +4 -0
  103. package/registry/blocks/cart/money.js +28 -0
  104. package/registry/blocks/chart/README.md +316 -0
  105. package/registry/blocks/chart/block.json +10 -0
  106. package/registry/blocks/chart/cartesian.jsx +642 -0
  107. package/registry/blocks/chart/core.jsx +262 -0
  108. package/registry/blocks/chart/demo.jsx +357 -0
  109. package/registry/blocks/chart/heatmap.jsx +336 -0
  110. package/registry/blocks/chart/index.js +8 -0
  111. package/registry/blocks/chart/pie.jsx +245 -0
  112. package/registry/blocks/chart/sparkline.jsx +88 -0
  113. package/registry/blocks/chat/README.md +273 -0
  114. package/registry/blocks/chat/attachment.jsx +95 -0
  115. package/registry/blocks/chat/block.json +10 -0
  116. package/registry/blocks/chat/chat.jsx +132 -0
  117. package/registry/blocks/chat/composer.jsx +364 -0
  118. package/registry/blocks/chat/demo.jsx +578 -0
  119. package/registry/blocks/chat/header.jsx +34 -0
  120. package/registry/blocks/chat/index.js +9 -0
  121. package/registry/blocks/chat/message.jsx +450 -0
  122. package/registry/blocks/chat/scroller.jsx +247 -0
  123. package/registry/blocks/chat/text.jsx +104 -0
  124. package/registry/blocks/code-editor/README.md +210 -0
  125. package/registry/blocks/code-editor/block.json +22 -0
  126. package/registry/blocks/code-editor/code-editor.jsx +534 -0
  127. package/registry/blocks/code-editor/codemirror.js +38 -0
  128. package/registry/blocks/code-editor/demo.jsx +205 -0
  129. package/registry/blocks/code-editor/index.js +8 -0
  130. package/registry/blocks/code-editor/json.js +317 -0
  131. package/registry/blocks/code-editor/sql.js +136 -0
  132. package/registry/blocks/code-editor/text.js +101 -0
  133. package/registry/blocks/code-editor/theme.js +186 -0
  134. package/registry/blocks/comment-box/README.md +123 -0
  135. package/registry/blocks/comment-box/block.json +10 -0
  136. package/registry/blocks/comment-box/comment-body.jsx +217 -0
  137. package/registry/blocks/comment-box/comment-box.jsx +198 -0
  138. package/registry/blocks/comment-box/comment-time.jsx +77 -0
  139. package/registry/blocks/comment-box/demo.jsx +141 -0
  140. package/registry/blocks/comment-box/index.js +8 -0
  141. package/registry/blocks/contact-section/README.md +152 -0
  142. package/registry/blocks/contact-section/block.json +11 -0
  143. package/registry/blocks/contact-section/contact-section.jsx +509 -0
  144. package/registry/blocks/contact-section/demo.jsx +93 -0
  145. package/registry/blocks/contact-section/index.js +4 -0
  146. package/registry/blocks/csv-import/README.md +133 -0
  147. package/registry/blocks/csv-import/block.json +13 -0
  148. package/registry/blocks/csv-import/csv-import.jsx +479 -0
  149. package/registry/blocks/csv-import/demo.jsx +106 -0
  150. package/registry/blocks/csv-import/import.js +200 -0
  151. package/registry/blocks/csv-import/index.js +7 -0
  152. package/registry/blocks/data-table/README.md +386 -0
  153. package/registry/blocks/data-table/block.json +10 -0
  154. package/registry/blocks/data-table/data-table.jsx +1164 -0
  155. package/registry/blocks/data-table/demo.jsx +588 -0
  156. package/registry/blocks/data-table/index.js +5 -0
  157. package/registry/blocks/data-table/model.js +169 -0
  158. package/registry/blocks/diagram/README.md +108 -0
  159. package/registry/blocks/diagram/block.json +13 -0
  160. package/registry/blocks/diagram/demo.jsx +113 -0
  161. package/registry/blocks/diagram/diagram.jsx +481 -0
  162. package/registry/blocks/diagram/index.js +7 -0
  163. package/registry/blocks/diagram/mermaid.js +52 -0
  164. package/registry/blocks/diagram/palette.js +313 -0
  165. package/registry/blocks/diagram/source.js +98 -0
  166. package/registry/blocks/doc-editor/README.md +215 -0
  167. package/registry/blocks/doc-editor/block.json +15 -0
  168. package/registry/blocks/doc-editor/demo.jsx +225 -0
  169. package/registry/blocks/doc-editor/doc-editor.jsx +291 -0
  170. package/registry/blocks/doc-editor/doc.js +309 -0
  171. package/registry/blocks/doc-editor/index.js +7 -0
  172. package/registry/blocks/doc-editor/schema.jsx +56 -0
  173. package/registry/blocks/doc-editor/shadcn-classes.js +110 -0
  174. package/registry/blocks/doc-editor/theme.js +82 -0
  175. package/registry/blocks/doc-page/README.md +119 -0
  176. package/registry/blocks/doc-page/autosave.js +104 -0
  177. package/registry/blocks/doc-page/block.json +10 -0
  178. package/registry/blocks/doc-page/demo.jsx +137 -0
  179. package/registry/blocks/doc-page/doc-page.jsx +276 -0
  180. package/registry/blocks/doc-page/index.js +4 -0
  181. package/registry/blocks/email-render/README.md +151 -0
  182. package/registry/blocks/email-render/block.json +14 -0
  183. package/registry/blocks/email-render/demo.jsx +153 -0
  184. package/registry/blocks/email-render/doc-email.jsx +210 -0
  185. package/registry/blocks/email-render/email.jsx +221 -0
  186. package/registry/blocks/email-render/index.js +10 -0
  187. package/registry/blocks/email-render/render.js +50 -0
  188. package/registry/blocks/email-render/theme.js +215 -0
  189. package/registry/blocks/faq/README.md +101 -0
  190. package/registry/blocks/faq/block.json +11 -0
  191. package/registry/blocks/faq/demo.jsx +89 -0
  192. package/registry/blocks/faq/faq.jsx +220 -0
  193. package/registry/blocks/faq/index.js +4 -0
  194. package/registry/blocks/file-upload/README.md +138 -0
  195. package/registry/blocks/file-upload/block.json +10 -0
  196. package/registry/blocks/file-upload/demo.jsx +204 -0
  197. package/registry/blocks/file-upload/file-upload.jsx +635 -0
  198. package/registry/blocks/file-upload/index.js +4 -0
  199. package/registry/blocks/flow-canvas/README.md +420 -0
  200. package/registry/blocks/flow-canvas/block.json +13 -0
  201. package/registry/blocks/flow-canvas/board.js +819 -0
  202. package/registry/blocks/flow-canvas/chrome.jsx +649 -0
  203. package/registry/blocks/flow-canvas/context.jsx +35 -0
  204. package/registry/blocks/flow-canvas/demo.jsx +249 -0
  205. package/registry/blocks/flow-canvas/edges.jsx +107 -0
  206. package/registry/blocks/flow-canvas/flow-canvas.jsx +1329 -0
  207. package/registry/blocks/flow-canvas/index.js +41 -0
  208. package/registry/blocks/flow-canvas/nodes.jsx +392 -0
  209. package/registry/blocks/flow-canvas/theme.js +174 -0
  210. package/registry/blocks/footer/README.md +133 -0
  211. package/registry/blocks/footer/block.json +11 -0
  212. package/registry/blocks/footer/demo.jsx +119 -0
  213. package/registry/blocks/footer/footer.jsx +406 -0
  214. package/registry/blocks/footer/index.js +4 -0
  215. package/registry/blocks/gantt/README.md +220 -0
  216. package/registry/blocks/gantt/block.json +10 -0
  217. package/registry/blocks/gantt/demo.jsx +148 -0
  218. package/registry/blocks/gantt/gantt.jsx +923 -0
  219. package/registry/blocks/gantt/index.js +7 -0
  220. package/registry/blocks/gantt/rows.jsx +468 -0
  221. package/registry/blocks/gantt/schedule.js +278 -0
  222. package/registry/blocks/job-panel/README.md +80 -0
  223. package/registry/blocks/job-panel/block.json +3 -3
  224. package/registry/blocks/job-panel/demo.jsx +230 -3
  225. package/registry/blocks/job-panel/job-panel.jsx +169 -3
  226. package/registry/blocks/json-editor/README.md +133 -0
  227. package/registry/blocks/json-editor/block.json +10 -0
  228. package/registry/blocks/json-editor/demo.jsx +92 -0
  229. package/registry/blocks/json-editor/index.js +7 -0
  230. package/registry/blocks/json-editor/json-editor.jsx +305 -0
  231. package/registry/blocks/json-editor/json-tree.jsx +393 -0
  232. package/registry/blocks/json-editor/tree.js +225 -0
  233. package/registry/blocks/map/README.md +221 -0
  234. package/registry/blocks/map/block.json +14 -0
  235. package/registry/blocks/map/demo.jsx +201 -0
  236. package/registry/blocks/map/geo.js +198 -0
  237. package/registry/blocks/map/index.js +8 -0
  238. package/registry/blocks/map/list.jsx +128 -0
  239. package/registry/blocks/map/map-view.jsx +715 -0
  240. package/registry/blocks/map/maplibre.js +30 -0
  241. package/registry/blocks/map/marks.jsx +146 -0
  242. package/registry/blocks/map/theme.js +142 -0
  243. package/registry/blocks/menubar/README.md +109 -0
  244. package/registry/blocks/menubar/block.json +10 -0
  245. package/registry/blocks/menubar/demo.jsx +128 -0
  246. package/registry/blocks/menubar/index.js +4 -0
  247. package/registry/blocks/menubar/menubar.jsx +199 -0
  248. package/registry/blocks/mind-map/README.md +206 -0
  249. package/registry/blocks/mind-map/block.json +10 -0
  250. package/registry/blocks/mind-map/demo.jsx +103 -0
  251. package/registry/blocks/mind-map/index.js +31 -0
  252. package/registry/blocks/mind-map/mind-map.jsx +1125 -0
  253. package/registry/blocks/mind-map/topic.jsx +143 -0
  254. package/registry/blocks/mind-map/tree.js +645 -0
  255. package/registry/blocks/navigation-menu/README.md +124 -0
  256. package/registry/blocks/navigation-menu/block.json +10 -0
  257. package/registry/blocks/navigation-menu/demo.jsx +84 -0
  258. package/registry/blocks/navigation-menu/index.js +4 -0
  259. package/registry/blocks/navigation-menu/navigation-menu.jsx +448 -0
  260. package/registry/blocks/order-detail/README.md +109 -0
  261. package/registry/blocks/order-detail/block.json +10 -0
  262. package/registry/blocks/order-detail/demo.jsx +88 -0
  263. package/registry/blocks/order-detail/index.js +4 -0
  264. package/registry/blocks/order-detail/order-detail.jsx +354 -0
  265. package/registry/blocks/org-chart/README.md +145 -0
  266. package/registry/blocks/org-chart/block.json +10 -0
  267. package/registry/blocks/org-chart/demo.jsx +94 -0
  268. package/registry/blocks/org-chart/index.js +7 -0
  269. package/registry/blocks/org-chart/org-chart.jsx +631 -0
  270. package/registry/blocks/org-chart/org.js +245 -0
  271. package/registry/blocks/org-chart/person.jsx +96 -0
  272. package/registry/blocks/page-builder/README.md +257 -0
  273. package/registry/blocks/page-builder/block.json +14 -0
  274. package/registry/blocks/page-builder/demo.jsx +145 -0
  275. package/registry/blocks/page-builder/grapes.js +18 -0
  276. package/registry/blocks/page-builder/index.js +11 -0
  277. package/registry/blocks/page-builder/page-builder.jsx +704 -0
  278. package/registry/blocks/page-builder/page-view.jsx +33 -0
  279. package/registry/blocks/page-builder/page.js +118 -0
  280. package/registry/blocks/page-builder/panels.jsx +588 -0
  281. package/registry/blocks/page-builder/sections.js +209 -0
  282. package/registry/blocks/page-builder/theme.js +87 -0
  283. package/registry/blocks/page-builder/thumbs.jsx +189 -0
  284. package/registry/blocks/page-builder/tokens.js +329 -0
  285. package/registry/blocks/payment-methods/README.md +104 -0
  286. package/registry/blocks/payment-methods/block.json +10 -0
  287. package/registry/blocks/payment-methods/demo.jsx +71 -0
  288. package/registry/blocks/payment-methods/index.js +4 -0
  289. package/registry/blocks/payment-methods/payment-methods.jsx +302 -0
  290. package/registry/blocks/pdf-document/README.md +213 -0
  291. package/registry/blocks/pdf-document/block.json +13 -0
  292. package/registry/blocks/pdf-document/demo.jsx +136 -0
  293. package/registry/blocks/pdf-document/index.js +33 -0
  294. package/registry/blocks/pdf-document/money.js +83 -0
  295. package/registry/blocks/pdf-document/pdf-document.jsx +144 -0
  296. package/registry/blocks/pdf-document/primitives.jsx +271 -0
  297. package/registry/blocks/pdf-document/quote.jsx +179 -0
  298. package/registry/blocks/pdf-document/theme.js +193 -0
  299. package/registry/blocks/pdf-viewer/README.md +191 -0
  300. package/registry/blocks/pdf-viewer/block.json +14 -0
  301. package/registry/blocks/pdf-viewer/demo.jsx +252 -0
  302. package/registry/blocks/pdf-viewer/helpers.js +270 -0
  303. package/registry/blocks/pdf-viewer/index.js +8 -0
  304. package/registry/blocks/pdf-viewer/notes.jsx +195 -0
  305. package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1368 -0
  306. package/registry/blocks/pdf-viewer/pdfjs.js +37 -0
  307. package/registry/blocks/pdf-viewer/ssr-shim-end.js +6 -0
  308. package/registry/blocks/pdf-viewer/ssr-shim.js +44 -0
  309. package/registry/blocks/product-detail/README.md +217 -0
  310. package/registry/blocks/product-detail/block.json +10 -0
  311. package/registry/blocks/product-detail/demo.jsx +233 -0
  312. package/registry/blocks/product-detail/index.js +7 -0
  313. package/registry/blocks/product-detail/price.jsx +115 -0
  314. package/registry/blocks/product-detail/product-detail.jsx +348 -0
  315. package/registry/blocks/product-detail/product-media.jsx +102 -0
  316. package/registry/blocks/product-detail/product-options.jsx +144 -0
  317. package/registry/blocks/product-grid/README.md +162 -0
  318. package/registry/blocks/product-grid/block.json +10 -0
  319. package/registry/blocks/product-grid/demo.jsx +255 -0
  320. package/registry/blocks/product-grid/index.js +5 -0
  321. package/registry/blocks/product-grid/product-card.jsx +232 -0
  322. package/registry/blocks/product-grid/product-grid.jsx +235 -0
  323. package/registry/blocks/record-form/README.md +196 -0
  324. package/registry/blocks/record-form/block.json +10 -0
  325. package/registry/blocks/record-form/demo.jsx +525 -0
  326. package/registry/blocks/record-form/index.js +4 -0
  327. package/registry/blocks/record-form/record-fields.jsx +457 -0
  328. package/registry/blocks/record-form/record-form.jsx +415 -0
  329. package/registry/blocks/record-form/rules.js +81 -0
  330. package/registry/blocks/record-notes/README.md +139 -0
  331. package/registry/blocks/record-notes/block.json +10 -0
  332. package/registry/blocks/record-notes/demo.jsx +179 -0
  333. package/registry/blocks/record-notes/index.js +4 -0
  334. package/registry/blocks/record-notes/record-notes.jsx +266 -0
  335. package/registry/blocks/search-dialog/README.md +108 -0
  336. package/registry/blocks/search-dialog/block.json +10 -0
  337. package/registry/blocks/search-dialog/demo.jsx +116 -0
  338. package/registry/blocks/search-dialog/index.js +3 -0
  339. package/registry/blocks/search-dialog/search-dialog.jsx +235 -0
  340. package/registry/blocks/settings-list/README.md +171 -0
  341. package/registry/blocks/settings-list/block.json +10 -0
  342. package/registry/blocks/settings-list/demo.jsx +333 -0
  343. package/registry/blocks/settings-list/index.js +4 -0
  344. package/registry/blocks/settings-list/row-state.jsx +93 -0
  345. package/registry/blocks/settings-list/settings-list.jsx +349 -0
  346. package/registry/blocks/settings-list/settings-matrix.jsx +118 -0
  347. package/registry/blocks/setup-checklist/README.md +142 -0
  348. package/registry/blocks/setup-checklist/block.json +10 -0
  349. package/registry/blocks/setup-checklist/demo.jsx +159 -0
  350. package/registry/blocks/setup-checklist/index.js +4 -0
  351. package/registry/blocks/setup-checklist/setup-checklist.jsx +446 -0
  352. package/registry/blocks/share-dialog/README.md +113 -0
  353. package/registry/blocks/share-dialog/block.json +10 -0
  354. package/registry/blocks/share-dialog/demo.jsx +140 -0
  355. package/registry/blocks/share-dialog/index.js +3 -0
  356. package/registry/blocks/share-dialog/share-dialog.jsx +384 -0
  357. package/registry/blocks/sidebar/README.md +190 -0
  358. package/registry/blocks/sidebar/block.json +43 -0
  359. package/registry/blocks/sidebar/demo.jsx +577 -0
  360. package/registry/blocks/sidebar/index.js +4 -0
  361. package/registry/blocks/sidebar/sidebar-panel.jsx +149 -0
  362. package/registry/blocks/sidebar/sidebar.jsx +864 -0
  363. package/registry/blocks/signature-pad/README.md +137 -0
  364. package/registry/blocks/signature-pad/block.json +13 -0
  365. package/registry/blocks/signature-pad/colors.js +115 -0
  366. package/registry/blocks/signature-pad/demo.jsx +94 -0
  367. package/registry/blocks/signature-pad/index.js +6 -0
  368. package/registry/blocks/signature-pad/pad.js +7 -0
  369. package/registry/blocks/signature-pad/signature-pad.jsx +434 -0
  370. package/registry/blocks/signature-pad/signature.js +68 -0
  371. package/registry/blocks/sketch-canvas/README.md +280 -0
  372. package/registry/blocks/sketch-canvas/block.json +13 -0
  373. package/registry/blocks/sketch-canvas/demo.jsx +159 -0
  374. package/registry/blocks/sketch-canvas/excalidraw.js +40 -0
  375. package/registry/blocks/sketch-canvas/index.js +9 -0
  376. package/registry/blocks/sketch-canvas/palette.js +226 -0
  377. package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1188 -0
  378. package/registry/blocks/sketch-canvas/sketch.js +179 -0
  379. package/registry/blocks/sketch-canvas/ssr-shim-end.js +6 -0
  380. package/registry/blocks/sketch-canvas/ssr-shim.js +45 -0
  381. package/registry/blocks/sketch-canvas/stamps.js +104 -0
  382. package/registry/blocks/sketch-canvas/theme.js +131 -0
  383. package/registry/blocks/spreadsheet/README.md +307 -0
  384. package/registry/blocks/spreadsheet/block.json +13 -0
  385. package/registry/blocks/spreadsheet/cells.jsx +455 -0
  386. package/registry/blocks/spreadsheet/chrome.jsx +325 -0
  387. package/registry/blocks/spreadsheet/demo.jsx +177 -0
  388. package/registry/blocks/spreadsheet/grid.js +387 -0
  389. package/registry/blocks/spreadsheet/index.js +34 -0
  390. package/registry/blocks/spreadsheet/spreadsheet.jsx +1732 -0
  391. package/registry/blocks/spreadsheet/types.js +473 -0
  392. package/registry/blocks/stat-cards/README.md +173 -0
  393. package/registry/blocks/stat-cards/block.json +10 -0
  394. package/registry/blocks/stat-cards/demo.jsx +257 -0
  395. package/registry/blocks/stat-cards/index.js +4 -0
  396. package/registry/blocks/stat-cards/stat-cards.jsx +307 -0
  397. package/registry/blocks/stat-cards/usage.jsx +243 -0
  398. package/registry/blocks/task-list/README.md +176 -0
  399. package/registry/blocks/task-list/block.json +10 -0
  400. package/registry/blocks/task-list/demo.jsx +134 -0
  401. package/registry/blocks/task-list/index.js +5 -0
  402. package/registry/blocks/task-list/task-list.jsx +907 -0
  403. package/registry/blocks/task-model/README.md +455 -0
  404. package/registry/blocks/task-model/block.json +167 -0
  405. package/registry/blocks/task-model/changes.js +89 -0
  406. package/registry/blocks/task-model/dates.js +371 -0
  407. package/registry/blocks/task-model/demo.jsx +215 -0
  408. package/registry/blocks/task-model/dnd.jsx +240 -0
  409. package/registry/blocks/task-model/index.js +97 -0
  410. package/registry/blocks/task-model/model.js +919 -0
  411. package/registry/blocks/task-model/parts.jsx +449 -0
  412. package/registry/blocks/task-model/toolbar.jsx +271 -0
  413. package/registry/blocks/testimonials/README.md +139 -0
  414. package/registry/blocks/testimonials/block.json +11 -0
  415. package/registry/blocks/testimonials/demo.jsx +137 -0
  416. package/registry/blocks/testimonials/index.js +4 -0
  417. package/registry/blocks/testimonials/testimonials.jsx +430 -0
  418. package/registry/blocks/transaction-list/README.md +126 -0
  419. package/registry/blocks/transaction-list/block.json +10 -0
  420. package/registry/blocks/transaction-list/demo.jsx +79 -0
  421. package/registry/blocks/transaction-list/index.js +4 -0
  422. package/registry/blocks/transaction-list/transaction-list.jsx +383 -0
  423. package/registry/blocks/whiteboard/README.md +179 -0
  424. package/registry/blocks/whiteboard/block.json +10 -0
  425. package/registry/blocks/whiteboard/demo.jsx +169 -0
  426. package/registry/blocks/whiteboard/index.js +6 -0
  427. package/registry/blocks/whiteboard/templates.js +150 -0
  428. package/registry/blocks/whiteboard/whiteboard.jsx +721 -0
  429. package/src/install.mjs +18 -0
  430. package/src/shadcn.mjs +8 -0
  431. package/templates/next/files/README.md +145 -11
  432. package/templates/next/files/app/globals.css +55 -4
  433. package/templates/next/files/check/run.mjs +3 -3
  434. package/templates/next/files/components/ui/avatar.jsx +141 -0
  435. package/templates/next/files/components/ui/breadcrumb.jsx +149 -0
  436. package/templates/next/files/components/ui/button-group.jsx +70 -0
  437. package/templates/next/files/components/ui/button.jsx +132 -12
  438. package/templates/next/files/components/ui/calendar.jsx +609 -0
  439. package/templates/next/files/components/ui/carousel.jsx +281 -0
  440. package/templates/next/files/components/ui/collapsible.jsx +208 -0
  441. package/templates/next/files/components/ui/combobox.jsx +608 -0
  442. package/templates/next/files/components/ui/command.jsx +577 -0
  443. package/templates/next/files/components/ui/context-menu.jsx +191 -0
  444. package/templates/next/files/components/ui/date-picker.jsx +360 -0
  445. package/templates/next/files/components/ui/distribution.jsx +117 -0
  446. package/templates/next/files/components/ui/feedback.jsx +143 -0
  447. package/templates/next/files/components/ui/field.jsx +115 -3
  448. package/templates/next/files/components/ui/file-input.jsx +234 -0
  449. package/templates/next/files/components/ui/icon.jsx +130 -8
  450. package/templates/next/files/components/ui/image.jsx +143 -0
  451. package/templates/next/files/components/ui/index.js +47 -2
  452. package/templates/next/files/components/ui/input-group.jsx +213 -0
  453. package/templates/next/files/components/ui/input-otp.jsx +170 -0
  454. package/templates/next/files/components/ui/item.jsx +206 -0
  455. package/templates/next/files/components/ui/menu.jsx +454 -82
  456. package/templates/next/files/components/ui/notice.jsx +126 -14
  457. package/templates/next/files/components/ui/pagination.jsx +156 -0
  458. package/templates/next/files/components/ui/popover.jsx +284 -0
  459. package/templates/next/files/components/ui/radio.jsx +193 -0
  460. package/templates/next/files/components/ui/rating.jsx +131 -0
  461. package/templates/next/files/components/ui/resizable.jsx +338 -0
  462. package/templates/next/files/components/ui/scroll-area.jsx +123 -0
  463. package/templates/next/files/components/ui/separator.jsx +119 -0
  464. package/templates/next/files/components/ui/sheet.jsx +96 -11
  465. package/templates/next/files/components/ui/slider.jsx +174 -0
  466. package/templates/next/files/components/ui/stat.jsx +574 -0
  467. package/templates/next/files/components/ui/steps.jsx +338 -0
  468. package/templates/next/files/components/ui/swatch.jsx +33 -0
  469. package/templates/next/files/components/ui/table.jsx +130 -0
  470. package/templates/next/files/components/ui/toast.jsx +440 -0
  471. package/templates/next/files/components/ui/toggle.jsx +195 -0
  472. package/templates/next/files/components/ui/tooltip.jsx +270 -0
  473. package/templates/next/template.json +264 -8
@@ -0,0 +1,66 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "json-editor",
4
+ "type": "registry:block",
5
+ "title": "JsonEditor",
6
+ "description": "A JSON editor in the workspace theme with two views of one value: a Tree of folding rows (edit a value or rename a key in place, add keys and items, change a type, copy a path, delete with Undo, arrow keys throughout) and Code (the code-editor block in JSON mode). Checked as it changes: the status line says what is wrong in plain words and which line, and clicking it goes there. A Format button, copy as pretty or one-line JSON, a friendly empty state with starters, and an optional Save that is optimistic and rolls back with a notice when it fails.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/core.json",
9
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/code-editor.json"
10
+ ],
11
+ "dependencies": [],
12
+ "files": [
13
+ {
14
+ "path": "registry/blocks/json-editor/index.js",
15
+ "type": "registry:component",
16
+ "target": "@components/makersclaw/blocks/json-editor/index.js",
17
+ "content": "// The JsonEditor block — `makersclaw add json-editor`. Fixed: compose it\n// through its props (README.md beside this file). Built on code-editor:\n// the Code view is a CodeEditor in JSON mode. The tree helpers are plain\n// functions of plain values and run anywhere, the server included.\nexport { JsonEditor } from \"./json-editor\";\nexport { JsonTree } from \"./json-tree\";\nexport { typeOf, pathLabel, getAt, setAt, removeAt, insertAt, renameKey, flattenTree, expandedTo, summary, preview } from \"./tree\";\n"
18
+ },
19
+ {
20
+ "path": "registry/blocks/json-editor/json-editor.jsx",
21
+ "type": "registry:component",
22
+ "target": "@components/makersclaw/blocks/json-editor/json-editor.jsx",
23
+ "content": "\"use client\";\n\n// JsonEditor — a JSON value, edited as a tree of rows or as text, checked\n// as it changes. Built on code-editor (the Code view is a CodeEditor in\n// JSON mode) and never on CodeMirror directly.\n//\n// The value is the truth: a plain JSON value in, a plain JSON value out on\n// every valid edit. The text is what the Code view shows; while it does\n// not parse, the last valid value stands, the Tree view waits, and the\n// status line says what is wrong and where (click it to go there).\nimport { useEffect, useRef, useState } from \"react\";\nimport { Button, Empty, Menu, Notice, ToggleGroup, cx, toast } from \"../../ui\";\nimport { CodeEditor, checkJson, formatJson, stringifyJson, useCodeEditor } from \"../code-editor\";\nimport { JsonTree } from \"./json-tree\";\nimport { allContainers, expandedTo, summary, typeOf } from \"./tree\";\n\nconst same = (a, b) => JSON.stringify(a) === JSON.stringify(b);\nconst px = (v) => (typeof v === \"number\" ? `${v}px` : v);\n\nexport function JsonEditor({\n value,\n defaultValue,\n onChange,\n onSave,\n view: viewProp,\n defaultView = \"tree\",\n onViewChange,\n readOnly = false,\n title,\n expandDepth = 2,\n height,\n maxHeight = 480,\n placeholder = 'Paste or type JSON — {\"name\": \"Ada\"}',\n label,\n className,\n}) {\n const initial = value !== undefined ? value : defaultValue;\n const [data, setData] = useState(initial);\n const [text, setText] = useState(() => stringifyJson(initial));\n const [ownView, setOwnView] = useState(defaultView);\n const view = viewProp ?? ownView;\n const [open, setOpen] = useState(() => expandedTo(initial, expandDepth));\n const [baseline, setBaseline] = useState(() => JSON.stringify(initial ?? null));\n const [saving, setSaving] = useState(false);\n const [saveError, setSaveError] = useState(null);\n const [savedAt, setSavedAt] = useState(null);\n const ed = useCodeEditor();\n\n const check = checkJson(text, { allowEmpty: true });\n const empty = !text.trim();\n const valid = check.ok;\n const dirty = !empty && valid && JSON.stringify(data ?? null) !== baseline;\n\n const setView = (v) => {\n if (viewProp === undefined) setOwnView(v);\n onViewChange?.(v);\n };\n\n // Controlled: a value that is not ours is another record — load it.\n const last = useRef(initial);\n last.current = data;\n const valueKey = value === undefined ? undefined : JSON.stringify(value);\n useEffect(() => {\n if (value === undefined || same(value, last.current)) return;\n setData(value);\n setText(stringifyJson(value));\n setOpen(expandedTo(value, expandDepth));\n setBaseline(JSON.stringify(value ?? null));\n setSaveError(null);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [valueKey]);\n\n const emit = (next) => {\n setData(next);\n setSavedAt(null);\n onChange?.(next);\n };\n\n // The Code view typed: keep the text; a text that parses is the value.\n const onText = (t) => {\n setText(t);\n const r = checkJson(t, { allowEmpty: true });\n if (r.ok && !r.empty && !same(r.value, data)) emit(r.value);\n };\n\n // The Tree view edited: the value, then its text.\n const onTreeEdit = (next, meta) => {\n emit(next);\n setText(stringifyJson(next));\n if (meta?.undo) toast(meta.message, { action: { label: \"Undo\", onClick: meta.undo } });\n };\n\n const start = (v) => {\n emit(v);\n setText(stringifyJson(v));\n setView(\"tree\");\n };\n\n async function save() {\n if (!onSave || !dirty || saving) return;\n const snapshot = data;\n const before = baseline;\n // Optimistic: counted as saved at once; a refusal puts \"unsaved\" back.\n setBaseline(JSON.stringify(snapshot ?? null));\n setSaving(true);\n setSaveError(null);\n let r;\n try {\n r = await onSave(snapshot);\n } catch (e) {\n r = { ok: false, error: e?.message };\n }\n setSaving(false);\n if (r && r.ok !== false) {\n setSavedAt(Date.now());\n return;\n }\n setBaseline(before);\n setSaveError(r?.error || \"The save did not go through.\");\n }\n\n const format = () => {\n const out = formatJson(text);\n if (out != null && out !== text) setText(out);\n };\n\n async function copyText(t, what) {\n try {\n await navigator.clipboard.writeText(t);\n toast.success(`Copied ${what}`);\n } catch {\n toast.error(\"Could not reach the clipboard\");\n }\n }\n\n const goToError = () => {\n if (valid) return;\n setView(\"code\");\n const { line, col } = check.error;\n // The editor mounts on the switch; go there once it has.\n setTimeout(() => ed.current?.goTo(line, col), 0);\n };\n\n const onKeyDown = (e) => {\n if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === \"s\" && onSave) {\n e.preventDefault();\n save();\n }\n };\n\n const box = height != null ? { height: px(height) } : { maxHeight: px(maxHeight) };\n const container = data !== null && typeof data === \"object\";\n\n const moreItems = [\n { label: \"Copy JSON\", icon: \"copy\", disabled: !valid || empty, onSelect: () => copyText(formatJson(text) ?? text, \"the JSON\") },\n { label: \"Copy on one line\", icon: \"copy\", disabled: !valid || empty, onSelect: () => copyText(formatJson(text, { indent: 0 }) ?? text, \"the JSON on one line\") },\n view === \"tree\" && container && { type: \"separator\" },\n view === \"tree\" && container && { label: \"Expand all\", icon: \"maximize\", onSelect: () => setOpen(allContainers(data)) },\n view === \"tree\" && container && { label: \"Collapse all\", icon: \"minus\", onSelect: () => setOpen(new Set()) },\n view === \"code\" && !readOnly && { type: \"separator\" },\n view === \"code\" && !readOnly && { label: \"Format\", icon: \"wand\", shortcut: \"⇧⌥F\", disabled: !valid || empty, onSelect: format },\n view === \"code\" && !readOnly && { label: \"Put on one line\", icon: \"minus\", disabled: !valid || empty, onSelect: () => setText(formatJson(text, { indent: 0 }) ?? text) },\n ];\n\n return (\n // raw: a plain frame that hears Cmd/Ctrl+S from either view; no control of its own.\n <div className={cx(\"flex min-w-0 flex-col overflow-hidden rounded-lg border border-border bg-card text-card-foreground\", className)} onKeyDown={onKeyDown}>\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-2 border-b border-border py-2 ps-4 pe-2\">\n <div className=\"me-auto min-w-0 py-0.5\">\n {title ? <p className=\"truncate text-sm font-medium\">{title}</p> : null}\n <Status\n empty={empty}\n check={check}\n data={data}\n dirty={dirty}\n saving={saving}\n savedAt={savedAt}\n onSave={onSave}\n onError={goToError}\n />\n </div>\n <div className=\"flex items-center gap-1\">\n <ToggleGroup\n size=\"sm\"\n aria-label=\"View\"\n value={view}\n onChange={setView}\n items={[\n { value: \"tree\", label: \"Tree\", icon: \"list\", disabled: !valid && !empty },\n { value: \"code\", label: \"Code\", icon: \"code\" },\n ]}\n />\n {view === \"code\" && !readOnly ? (\n <Button size=\"sm\" variant=\"quiet\" icon=\"wand\" onClick={format} disabled={!valid || empty} className=\"max-sm:hidden\">\n Format\n </Button>\n ) : null}\n <Menu items={moreItems} label=\"More JSON actions\" />\n {onSave && !readOnly ? (\n <Button size=\"sm\" variant=\"primary\" onClick={save} disabled={!dirty} loading={saving} className=\"ms-1\">\n Save\n </Button>\n ) : null}\n </div>\n </div>\n\n {saveError ? (\n <Notice\n tone=\"danger\"\n title=\"Your changes were not saved\"\n className=\"m-3 mb-0\"\n onDismiss={() => setSaveError(null)}\n action={\n <Button size=\"sm\" onClick={save}>\n Try again\n </Button>\n }\n >\n {saveError} They are still here.\n </Notice>\n ) : null}\n\n {view === \"code\" ? (\n <CodeEditor\n language=\"json\"\n value={text}\n onChange={onText}\n onSave={onSave ? () => save() : undefined}\n readOnly={readOnly}\n placeholder={placeholder}\n height={height}\n minHeight={height != null ? undefined : 160}\n maxHeight={maxHeight}\n editorRef={ed}\n frameless\n label={label || title || \"JSON\"}\n autoFocus={empty}\n />\n ) : empty ? (\n <div className=\"p-3\">\n <Empty\n icon=\"code\"\n title=\"No JSON yet\"\n body=\"Start with an empty object or list and add to it here, or paste JSON you already have.\"\n action={\n readOnly ? null : (\n <div className=\"flex flex-wrap justify-center gap-2\">\n <Button size=\"sm\" variant=\"primary\" icon=\"plus\" onClick={() => start({})}>\n New object\n </Button>\n <Button size=\"sm\" icon=\"list\" onClick={() => start([])}>\n New list\n </Button>\n <Button size=\"sm\" variant=\"quiet\" icon=\"code\" onClick={() => setView(\"code\")}>\n Paste JSON\n </Button>\n </div>\n )\n }\n />\n </div>\n ) : (\n <JsonTree\n value={data}\n onEdit={onTreeEdit}\n open={open}\n setOpen={setOpen}\n readOnly={readOnly}\n onNotice={(m) => toast(m)}\n label={label || title || \"JSON\"}\n style={box}\n />\n )}\n </div>\n );\n}\n\n/** The line under the title: what the JSON is, or what is wrong with it. */\nfunction Status({ empty, check, data, dirty, saving, savedAt, onSave, onError }) {\n if (empty) return <p className=\"text-xs text-muted-foreground\">Empty</p>;\n if (!check.ok) {\n const { line, message } = check.error;\n return (\n // raw: the error as a link to its line — a Button's link variant has no danger tone.\n <button\n type=\"button\"\n onClick={onError}\n className=\"flex max-w-full items-center gap-1.5 rounded-sm text-start text-xs text-destructive outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <span aria-hidden=\"true\" className=\"size-1.5 shrink-0 rounded-full bg-destructive\" />\n <span className=\"truncate\">\n Line {line}: {message}\n </span>\n </button>\n );\n }\n const t = typeOf(data);\n const shape = t === \"object\" || t === \"array\" ? summary(data) : t === \"string\" ? \"text\" : t;\n return (\n <p className=\"truncate text-xs text-muted-foreground\" aria-live=\"polite\">\n Valid JSON · {shape}\n {onSave ? (saving ? \" · Saving…\" : dirty ? \" · Unsaved changes\" : savedAt ? \" · Saved\" : \"\") : \"\"}\n </p>\n );\n}\n"
24
+ },
25
+ {
26
+ "path": "registry/blocks/json-editor/json-tree.jsx",
27
+ "type": "registry:component",
28
+ "target": "@components/makersclaw/blocks/json-editor/json-tree.jsx",
29
+ "content": "\"use client\";\n\n// The tree view: a JSON value as folding rows, edited in place.\n//\n// One row per key (or list item). Containers fold; text, numbers,\n// true/false and null edit inline (double-click, or Enter on the row);\n// keys rename (double-click the key, or F2). Each row's ⋯ menu adds, copies\n// its path or value, changes its type, or deletes it (with Undo). The rows\n// are a WAI-ARIA tree: one Tab stop, arrows move and fold, Home/End jump.\nimport { useEffect, useRef, useState } from \"react\";\nimport { Icon, IconButton, Menu, cx } from \"../../ui\";\nimport { convertType, editText, flattenTree, insertAt, isContainer, parseEdit, pathId, pathLabel, preview, removeAt, renameKey, setAt, summary, uniqueKey } from \"./tree\";\n\n// Types are categories: the chart hues, as the code view paints them.\nconst TONE = {\n string: \"text-chart-2\",\n number: \"text-chart-3 tabular-nums\",\n boolean: \"text-chart-3\",\n null: \"text-chart-3 italic\",\n};\n\nconst TYPES = [\n { value: \"string\", label: \"Text\" },\n { value: \"number\", label: \"Number\" },\n { value: \"boolean\", label: \"True / false\" },\n { value: \"null\", label: \"Empty (null)\" },\n { value: \"object\", label: \"Object\" },\n { value: \"array\", label: \"List\" },\n];\n\nconst INDENT = 18;\n\nasync function copy(text) {\n try {\n await navigator.clipboard.writeText(text);\n return true;\n } catch {\n return false;\n }\n}\n\nexport function JsonTree({ value, onEdit, open, setOpen, readOnly = false, onNotice, className, style, label = \"JSON\" }) {\n const rows = flattenTree(value, open);\n const [focusId, setFocusId] = useState(rows[0]?.id ?? null);\n const [editing, setEditing] = useState(null); // { id, field: \"key\" | \"value\", draft, error }\n const [hoverId, setHoverId] = useState(null);\n const rowRefs = useRef(new Map());\n const wantFocus = useRef(null);\n\n // Keep a focused row that still exists.\n const current = rows.find((r) => r.id === focusId) ? focusId : rows[0]?.id ?? null;\n\n useEffect(() => {\n if (!wantFocus.current) return;\n const el = rowRefs.current.get(wantFocus.current);\n wantFocus.current = null;\n el?.focus();\n });\n\n const focusRow = (id) => {\n setFocusId(id);\n wantFocus.current = id;\n };\n\n const toggle = (row, to) => {\n if (!isContainer(row.value)) return;\n const next = new Set(open);\n const want = to ?? !open.has(row.id);\n if (want) next.add(row.id);\n else next.delete(row.id);\n setOpen(next);\n };\n\n // ---- edits: each hands the new root out, with words for an Undo toast.\n function commitValue(row, draft) {\n const next = parseEdit(draft, row.type);\n setEditing(null);\n focusRow(row.id);\n if (Object.is(next, row.value) || JSON.stringify(next) === JSON.stringify(row.value)) return;\n onEdit(setAt(value, row.path, next));\n }\n\n function commitKey(row, draft) {\n const r = renameKey(value, row.path, draft.trim());\n if (r.error) {\n setEditing((e) => e && { ...e, error: r.error });\n return;\n }\n setEditing(null);\n const newPath = [...row.path.slice(0, -1), draft.trim()];\n focusRow(pathId(newPath));\n if (r.root !== value) onEdit(r.root);\n }\n\n function add(row) {\n const parentPath = row ? row.path : [];\n const parent = row ? row.value : value;\n const openNext = new Set(open);\n if (row) openNext.add(row.id);\n setOpen(openNext);\n if (Array.isArray(parent)) {\n const path = [...parentPath, parent.length];\n onEdit(insertAt(value, parentPath, { value: \"\" }));\n setFocusId(pathId(path)); // the field takes focus, not the row\n setEditing({ id: pathId(path), field: \"value\", draft: \"\" });\n } else {\n const key = uniqueKey(parent);\n const path = [...parentPath, key];\n onEdit(insertAt(value, parentPath, { key, value: \"\" }));\n setFocusId(pathId(path));\n setEditing({ id: pathId(path), field: \"key\", draft: key, select: true });\n }\n }\n\n function remove(row) {\n const before = value;\n const { root } = removeAt(value, row.path);\n // Focus stays in place: the next sibling (in a list, the next item\n // takes this one's index), else the one before, else the parent.\n const siblings = rows.filter((r) => r.parentId === row.parentId);\n const pos = siblings.findIndex((r) => r.id === row.id);\n const inList = typeof row.key === \"number\";\n const target = pos < siblings.length - 1 ? (inList ? row.id : siblings[pos + 1].id) : (siblings[pos - 1]?.id ?? row.parentId);\n onEdit(root, { undo: () => onEdit(before), message: `Deleted ${inList ? `item ${row.key}` : `\"${row.key}\"`}` });\n focusRow(target);\n }\n\n function retype(row, type) {\n if (row.type === type) return;\n onEdit(setAt(value, row.path, convertType(row.value, type)));\n if (type === \"object\" || type === \"array\") setOpen(new Set(open).add(row.id));\n }\n\n // ---- keys: the tree pattern.\n function onKeyDown(e, row, i) {\n if (editing) return;\n const go = (j) => {\n if (rows[j]) {\n e.preventDefault();\n focusRow(rows[j].id);\n }\n };\n switch (e.key) {\n case \"ArrowDown\":\n return go(i + 1);\n case \"ArrowUp\":\n return go(i - 1);\n case \"Home\":\n return go(0);\n case \"End\":\n return go(rows.length - 1);\n case \"ArrowRight\":\n e.preventDefault();\n if (isContainer(row.value) && !row.open) toggle(row, true);\n else if (row.open && rows[i + 1]?.parentId === row.id) focusRow(rows[i + 1].id);\n return;\n case \"ArrowLeft\": {\n e.preventDefault();\n if (row.open) return toggle(row, false);\n const parent = rows.find((r) => r.id === row.parentId);\n if (parent) focusRow(parent.id);\n return;\n }\n case \"Enter\":\n case \" \":\n e.preventDefault();\n if (isContainer(row.value)) toggle(row);\n else if (!readOnly) setEditing({ id: row.id, field: \"value\", draft: editText(row.value), select: true });\n return;\n case \"F2\":\n if (!readOnly && typeof row.key === \"string\") {\n e.preventDefault();\n setEditing({ id: row.id, field: \"key\", draft: row.key, select: true });\n }\n return;\n case \"Delete\":\n case \"Backspace\":\n if (!readOnly && row.path.length) {\n e.preventDefault();\n remove(row);\n }\n return;\n default:\n }\n }\n\n const rootIsContainer = isContainer(value);\n\n return (\n <div className={cx(\"overflow-auto py-1.5\", className)} style={style}>\n <div role=\"tree\" aria-label={label} className=\"px-1.5\" onMouseLeave={() => setHoverId(null)}>\n {rows.map((row, i) => {\n const container = isContainer(row.value);\n const isEditing = editing?.id === row.id;\n const menu = row.id !== current && row.id !== hoverId ? [] : readOnly\n ? [\n { label: \"Copy path\", icon: \"link\", onSelect: () => copy(pathLabel(row.path)).then((ok) => onNotice?.(ok ? `Copied ${pathLabel(row.path)}` : \"Could not reach the clipboard\")) },\n { label: \"Copy value\", icon: \"copy\", onSelect: () => copy(container ? JSON.stringify(row.value, null, 2) : editText(row.value)).then((ok) => onNotice?.(ok ? \"Copied the value\" : \"Could not reach the clipboard\")) },\n ]\n : [\n !container && { label: \"Edit value\", icon: \"pencil\", shortcut: \"↵\", onSelect: () => setEditing({ id: row.id, field: \"value\", draft: editText(row.value), select: true }) },\n typeof row.key === \"string\" && { label: \"Rename key\", icon: \"type\", shortcut: \"F2\", onSelect: () => setEditing({ id: row.id, field: \"key\", draft: row.key, select: true }) },\n container && { label: Array.isArray(row.value) ? \"Add item\" : \"Add key\", icon: \"plus\", onSelect: () => add(row) },\n { type: \"separator\" },\n { label: \"Copy path\", icon: \"link\", onSelect: () => copy(pathLabel(row.path)).then((ok) => onNotice?.(ok ? `Copied ${pathLabel(row.path)}` : \"Could not reach the clipboard\")) },\n { label: \"Copy value\", icon: \"copy\", onSelect: () => copy(container ? JSON.stringify(row.value, null, 2) : editText(row.value)).then((ok) => onNotice?.(ok ? \"Copied the value\" : \"Could not reach the clipboard\")) },\n { label: \"Change type\", icon: \"layers\", items: [{ type: \"radio\", value: row.type, onValueChange: (t) => retype(row, t), options: TYPES }] },\n row.path.length > 0 && { type: \"separator\" },\n row.path.length > 0 && { label: \"Delete\", icon: \"trash\", tone: \"danger\", shortcut: \"⌫\", onSelect: () => remove(row) },\n ];\n return (\n // raw: a treeitem row — the WAI-ARIA tree needs the row itself\n // focusable with its own keys; no kit part is a tree row.\n <div\n key={row.id}\n ref={(el) => {\n if (el) rowRefs.current.set(row.id, el);\n else rowRefs.current.delete(row.id);\n }}\n role=\"treeitem\"\n aria-level={row.depth + 1}\n aria-expanded={container ? row.open : undefined}\n aria-selected={row.id === current}\n aria-label={`${row.key ?? \"value\"}: ${container ? summary(row.value) : editText(row.value)}`}\n tabIndex={row.id === current ? 0 : -1}\n onFocus={() => setFocusId(row.id)}\n onMouseEnter={() => setHoverId(row.id)}\n onKeyDown={(e) => onKeyDown(e, row, i)}\n onClick={() => container && !isEditing && toggle(row)}\n className={cx(\n \"group/row relative flex min-h-9 items-center gap-2 rounded-md pe-1 font-mono text-[13px] outline-none max-sm:min-h-11\",\n \"transition-colors duration-150 motion-reduce:transition-none\",\n \"hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring\",\n row.id === current && \"bg-muted\",\n container && \"cursor-pointer\",\n )}\n style={{ paddingInlineStart: 6 + row.depth * INDENT }}\n >\n {Array.from({ length: row.depth }, (_, d) => (\n <span key={d} aria-hidden=\"true\" className=\"absolute inset-y-0 w-px bg-border\" style={{ insetInlineStart: 6 + d * INDENT + 8 }} />\n ))}\n <span aria-hidden=\"true\" className=\"grid size-4 shrink-0 place-items-center text-muted-foreground\">\n {container ? <Icon name=\"chevron-right\" size={14} className={cx(\"transition-transform duration-150 motion-reduce:transition-none\", row.open && \"rotate-90\")} /> : null}\n </span>\n\n {row.key !== null ? (\n isEditing && editing.field === \"key\" ? (\n <InlineInput\n draft={editing}\n onDraft={(d) => setEditing({ ...editing, draft: d, error: null })}\n onCommit={(d) => commitKey(row, d)}\n onCancel={() => {\n setEditing(null);\n focusRow(row.id);\n }}\n label={`Rename ${row.key}`}\n />\n ) : (\n <span\n className={cx(\"max-w-[45%] shrink-0 truncate\", typeof row.key === \"number\" ? \"text-muted-foreground tabular-nums\" : \"text-foreground\")}\n title={typeof row.key === \"string\" && row.key.length > 24 ? row.key : undefined}\n onDoubleClick={(e) => {\n if (readOnly || typeof row.key !== \"string\") return;\n e.stopPropagation();\n setEditing({ id: row.id, field: \"key\", draft: row.key, select: true });\n }}\n >\n {row.key}\n </span>\n )\n ) : null}\n {row.key !== null ? <span aria-hidden=\"true\" className=\"-ms-1.5 text-muted-foreground\">:</span> : null}\n\n {container ? (\n <span className=\"min-w-0 truncate font-sans text-xs text-muted-foreground\" title={row.open ? undefined : preview(row.value, 400)}>\n {row.open ? summary(row.value) : preview(row.value)}\n </span>\n ) : isEditing && editing.field === \"value\" ? (\n <InlineInput\n draft={editing}\n onDraft={(d) => setEditing({ ...editing, draft: d })}\n onCommit={(d) => commitValue(row, d)}\n onCancel={() => {\n setEditing(null);\n focusRow(row.id);\n }}\n label={`Edit ${row.key ?? \"value\"}`}\n quoted={row.type === \"string\"}\n />\n ) : (\n <span\n className={cx(\"min-w-0 truncate\", TONE[row.type])}\n title={editText(row.value).length > 40 ? editText(row.value) : undefined}\n onDoubleClick={(e) => {\n if (readOnly) return;\n e.stopPropagation();\n setEditing({ id: row.id, field: \"value\", draft: editText(row.value), select: true });\n }}\n >\n {row.type === \"string\" ? JSON.stringify(row.value) : String(row.value)}\n </span>\n )}\n\n {/* The row's actions: only on the row in hand (focused or under the\n pointer), so the tree stays one Tab stop and a long list\n does not carry a menu per row. */}\n {row.id === current || row.id === hoverId ? (\n <span className=\"ms-auto flex shrink-0 items-center gap-0.5 ps-3\" onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}>\n {container && !readOnly ? (\n <IconButton icon=\"plus\" size=\"sm\" label={Array.isArray(row.value) ? `Add an item to ${pathLabel(row.path)}` : `Add a key to ${pathLabel(row.path)}`} onClick={() => add(row)} className=\"max-sm:hidden\" />\n ) : null}\n <Menu items={menu} label={`Actions for ${pathLabel(row.path)}`} />\n </span>\n ) : null}\n {isEditing && editing.error ? (\n <span role=\"alert\" className=\"absolute top-full z-10 mt-1 rounded-md border border-border bg-popover px-2 py-1 font-sans text-xs text-destructive shadow-lg\" style={{ insetInlineStart: 6 + row.depth * INDENT + 24 }}>\n {editing.error}\n </span>\n ) : null}\n </div>\n );\n })}\n {rootIsContainer && !readOnly ? (\n <div className=\"px-1.5 pt-1\">\n {/* raw: the tree's last row — a quiet \"add\" in the rows' own shape, a Button would sit off the grid */}\n <button\n type=\"button\"\n onClick={() => add(null)}\n className=\"flex min-h-9 w-full items-center gap-2 rounded-md ps-1.5 text-start text-sm text-muted-foreground outline-none transition-colors duration-150 hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring max-sm:min-h-11 motion-reduce:transition-none\"\n >\n <Icon name=\"plus\" size={14} />\n {Array.isArray(value) ? \"Add item\" : \"Add key\"}\n </button>\n </div>\n ) : null}\n </div>\n </div>\n );\n}\n\n/** The field a key or a value is edited in, the row's own height. */\nfunction InlineInput({ draft, onDraft, onCommit, onCancel, label, quoted = false }) {\n const ref = useRef(null);\n const done = useRef(false);\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n el.focus();\n if (draft.select) el.select();\n // Once: the field opened.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n const width = Math.min(48, Math.max(6, String(draft.draft).length + 2));\n return (\n <span className={cx(\"relative inline-flex min-w-0 items-center\", quoted && \"before:text-chart-2 before:content-['“'] after:text-chart-2 after:content-['”']\")}>\n {/* raw: an inline field the row's height (the kit's Input is a 44px form control) */}\n <input\n ref={ref}\n aria-label={label}\n aria-invalid={draft.error ? true : undefined}\n value={draft.draft}\n onChange={(e) => onDraft(e.target.value)}\n onClick={(e) => e.stopPropagation()}\n onDoubleClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n e.stopPropagation();\n if (e.key === \"Enter\") {\n e.preventDefault();\n done.current = true;\n onCommit(e.currentTarget.value);\n // Still here (the key was refused): the next blur commits again.\n queueMicrotask(() => {\n done.current = false;\n });\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n done.current = true;\n onCancel();\n }\n }}\n onBlur={(e) => {\n if (done.current) return;\n onCommit(e.currentTarget.value);\n }}\n style={{ width: `${width}ch` }}\n className={cx(\n \"h-7 max-w-[60vw] rounded-sm border bg-background px-1.5 font-mono text-[13px] text-foreground outline-none max-sm:text-base\",\n draft.error ? \"border-destructive\" : \"border-ring\",\n )}\n />\n </span>\n );\n}\n"
30
+ },
31
+ {
32
+ "path": "registry/blocks/json-editor/tree.js",
33
+ "type": "registry:component",
34
+ "target": "@components/makersclaw/blocks/json-editor/tree.js",
35
+ "content": "// The tree view's model: a JSON value seen as rows, and the edits a row\n// makes, as plain functions of plain values (node and the server run them).\n//\n// A PATH is the list of keys and indexes from the root to a value:\n// [\"deal\", \"owner\", \"email\"], [\"deal\", \"tags\", 0]. Every edit returns a NEW\n// root and leaves the old one untouched, so undo is keeping the old one.\n\n/** \"object\" · \"array\" · \"string\" · \"number\" · \"boolean\" · \"null\". */\nexport function typeOf(v) {\n if (v === null) return \"null\";\n if (Array.isArray(v)) return \"array\";\n const t = typeof v;\n return t === \"object\" ? \"object\" : t === \"string\" || t === \"number\" || t === \"boolean\" ? t : \"null\";\n}\n\nexport const isContainer = (v) => v !== null && typeof v === \"object\";\n\n/** A stable id for a path: the key a row is remembered by. */\nexport const pathId = (path) => JSON.stringify(path);\n\nconst IDENT = /^[A-Za-z_$][\\w$]*$/;\n\n/** How people write a path: deal.owner.email, deal.tags[0], meta[\"first name\"]. */\nexport function pathLabel(path) {\n let out = \"\";\n for (const k of path) {\n if (typeof k === \"number\") out += `[${k}]`;\n else if (IDENT.test(k)) out += out ? `.${k}` : k;\n else out += `[${JSON.stringify(k)}]`;\n }\n return out || \"(root)\";\n}\n\n/** \"3 keys\" · \"1 item\" · \"empty\". */\nexport function summary(v) {\n if (Array.isArray(v)) return v.length ? `${v.length} item${v.length === 1 ? \"\" : \"s\"}` : \"empty list\";\n if (isContainer(v)) {\n const n = Object.keys(v).length;\n return n ? `${n} key${n === 1 ? \"\" : \"s\"}` : \"empty object\";\n }\n return \"\";\n}\n\nexport function getAt(root, path) {\n let v = root;\n for (const k of path) {\n if (!isContainer(v)) return undefined;\n v = v[k];\n }\n return v;\n}\n\n/** The root with the value at `path` replaced. */\nexport function setAt(root, path, value) {\n if (!path.length) return value;\n const [k, ...rest] = path;\n const here = Array.isArray(root) ? [...root] : { ...root };\n here[k] = setAt(root?.[k], rest, value);\n return here;\n}\n\n/** The root without the value at `path`: { root, removed, index } —\n * `index` is where it stood (a key's position in its object), for undo. */\nexport function removeAt(root, path) {\n if (!path.length) return { root: undefined, removed: root, index: 0 };\n const parentPath = path.slice(0, -1);\n const k = path[path.length - 1];\n const parent = getAt(root, parentPath);\n if (Array.isArray(parent)) {\n const next = parent.filter((_, i) => i !== k);\n return { root: setAt(root, parentPath, next), removed: parent[k], index: k };\n }\n const keys = Object.keys(parent);\n const next = {};\n for (const key of keys) if (key !== k) next[key] = parent[key];\n return { root: setAt(root, parentPath, next), removed: parent[k], index: keys.indexOf(k) };\n}\n\n/** Put a value into the container at `parentPath`, at `index` (a list) or\n * as `key` at position `index` (an object; the end when index is null). */\nexport function insertAt(root, parentPath, { key, index = null, value }) {\n const parent = getAt(root, parentPath);\n if (Array.isArray(parent)) {\n const next = [...parent];\n next.splice(index == null ? next.length : index, 0, value);\n return setAt(root, parentPath, next);\n }\n const entries = Object.entries(parent ?? {}).filter(([k]) => k !== key);\n entries.splice(index == null ? entries.length : Math.min(index, entries.length), 0, [key, value]);\n return setAt(root, parentPath, Object.fromEntries(entries));\n}\n\n/** Rename the key at `path`, keeping its place. { root } or { error }. */\nexport function renameKey(root, path, to) {\n const parentPath = path.slice(0, -1);\n const from = path[path.length - 1];\n const parent = getAt(root, parentPath);\n const name = String(to);\n if (name === from) return { root };\n if (!name.length) return { error: \"A key needs a name\" };\n if (Object.hasOwn(parent, name)) return { error: `There is already a key \"${name}\" here` };\n const next = {};\n for (const [k, v] of Object.entries(parent)) next[k === from ? name : k] = v;\n return { root: setAt(root, parentPath, next) };\n}\n\n/** A key not yet used in `obj`: base, base_2, base_3… */\nexport function uniqueKey(obj, base = \"new_key\") {\n if (!Object.hasOwn(obj ?? {}, base)) return base;\n for (let n = 2; ; n++) if (!Object.hasOwn(obj, `${base}_${n}`)) return `${base}_${n}`;\n}\n\n/** What a typed edit means. Text stays text; for a number, true/false/\n * null or a {…}/[…] the field is read as JSON — unless the value was text\n * and still reads as text-ish. `prevType` is the type being edited. */\nexport function parseEdit(text, prevType) {\n const s = String(text ?? \"\");\n if (prevType === \"string\") return s;\n const t = s.trim();\n if (t === \"true\") return true;\n if (t === \"false\") return false;\n if (t === \"null\") return null;\n if (/^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$/.test(t)) return Number(t);\n if (/^[[{\"]/.test(t)) {\n try {\n return JSON.parse(t);\n } catch {\n /* fall through: keep it as text */\n }\n }\n return s;\n}\n\n/** The text a value is edited as: text bare, everything else as JSON. */\nexport const editText = (v) => (typeof v === \"string\" ? v : JSON.stringify(v));\n\n/** Turn a value into another type, keeping what can be kept. */\nexport function convertType(v, type) {\n const from = typeOf(v);\n if (from === type) return v;\n switch (type) {\n case \"string\":\n return isContainer(v) ? JSON.stringify(v) : v === null ? \"\" : String(v);\n case \"number\": {\n const n = Number(typeof v === \"string\" ? v.trim() : v);\n return Number.isFinite(n) ? n : 0;\n }\n case \"boolean\":\n return typeof v === \"string\" ? v.trim().toLowerCase() === \"true\" : Boolean(isContainer(v) ? Object.keys(v).length : v);\n case \"null\":\n return null;\n case \"array\":\n return from === \"object\" ? Object.values(v) : v === null || v === \"\" ? [] : [v];\n case \"object\":\n return from === \"array\" ? Object.fromEntries(v.map((x, i) => [String(i), x])) : v === null || v === \"\" ? {} : { value: v };\n default:\n return v;\n }\n}\n\n/** The ids of the containers open `depth` levels down: 1 opens the\n * top-level rows, 2 their children too; 0 shows only the top level. */\nexport function expandedTo(value, depth) {\n const out = new Set();\n const walk = (v, path, d) => {\n if (!isContainer(v)) return;\n if (path.length) {\n if (d >= depth) return;\n out.add(pathId(path));\n }\n for (const [k, child] of entries(v)) walk(child, [...path, k], path.length ? d + 1 : 0);\n };\n walk(value, [], 0);\n return out;\n}\n\n/** Every container id (Expand all). */\nexport const allContainers = (value) => expandedTo(value, Infinity);\n\nconst entries = (v) => (Array.isArray(v) ? v.map((x, i) => [i, x]) : Object.entries(v));\n\n/**\n * The rows the tree shows: the root's children (the root itself when it is\n * not a container), each open container's children beneath it, depth\n * first. Each row: { id, path, key, depth, type, value, size, open,\n * parentId, last } — `size` a container's summary, `last` whether it is\n * the last child of its parent (for the guide lines).\n */\nexport function flattenTree(value, open) {\n const rows = [];\n if (!isContainer(value)) {\n if (value !== undefined) rows.push({ id: pathId([]), path: [], key: null, depth: 0, type: typeOf(value), value, size: \"\", open: false, parentId: null, last: true });\n return rows;\n }\n const walk = (v, path, depth) => {\n const list = entries(v);\n list.forEach(([k, child], i) => {\n const p = [...path, k];\n const id = pathId(p);\n const container = isContainer(child);\n const isOpen = container && open.has(id);\n rows.push({ id, path: p, key: k, depth, type: typeOf(child), value: child, size: container ? summary(child) : \"\", open: isOpen, parentId: pathId(path), last: i === list.length - 1 });\n if (isOpen) walk(child, p, depth + 1);\n });\n };\n walk(value, [], 0);\n return rows;\n}\n\n/** A short one-line look at a value, for a folded row. */\nexport function preview(v, max = 60) {\n if (!isContainer(v)) return typeof v === \"string\" ? JSON.stringify(v) : String(v);\n const parts = [];\n let len = 0;\n for (const [k, x] of entries(v)) {\n const bit = (Array.isArray(v) ? \"\" : `${k}: `) + (isContainer(x) ? (Array.isArray(x) ? \"[…]\" : \"{…}\") : typeof x === \"string\" ? JSON.stringify(x) : String(x));\n len += bit.length + 2;\n if (len > max) {\n parts.push(\"…\");\n break;\n }\n parts.push(bit);\n }\n return Array.isArray(v) ? `[${parts.join(\", \")}]` : `{ ${parts.join(\", \")} }`;\n}\n"
36
+ },
37
+ {
38
+ "path": "registry/blocks/json-editor/README.md",
39
+ "type": "registry:file",
40
+ "target": "@components/makersclaw/blocks/json-editor/README.md",
41
+ "content": "# JsonEditor\n\nA JSON editor with two views of one value. **Tree** shows it as folding\nrows: double-click a value to edit it, double-click a key to rename it,\n`+` adds a key or an item, and each row's ⋯ menu copies its path\n(`deal.owner.email`) or value, changes its type, or deletes it — with Undo\nin a toast. **Code** is the text, in the `code-editor` block's JSON mode.\n\nIt is checked as it changes. The status line under the title says\n`Valid JSON · 5 keys`, or what is wrong in plain words and where —\n`Line 4: A comma is missing before this` — and clicking it opens the Code\nview at that spot. While the text does not parse the last valid value\nstands and the Tree view waits. Format lays the text out again (keys,\norder and numbers as written); the ⋯ menu copies it pretty or on one\nline, and expands or collapses the tree. Empty, it offers a new object, a\nnew list, or a place to paste.\n\nBuilt on `code-editor` (which `add` installs with it); it never imports\nCodeMirror itself.\n\n```jsx\nimport { JsonEditor } from \"../components/blocks/json-editor\"; // a Makersclaw app (\"./blocks/json-editor\" from components/)\nimport { JsonEditor } from \"@/components/makersclaw/blocks/json-editor\"; // a shadcn project\n```\n\n`JsonEditor` is a client component: render it from a `\"use client\"`\ncomponent. The tree helpers (`getAt`, `setAt`, `pathLabel`…) are plain\nfunctions and work on the server too.\n\n## The data\n\nA plain JSON value: an object, a list, text, a number, true/false or null\n— what `JSON.parse` gives and a `jsonb` column stores. The editor hands\nback a new value on every valid edit and never changes the one you gave\nit.\n\nTwo things JavaScript does to any JSON value, here too: keys that look\nlike whole numbers (`\"2\"`) come first in an object, and numbers past 2^53\nlose precision. The Code view's text keeps both as typed until it is\nedited from the Tree.\n\n## Props\n\n- `value` — the JSON value, controlled. A new `value` that is not what\n the editor last sent through `onChange` is another record: it replaces\n the text, the folds and the saved state.\n- `defaultValue` — the starting value, uncontrolled. With neither, the\n editor starts empty.\n- `onChange(value)` — every valid edit, with the whole value. A keystroke\n that leaves the text broken sends nothing.\n- `onSave(value) → { ok, error? }` — optional, may be async. Shows a Save\n button (and Cmd/Ctrl+S) that is live when there are valid unsaved\n changes. It is optimistic: the status says Saved at once; `{ ok: false,\n error }` (or a throw) puts \"Unsaved changes\" back and shows `error` in a\n notice with Try again. Without `onSave`, save through `onChange`\n yourself.\n- `view` / `defaultView` — `\"tree\"` (default) or `\"code\"`, controlled or\n not; `onViewChange(view)` hears the toggle.\n- `readOnly` — `false` (default). `true`: the tree only folds and copies,\n the text only selects and copies.\n- `title` — a heading above the status line (\"Webhook payload\").\n- `expandDepth` — how many levels the tree opens with, default `2`\n (`0` shows only the top level).\n- `height` — a fixed height (px number or CSS length) for both views.\n Without it they grow to `maxHeight` (480), then scroll.\n- `placeholder` — the empty Code view's hint.\n- `label` — what a screen reader calls the tree and the editor (default:\n `title`, else \"JSON\").\n- `className` — the frame's classes.\n\n### Keys\n\nIn the tree (one Tab stop): ↑ ↓ move · → open, then into it · ← close,\nthen to the parent · Home / End · Enter or Space opens a folder or edits a\nvalue · F2 renames a key · Delete removes the row (Undo in the toast).\nEditing: Enter keeps it, Esc puts it back. Cmd/Ctrl+S saves (with\n`onSave`); the Code view keeps every `code-editor` key (Shift+Alt+F\nformats).\n\n### Editing a value\n\nText is edited bare (no quotes) and stays text. Anything else is read as\nJSON: `42`, `true`, `null`, `[1, 2]`, `{\"a\": 1}`; what does not read stays\nas text. To turn text into a number (or a list into an object…), use the\nrow's ⋯ → Change type, which keeps what it can.\n\n## The helpers\n\n`typeOf(v)`, `pathLabel(path)` (`[\"deal\", \"tags\", 0]` → `deal.tags[0]`),\n`getAt(root, path)`, `setAt(root, path, v)`, `removeAt(root, path)` →\n`{ root, removed, index }`, `insertAt(root, parentPath, { key?, index?,\nvalue })`, `renameKey(root, path, to)` → `{ root }` or `{ error }`,\n`flattenTree(value, openIds)`, `expandedTo(value, depth)`, `summary(v)`\n(\"3 keys\"), `preview(v)`. Every edit returns a new root. `JsonTree` is the\ntree on its own (`value`, `onEdit(next, { undo?, message? })`, `open`,\n`setOpen`, `readOnly`) for a screen that wants rows without the frame.\n\n## Example\n\n```jsx\n\"use client\";\nimport { useState } from \"react\";\nimport { JsonEditor } from \"../components/blocks/json-editor\";\nimport { saveIntegrationSettings } from \"../lib/actions\"; // your server action → { ok, error? }\n\nexport function WebhookSettings({ integration }) {\n const [settings, setSettings] = useState(integration.settings);\n return (\n <JsonEditor\n title=\"Webhook settings\"\n value={settings}\n onChange={setSettings}\n onSave={(v) => saveIntegrationSettings(integration.id, v)}\n />\n );\n}\n```\n\nInspect a payload, read-only, as text first:\n\n```jsx\n<JsonEditor title=\"Last delivery\" value={delivery.body} readOnly defaultView=\"code\" height={320} />\n```\n\n## What it will not do\n\n- No JSON Schema: it checks that the text is JSON, not that it has the\n keys your app needs. Check that in `onSave` and return `{ ok: false,\n error: \"…\" }`.\n- No drag to reorder rows; order changes in the Code view.\n- It does not diff two values or merge edits from two people.\n- Very large documents (tens of thousands of rows) are better read in the\n Code view; the tree draws every open row.\n"
42
+ }
43
+ ],
44
+ "categories": [
45
+ "json",
46
+ "editor",
47
+ "tree",
48
+ "jsonb",
49
+ "config",
50
+ "settings",
51
+ "payload",
52
+ "webhook",
53
+ "metadata",
54
+ "validate",
55
+ "format",
56
+ "inspector",
57
+ "key value"
58
+ ],
59
+ "docs": "Installed to components/makersclaw/blocks/json-editor. Import: import { JsonEditor, JsonTree, typeOf, pathLabel, getAt, setAt, removeAt, insertAt, renameKey, flattenTree, expandedTo, summary, preview } from \"@/components/makersclaw/blocks/json-editor\". Props and an example: components/makersclaw/blocks/json-editor/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
60
+ "meta": {
61
+ "makersclaw": {
62
+ "version": "0.3.0",
63
+ "useWhen": "A screen edits or inspects a JSON value: an integration's settings, a webhook payload, a record's metadata or custom fields column (jsonb), an API response, feature flags. Not for editing SQL or other code (code-editor), a form of known fields (record-form), or a table of rows (data-table)."
64
+ }
65
+ }
66
+ }
package/r/map.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "map",
4
+ "type": "registry:block",
5
+ "title": "MapView",
6
+ "description": "An interactive map (MapLibre) in the workspace theme: places from rows as markers in token tones or your own kit content, clustered when they crowd, a popup with kit parts, a searchable list beside the map that shares one selection and hover with it, fit-to-all, zoom controls, draggable markers with an optimistic save, and tinted areas for territories. Light and dark tiles follow the workspace scheme (OpenFreeMap by default, no key); `styleUrl` swaps them. Helpers for bounds, clustering and distances run anywhere.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/core.json"
9
+ ],
10
+ "dependencies": [
11
+ "maplibre-gl@6.11.1",
12
+ "react-map-gl@8.1.3"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "registry/blocks/map/index.js",
17
+ "type": "registry:component",
18
+ "target": "@components/makersclaw/blocks/map/index.js",
19
+ "content": "// The Map block — `makersclaw add map`. Fixed: compose it through its\n// props (README.md beside this file). The helpers (geo.js) are plain\n// functions and work on the server; the blocks built on this one\n// (location-map, store-locator, the Map view of work-views) import it,\n// never maplibre-gl or react-map-gl directly.\nexport { MapView, useMapView } from \"./map-view\";\nexport { MAP_STYLES, DEFAULT_STYLE, TONES } from \"./theme\";\nexport { readPosition, boundsOf, inBounds, clusterPoints, zoomToReveal, distanceKm, formatDistance, sortByDistance } from \"./geo\";\n"
20
+ },
21
+ {
22
+ "path": "registry/blocks/map/map-view.jsx",
23
+ "type": "registry:component",
24
+ "target": "@components/makersclaw/blocks/map/map-view.jsx",
25
+ "content": "\"use client\";\n\n// MapView — places on a map, with the list beside it, behind one set of\n// props (README.md beside this file). MapLibre draws the tiles; every mark\n// on them (markers, clusters, the popup) and every control is ours, in the\n// kit and the workspace's tokens.\n//\n// Client only, one static bundle: maplibre loads with the page (see\n// maplibre.js), and the map itself mounts after the first effect (the\n// mount guard). Until then — on the server and in the first client paint —\n// the frame, the list and a quiet placeholder are already there, so the\n// page does not jump and the places can be read before the map arrives.\n//\n// One selection, one hover, shared by the map, the list, the popup and the\n// phone's card strip. Picking in the list moves the camera to the place\n// (zooming until it stands clear of its cluster); picking on the map\n// scrolls the list to it. Clustering is ours (geo.js clusterPoints, in\n// screen pixels at the current zoom), so a cluster is kit-drawn HTML like\n// every other mark.\nimport { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from \"react\";\nimport { Button, ButtonGroup, Empty, Icon, IconButton, Notice, cx, toast } from \"../../ui\";\nimport { boundsOf, clusterPoints, idsKey, inBounds, isPointBounds, matches, readPosition, zoomToReveal } from \"./geo\";\nimport { ListPanel, Strip } from \"./list\";\nimport { AttributionControl, GLMap, Layer, Marker, Popup, Source, WORKER_URL, maplibregl } from \"./maplibre\";\nimport { ClusterMark, DefaultPopup, PointMark, PopupCard, StackList } from \"./marks\";\nimport { CHROME, TONES, prefersReducedMotion, resolveTones, schemeOf, styleFor, toneOrDefault } from \"./theme\";\n\n/** A ref for the map's handle: `const map = useMapView()`, then\n * `<MapView mapRef={map} />` and `map.current?.fitAll()`. */\nexport function useMapView() {\n return useRef(null);\n}\n\nconst WORLD = { lat: 30, lng: 0, zoom: 1.2 };\nconst NO_ITEMS = [];\nconst step = (zoom) => Math.round(zoom * 2) / 2;\nconst text = (v) => (typeof v === \"string\" || typeof v === \"number\" ? String(v) : \"\");\nconst defaultTitle = (item) => item?.title ?? item?.name ?? item?.label ?? item?.location?.label ?? \"Untitled\";\nconst defaultId = (item) => item?.id;\n\nexport function MapView(props) {\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n return <Frame {...props} mounted={mounted} />;\n}\n\nfunction Frame({\n mounted,\n items = NO_ITEMS,\n positionOf = readPosition,\n idOf = defaultId,\n titleOf = defaultTitle,\n metaOf,\n asideOf,\n toneOf,\n searchOf,\n renderMarker,\n renderPopup,\n selectedId,\n defaultSelectedId = null,\n onSelect,\n onMove,\n onMapClick,\n onViewChange,\n areas,\n onAreaClick,\n list = \"side\",\n search = true,\n cluster = true,\n clusterRadius = 48,\n popup = true,\n styleUrl,\n scheme: schemeProp = \"auto\",\n workerUrl = WORKER_URL,\n initialView,\n fitMaxZoom = 14,\n maxZoom = 18,\n fitOnChange = true,\n scrollZoom = true,\n controls = true,\n height = 520,\n noun = \"place\",\n nounPlural,\n emptyTitle,\n emptyBody,\n emptyAction,\n label = \"Map\",\n mapRef,\n className,\n}) {\n const plural = nounPlural ?? `${noun}s`;\n const rootRef = useRef(null);\n const glRef = useRef(null);\n const listRef = useRef(null);\n const stripRef = useRef(null);\n const hasList = list !== \"none\";\n\n const [query, setQuery] = useState(\"\");\n const [inner, setInner] = useState(defaultSelectedId);\n const selected = selectedId !== undefined ? selectedId : inner;\n const [hovered, setHovered] = useState(null);\n const [overrides, setOverrides] = useState({});\n const [zoomStep, setZoomStep] = useState(null);\n const [loaded, setLoaded] = useState(false);\n const [failure, setFailure] = useState(null);\n const [attempt, setAttempt] = useState(0);\n const [scheme, setScheme] = useState(schemeProp === \"dark\" ? \"dark\" : \"light\");\n const [tones, setTones] = useState({});\n const [phoneView, setPhoneView] = useState(\"map\");\n const [stack, setStack] = useState(null);\n const dragged = useRef(false);\n\n // ---- the rows -------------------------------------------------------------\n const rows = useMemo(\n () =>\n items.map((item) => {\n const id = String(idOf(item));\n const own = positionOf(item);\n const o = overrides[id];\n // An optimistic move holds until the row itself changes position.\n const pos = o && samePos(o.base, own) ? o.pos : own;\n const meta = metaOf?.(item) ?? null;\n const title = text(titleOf(item)) || \"Untitled\";\n return {\n id,\n item,\n pos,\n title,\n meta,\n aside: asideOf?.(item) ?? null,\n tone: toneOrDefault(toneOf?.(item)),\n words: searchOf ? text(searchOf(item)) : `${title} ${text(meta)}`,\n };\n }),\n [items, idOf, positionOf, titleOf, metaOf, asideOf, toneOf, searchOf, overrides],\n );\n const byId = useMemo(() => new Map(rows.map((r) => [r.id, r])), [rows]);\n const visible = useMemo(() => (query.trim() ? rows.filter((r) => matches(r.words, query)) : rows), [rows, query]);\n const located = useMemo(() => visible.filter((r) => r.pos), [visible]);\n const unlocated = useMemo(() => visible.filter((r) => !r.pos), [visible]);\n const points = useMemo(() => located.map((r) => ({ id: r.id, lat: r.pos.lat, lng: r.pos.lng })), [located]);\n const groups = useMemo(() => {\n if (zoomStep == null) return [];\n return cluster ? clusterPoints(points, zoomStep, clusterRadius) : points.map((p) => ({ type: \"point\", ...p }));\n }, [points, zoomStep, cluster, clusterRadius]);\n const locatedKey = useMemo(() => idsKey(points.map((p) => `${p.id}@${p.lat},${p.lng}`)), [points]);\n\n // ---- the camera -----------------------------------------------------------\n const map = () => glRef.current?.getMap?.() ?? null;\n const duration = (ms) => (prefersReducedMotion() ? 0 : ms);\n const padding = () => {\n const w = rootRef.current?.clientWidth ?? 800;\n return w < 480 ? 40 : 64;\n };\n\n const fitAll = useCallback(\n ({ animate = true } = {}) => {\n const m = map();\n if (!m) return;\n const b = boundsOf(points);\n const ms = animate ? duration(700) : 0;\n if (!b) {\n const v = initialView ?? WORLD;\n m.easeTo({ center: [v.lng, v.lat], zoom: v.zoom ?? WORLD.zoom, duration: ms });\n } else if (isPointBounds(b)) {\n m.easeTo({ center: [b.west, b.south], zoom: Math.max(m.getZoom(), Math.min(fitMaxZoom, 13)), duration: ms });\n } else {\n m.fitBounds(\n [\n [b.west, b.south],\n [b.east, b.north],\n ],\n { padding: padding(), maxZoom: fitMaxZoom, duration: ms },\n );\n }\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [points, initialView, fitMaxZoom],\n );\n\n /** Bring a place into view, clear of any cluster that hides it. */\n const reveal = useCallback(\n (id) => {\n const m = map();\n const r = byId.get(String(id));\n if (!m || !r?.pos) return;\n const z = m.getZoom();\n const need = cluster ? zoomToReveal(points, r.id, z, { radius: clusterRadius, maxZoom: Math.min(maxZoom, fitMaxZoom + 2) }) : z;\n const zoom = Math.max(z, need ?? z);\n const b = m.getBounds();\n const view = { west: b.getWest(), south: b.getSouth(), east: b.getEast(), north: b.getNorth() };\n // Already on screen, away from the edges, and not hidden in a cluster: leave the camera be.\n const inset = { west: view.west + (view.east - view.west) * 0.12, east: view.east - (view.east - view.west) * 0.12, south: view.south + (view.north - view.south) * 0.12, north: view.north - (view.north - view.south) * 0.25 };\n if (zoom === z && inBounds(inset, r.pos)) return;\n m.easeTo({ center: [r.pos.lng, r.pos.lat], zoom, duration: duration(600) });\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [byId, points, cluster, clusterRadius, maxZoom, fitMaxZoom],\n );\n\n // ---- selection ------------------------------------------------------------\n const pending = useRef({ id: undefined, reveal: false });\n const select = useCallback(\n (id, { reveal: move = false } = {}) => {\n const next = id == null ? null : String(id);\n pending.current = { id: next, reveal: move };\n setStack(null);\n if (selectedId === undefined) setInner(next);\n onSelect?.(next, next == null ? null : byId.get(next)?.item ?? null);\n },\n [selectedId, onSelect, byId],\n );\n\n // A selection that changed — from the list, the handle or the parent —\n // moves the camera; one made on the map itself does not.\n const seen = useRef(selected);\n useEffect(() => {\n if (seen.current === selected) return;\n seen.current = selected;\n const own = pending.current.id === (selected == null ? null : String(selected));\n const move = own ? pending.current.reveal : true;\n pending.current = { id: undefined, reveal: false };\n if (selected == null) return;\n if (move && loaded) reveal(selected);\n // Keep the chosen row and card in view.\n const q = `[data-map-id=\"${cssEscape(String(selected))}\"]`;\n listRef.current?.querySelector(q)?.scrollIntoView({ block: \"nearest\", behavior: prefersReducedMotion() ? \"auto\" : \"smooth\" });\n const card = stripRef.current?.querySelector(q);\n if (card && stripRef.current) {\n const s = stripRef.current;\n s.scrollTo({ left: card.offsetLeft - (s.clientWidth - card.clientWidth) / 2, behavior: prefersReducedMotion() ? \"auto\" : \"smooth\" });\n }\n }, [selected, loaded, reveal]);\n\n // Refit when the set of places on the map changes (new rows, a search).\n const fittedKey = useRef(null);\n useEffect(() => {\n if (!loaded) return;\n if (fittedKey.current === null) {\n fittedKey.current = locatedKey;\n return;\n }\n if (fittedKey.current === locatedKey) return;\n fittedKey.current = locatedKey;\n if (fitOnChange && points.length) fitAll();\n }, [locatedKey, loaded, fitOnChange, fitAll, points.length]);\n\n // ---- the scheme, and the tones areas are painted in ------------------------\n // Read on the first effect — the same flush that mounts the map — so the\n // map's first style is already the right one (no light flash in dark).\n useEffect(() => {\n const root = rootRef.current;\n const read = () => {\n const s = schemeProp === \"light\" || schemeProp === \"dark\" ? schemeProp : schemeOf(root);\n setScheme(s);\n if (areas?.length) setTones(resolveTones(root, TONES));\n };\n read();\n const mo = new MutationObserver(read);\n mo.observe(document.documentElement, { attributes: true, attributeFilter: [\"data-theme\", \"class\", \"style\"] });\n const scoped = root?.parentElement?.closest(\"[data-theme]\");\n if (scoped && scoped !== document.documentElement) mo.observe(scoped, { attributes: true, attributeFilter: [\"data-theme\", \"class\", \"style\"] });\n const mq = window.matchMedia?.(\"(prefers-color-scheme: dark)\");\n mq?.addEventListener?.(\"change\", read);\n return () => {\n mo.disconnect();\n mq?.removeEventListener?.(\"change\", read);\n };\n }, [schemeProp, areas?.length]);\n\n const mapStyle = styleFor(styleUrl, scheme);\n\n // ---- areas (territories, zones) -------------------------------------------\n const areaData = useMemo(() => {\n if (!areas?.length) return null;\n return {\n type: \"FeatureCollection\",\n features: areas\n .filter((a) => a?.geometry)\n .map((a, i) => {\n const tone = toneOrDefault(a.tone);\n const colour = tones[tone] ?? tones.primary ?? null;\n return { type: \"Feature\", id: i, geometry: a.geometry, properties: { id: String(a.id ?? i), label: text(a.label), paint: colour } };\n })\n .filter((f) => f.properties.paint),\n };\n }, [areas, tones]);\n\n // ---- the handle -----------------------------------------------------------\n const api = useRef({});\n api.current = {\n fitAll: (o) => fitAll(o),\n select: (id, { reveal: move = true } = {}) => select(id, { reveal: move }),\n flyTo: ({ lat, lng, zoom }) => map()?.easeTo({ center: [lng, lat], zoom: zoom ?? map()?.getZoom(), duration: duration(700) }),\n fitBounds: (b, { maxZoom: mz = fitMaxZoom } = {}) =>\n b &&\n map()?.fitBounds(\n [\n [b.west, b.south],\n [b.east, b.north],\n ],\n { padding: padding(), maxZoom: mz, duration: duration(700) },\n ),\n zoomIn: () => map()?.zoomIn({ duration: duration(250) }),\n zoomOut: () => map()?.zoomOut({ duration: duration(250) }),\n getView: () => viewOf(map()),\n getMap: () => map(),\n };\n useImperativeHandle(\n mapRef,\n () => ({\n fitAll: (o) => api.current.fitAll(o),\n select: (id, o) => api.current.select(id, o),\n flyTo: (p) => api.current.flyTo(p),\n fitBounds: (b, o) => api.current.fitBounds(b, o),\n zoomIn: () => api.current.zoomIn(),\n zoomOut: () => api.current.zoomOut(),\n getView: () => api.current.getView(),\n getMap: () => api.current.getMap(),\n }),\n [],\n );\n\n // ---- a moved marker: optimistic, rolled back when the save fails ---------\n async function moveTo(r, pos) {\n const base = positionOf(r.item);\n setOverrides((o) => ({ ...o, [r.id]: { pos, base } }));\n let res;\n try {\n res = await onMove(r.item, pos);\n } catch (e) {\n res = { ok: false, error: e?.message };\n }\n if (res?.ok) return;\n setOverrides((o) => {\n const next = { ...o };\n delete next[r.id];\n return next;\n });\n toast.error(`${r.title} wasn’t moved`, { description: res?.error || \"The new spot didn’t save, so it’s back where it was. Try again.\" });\n }\n\n // The map is made only once its box has a size. Made while hidden (a\n // container query shows it only when the frame is wide enough), it fitted\n // its first view to a 0×0 box, the camera came out invalid and no tile\n // was ever asked for: \"Loading the map…\" for good (store-locator and\n // location-map, seen in the pane 2026-09-24).\n const stageRef = useRef(null);\n const [sized, setSized] = useState(false);\n useEffect(() => {\n const el = stageRef.current;\n if (!el || sized) return;\n const has = () => el.clientWidth > 0 && el.clientHeight > 0;\n if (has()) return setSized(true);\n if (typeof ResizeObserver === \"undefined\") return setSized(true);\n const ro = new ResizeObserver(() => has() && setSized(true));\n ro.observe(el);\n return () => ro.disconnect();\n }, [mounted, sized]);\n\n // ---- the first view -------------------------------------------------------\n const firstView = useMemo(() => {\n const b = boundsOf(points);\n if (b && !isPointBounds(b)) {\n return {\n bounds: [\n [b.west, b.south],\n [b.east, b.north],\n ],\n fitBoundsOptions: { padding: 64, maxZoom: fitMaxZoom },\n };\n }\n if (b) return { longitude: b.west, latitude: b.south, zoom: Math.min(fitMaxZoom, 13) };\n const v = initialView ?? WORLD;\n return { longitude: v.lng, latitude: v.lat, zoom: v.zoom ?? WORLD.zoom };\n // Only the first render's matters: react-map-gl reads it once.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [attempt]);\n\n // `load` can be missed: in development React mounts twice, and the event\n // fired for a map the block had already let go of, so the cover stayed on\n // over a map that was ready (\"Loading the map…\" for good, seen in the pane\n // 2026-09-24). The first `idle` counts as loaded too.\n const loadedRef = useRef(false);\n loadedRef.current = loaded;\n // And a map already loaded and idle by the time we listen fires neither\n // again: ask it directly for a moment after it mounts.\n useEffect(() => {\n if (!mounted || !sized || loaded) return;\n let tries = 0;\n const t = setInterval(() => {\n const m = glRef.current?.getMap?.();\n if (m?.loaded?.() && !loadedRef.current) onLoad({ target: m });\n if (loadedRef.current || ++tries > 40) clearInterval(t);\n }, 250);\n return () => clearInterval(t);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [mounted, sized, loaded, attempt]);\n\n function onLoad(e) {\n const m = e.target;\n loadedRef.current = true;\n m.touchZoomRotate?.disableRotation?.();\n setZoomStep(step(m.getZoom()));\n setLoaded(true);\n setFailure(null);\n if (selected != null) reveal(selected);\n onViewChange?.(viewOf(m));\n }\n\n function onError(e) {\n // A tile or an icon that failed is maplibre's to retry; only a map that\n // never came up (the style unreachable, no WebGL) is worth a notice.\n if (loaded || e?.tile || e?.sourceId || e?.source) return;\n const msg = String(e?.error?.message ?? e?.message ?? \"\");\n setFailure(/webgl/i.test(msg) ? \"webgl\" : \"network\");\n }\n\n function retry() {\n setFailure(null);\n setLoaded(false);\n setZoomStep(null);\n fittedKey.current = null;\n setAttempt((a) => a + 1);\n }\n\n const selectedRow = selected == null ? null : byId.get(String(selected)) ?? null;\n const popupRow = popup && selectedRow?.pos && located.includes(selectedRow) ? selectedRow : null;\n const stackRows = stack ? stack.ids.map((id) => byId.get(id)).filter(Boolean) : [];\n const hoveredId = hovered == null ? null : String(hovered);\n\n const empty = (\n <Empty\n icon=\"map\"\n title={emptyTitle ?? `No ${plural} yet`}\n body={emptyBody ?? `${cap(plural)} with an address or coordinates show up here and on the map.`}\n action={emptyAction}\n className=\"mx-1 mt-2 border-0\"\n />\n );\n\n return (\n <div\n ref={rootRef}\n role=\"region\"\n aria-label={label}\n onKeyDown={(e) => {\n if (e.key !== \"Escape\" || e.defaultPrevented) return;\n if (stack) setStack(null);\n else if (selected != null) select(null);\n }}\n style={{ height }}\n className={cx(\"@container relative flex overflow-hidden rounded-lg border border-border bg-card text-card-foreground\", CHROME, className)}\n >\n <div className=\"flex min-h-0 min-w-0 flex-1 flex-col @3xl:flex-row\">\n {hasList ? (\n <ListPanel\n ref={listRef}\n rows={located.map((r) => (r.id === hoveredId ? { ...r, hovered: true } : r))}\n unlocated={unlocated}\n total={rows.length}\n query={query}\n onQuery={setQuery}\n search={search}\n noun={noun}\n plural={plural}\n selected={selected == null ? null : String(selected)}\n onPick={(id) => {\n setPhoneView(\"map\");\n select(id, { reveal: true });\n }}\n onHover={setHovered}\n onShowMap={() => setPhoneView(\"map\")}\n empty={empty}\n className={cx(\"flex-1 @3xl:flex @3xl:w-80 @3xl:flex-none @3xl:border-r @3xl:border-border\", phoneView === \"list\" ? \"flex\" : \"hidden\")}\n />\n ) : null}\n\n <div ref={stageRef} className={cx(\"relative min-h-0 min-w-0 flex-1\", hasList && phoneView === \"list\" && \"hidden @3xl:block\")}>\n {mounted && sized ? (\n <GLMap\n key={attempt}\n ref={glRef}\n mapLib={maplibregl}\n workerUrl={workerUrl}\n initialViewState={firstView}\n mapStyle={mapStyle}\n maxZoom={maxZoom}\n dragRotate={false}\n pitchWithRotate={false}\n touchPitch={false}\n scrollZoom={scrollZoom}\n attributionControl={false}\n interactiveLayerIds={areaData && onAreaClick ? [\"mc-areas-fill\"] : undefined}\n onLoad={onLoad}\n onIdle={(e) => {\n if (!loadedRef.current) onLoad(e);\n }}\n onError={onError}\n onMove={(e) => {\n const s = step(e.viewState.zoom);\n if (s !== zoomStep) setZoomStep(s);\n }}\n onMoveEnd={(e) => onViewChange?.(viewOf(e.target))}\n onClick={(e) => {\n const f = e.features?.[0];\n if (f && onAreaClick) {\n const a = areas.find((x, i) => String(x.id ?? i) === f.properties?.id);\n if (a) return onAreaClick(a);\n }\n if (stack) setStack(null);\n else if (selected != null) select(null);\n onMapClick?.({ lat: e.lngLat.lat, lng: e.lngLat.lng });\n }}\n style={{ width: \"100%\", height: \"100%\" }}\n >\n <AttributionControl compact={false} position=\"bottom-right\" />\n {areaData ? (\n <Source id=\"mc-areas\" type=\"geojson\" data={areaData}>\n <Layer id=\"mc-areas-fill\" type=\"fill\" filter={[\"in\", [\"geometry-type\"], [\"literal\", [\"Polygon\", \"MultiPolygon\"]]]} paint={{ \"fill-color\": [\"get\", \"paint\"], \"fill-opacity\": 0.14 }} />\n <Layer id=\"mc-areas-line\" type=\"line\" paint={{ \"line-color\": [\"get\", \"paint\"], \"line-width\": 1.5, \"line-opacity\": 0.8 }} />\n </Source>\n ) : null}\n\n {groups.map((g) => {\n if (g.type === \"cluster\") {\n const holds = selected != null && g.ids.includes(String(selected));\n return (\n <Marker\n key={g.id}\n longitude={g.lng}\n latitude={g.lat}\n anchor=\"center\"\n onClick={(e) => {\n // A native listener on the marker, so it runs before\n // maplibre's own map click (which would clear the selection).\n e.originalEvent.stopPropagation();\n const m = map();\n if (!m) return;\n if (isPointBounds(g.bounds) || m.getZoom() >= maxZoom - 0.5) {\n setStack(g);\n return;\n }\n m.fitBounds(\n [\n [g.bounds.west, g.bounds.south],\n [g.bounds.east, g.bounds.north],\n ],\n { padding: 96, maxZoom, duration: duration(500) },\n );\n }}\n >\n <ClusterMark count={g.count} holdsSelected={holds} label={`${g.count} ${plural} here. Zoom in`} />\n </Marker>\n );\n }\n const r = byId.get(g.id);\n if (!r) return null;\n const isSel = selected != null && String(selected) === r.id;\n const isHover = hoveredId === r.id;\n return (\n <Marker\n key={r.id}\n longitude={g.lng}\n latitude={g.lat}\n anchor=\"center\"\n draggable={Boolean(onMove)}\n onDragStart={() => {\n dragged.current = true;\n }}\n onDragEnd={(e) => {\n setTimeout(() => {\n dragged.current = false;\n }, 0);\n moveTo(r, { lat: e.lngLat.lat, lng: e.lngLat.lng });\n }}\n onClick={(e) => {\n // Native, on the marker: runs before maplibre's map click.\n // A keyboard Enter on the marker's button arrives here too.\n e.originalEvent.stopPropagation();\n if (dragged.current) return;\n select(isSel ? null : r.id);\n }}\n >\n <PointMark\n title={r.title}\n tone={r.tone}\n selected={isSel}\n hovered={isHover}\n showLabel={isHover && !(isSel && popupRow)}\n focusable={!hasList}\n custom={renderMarker ? renderMarker(r.item, { selected: isSel, hovered: isHover, tone: r.tone }) : null}\n onHover={(on) => setHovered(on ? r.id : (h) => (h === r.id ? null : h))}\n />\n </Marker>\n );\n })}\n\n {popupRow ? (\n <Popup\n key={`pop:${popupRow.id}`}\n longitude={popupRow.pos.lng}\n latitude={popupRow.pos.lat}\n anchor=\"bottom\"\n offset={20}\n closeButton={false}\n closeOnClick={false}\n focusAfterOpen={false}\n maxWidth=\"none\"\n >\n <PopupCard onClose={() => select(null)}>\n {renderPopup ? (\n renderPopup(popupRow.item, { close: () => select(null) })\n ) : (\n <DefaultPopup title={popupRow.title} meta={popupRow.meta} aside={popupRow.aside} />\n )}\n </PopupCard>\n </Popup>\n ) : null}\n\n {stack && stackRows.length ? (\n <Popup key={`stack:${stack.id}`} longitude={stack.lng} latitude={stack.lat} anchor=\"bottom\" offset={28} closeButton={false} closeOnClick={false} focusAfterOpen={false} maxWidth=\"none\">\n <PopupCard onClose={() => setStack(null)}>\n <StackList rows={stackRows} plural={plural} onPick={(id) => select(id)} />\n </PopupCard>\n </Popup>\n ) : null}\n </GLMap>\n ) : null}\n\n {!loaded && !failure ? (\n <div className=\"pointer-events-none absolute inset-0 grid place-items-center bg-muted\">\n <div className=\"absolute inset-0 animate-pulse bg-accent/40 motion-reduce:animate-none\" aria-hidden=\"true\" />\n <p className=\"relative flex items-center gap-2 text-sm text-muted-foreground\">\n <Icon name=\"map\" />\n Loading the map…\n </p>\n {!hasList && rows.length ? (\n <ul className=\"sr-only\">\n {rows.map((r) => (\n <li key={r.id}>{r.title}</li>\n ))}\n </ul>\n ) : null}\n </div>\n ) : null}\n\n {failure ? (\n <div className=\"absolute inset-0 z-[4] grid place-items-center bg-muted p-4\">\n <Notice\n tone=\"warning\"\n title={failure === \"webgl\" ? \"This browser can’t draw maps\" : \"The map didn’t load\"}\n action={\n failure === \"webgl\" ? null : (\n <Button variant=\"secondary\" size=\"sm\" onClick={retry}>\n Try again\n </Button>\n )\n }\n className=\"max-w-sm\"\n >\n {failure === \"webgl\" ? \"WebGL is turned off or not supported here.\" : \"The map tiles couldn’t be reached. Check the connection, then try again.\"}\n {hasList ? ` The ${plural} are still listed.` : \"\"}\n </Notice>\n </div>\n ) : null}\n\n {controls && loaded ? (\n <div className=\"absolute right-3 top-3 z-[3] flex flex-col items-end gap-2\">\n <ButtonGroup orientation=\"vertical\" size=\"sm\" aria-label=\"Zoom\" className=\"rounded-md shadow-sm\">\n <IconButton icon=\"plus\" label=\"Zoom in\" size=\"sm\" variant=\"secondary\" onClick={() => api.current.zoomIn()} />\n <IconButton icon=\"minus\" label=\"Zoom out\" size=\"sm\" variant=\"secondary\" onClick={() => api.current.zoomOut()} />\n </ButtonGroup>\n <IconButton icon=\"maximize\" label={`Show all ${plural}`} size=\"sm\" variant=\"secondary\" className=\"shadow-sm\" disabled={!points.length} onClick={() => fitAll()} />\n </div>\n ) : null}\n\n {hasList ? (\n <div className=\"absolute left-3 top-3 z-[3] @3xl:hidden\">\n <Button variant=\"secondary\" size=\"sm\" icon=\"list\" className=\"shadow-sm\" onClick={() => setPhoneView(\"list\")}>\n List\n <span className=\"text-muted-foreground\" data-nums>\n {visible.length}\n </span>\n </Button>\n </div>\n ) : null}\n </div>\n\n {hasList && located.length ? (\n <Strip ref={stripRef} rows={located} selected={selected == null ? null : String(selected)} onPick={(id) => select(id, { reveal: true })} className={cx(\"@3xl:hidden\", phoneView === \"list\" && \"hidden\")} />\n ) : null}\n </div>\n </div>\n );\n}\n\nfunction samePos(a, b) {\n if (!a && !b) return true;\n if (!a || !b) return false;\n return Math.abs(a.lat - b.lat) < 1e-9 && Math.abs(a.lng - b.lng) < 1e-9;\n}\n\nfunction viewOf(m) {\n if (!m) return null;\n const c = m.getCenter();\n const b = m.getBounds();\n return { center: { lat: c.lat, lng: c.lng }, zoom: m.getZoom(), bounds: { west: b.getWest(), south: b.getSouth(), east: b.getEast(), north: b.getNorth() } };\n}\n\nconst cap = (s) => (s ? s[0].toUpperCase() + s.slice(1) : s);\n\nfunction cssEscape(s) {\n return typeof CSS !== \"undefined\" && CSS.escape ? CSS.escape(s) : s.replace(/[\"\\\\]/g, \"\\\\$&\");\n}\n"
26
+ },
27
+ {
28
+ "path": "registry/blocks/map/list.jsx",
29
+ "type": "registry:component",
30
+ "target": "@components/makersclaw/blocks/map/list.jsx",
31
+ "content": "\"use client\";\n\n// The places beside the map: a searchable list (a column on the left\n// when the block is wide, a whole view behind a \"List\" button when it is\n// narrow) and, on a phone, a strip of cards under the map. Kit Items; the\n// map and the list share one selection and one hover.\nimport { forwardRef } from \"react\";\nimport { Button, Empty, InputGroup, Item, cx } from \"../../ui\";\nimport { Dot } from \"./marks\";\n\nexport const ListPanel = forwardRef(function ListPanel(\n { rows, unlocated, total, query, onQuery, search, plural, noun, selected, onPick, onHover, onShowMap, empty, className },\n ref,\n) {\n const shown = rows.length + unlocated.length;\n const count = query ? `${shown} of ${total} ${total === 1 ? noun : plural}` : `${total} ${total === 1 ? noun : plural}`;\n return (\n <aside className={cx(\"min-h-0 flex-col bg-card\", className)} aria-label={`${plural[0].toUpperCase()}${plural.slice(1)}`}>\n <div className=\"flex items-center gap-2 px-3 pt-3\">\n {search ? (\n <InputGroup\n icon=\"search\"\n type=\"text\"\n enterKeyHint=\"search\"\n value={query}\n onChange={(e) => onQuery(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Escape\" && query) {\n e.stopPropagation();\n onQuery(\"\");\n }\n }}\n placeholder={`Search ${plural}`}\n aria-label={`Search ${plural}`}\n action={query ? { label: \"Clear search\", icon: \"x\", onClick: () => onQuery(\"\") } : undefined}\n />\n ) : (\n <div className=\"flex-1\" />\n )}\n <Button variant=\"secondary\" size=\"sm\" icon=\"map\" onClick={onShowMap} className=\"@3xl:hidden\">\n Map\n </Button>\n </div>\n <p className=\"px-4 pb-1.5 pt-2 text-xs text-muted-foreground\" aria-live=\"polite\" data-nums>\n {total ? count : \"\"}\n </p>\n <div ref={ref} className=\"min-h-0 flex-1 overflow-y-auto overscroll-contain px-2 pb-2\">\n {total === 0 ? (\n empty\n ) : shown === 0 ? (\n <Empty\n icon=\"search\"\n title={`No ${plural} match “${query.trim()}”`}\n body=\"Check the spelling, or try another word.\"\n action={\n <Button variant=\"secondary\" size=\"sm\" onClick={() => onQuery(\"\")}>\n Clear search\n </Button>\n }\n className=\"mx-1 mt-2 border-0\"\n />\n ) : (\n <>\n {rows.map((r) => (\n <Row key={r.id} r={r} selected={selected === r.id} onPick={onPick} onHover={onHover} />\n ))}\n {unlocated.length ? (\n <>\n <p className=\"px-2 pb-1 pt-4 text-xs font-medium text-muted-foreground\" data-nums>\n Not on the map · {unlocated.length}\n </p>\n {unlocated.map((r) => (\n <Row key={r.id} r={r} selected={selected === r.id} onPick={onPick} onHover={onHover} missing />\n ))}\n </>\n ) : null}\n </>\n )}\n </div>\n </aside>\n );\n});\n\nfunction Row({ r, selected, onPick, onHover, missing }) {\n return (\n <Item\n data-map-id={r.id}\n title={r.title}\n description={r.meta ?? (missing ? \"No location yet\" : undefined)}\n meta={r.aside ?? undefined}\n media={<Dot tone={r.tone} missing={missing} />}\n onClick={() => onPick(r.id)}\n aria-current={selected ? \"true\" : undefined}\n onMouseEnter={() => onHover(r.id)}\n onMouseLeave={() => onHover(null)}\n className={cx(\"rounded-md transition-colors duration-150\", r.hovered && !selected && \"bg-accent/60\")}\n />\n );\n}\n\n/** A phone's strip of cards under the map: tap one to select it; the\n * selected card scrolls into view when the map picks another. */\nexport const Strip = forwardRef(function Strip({ rows, selected, onPick, className }, ref) {\n return (\n <div className={cx(\"border-t border-border bg-card\", className)}>\n <div\n ref={ref}\n className=\"flex snap-x snap-mandatory gap-2 overflow-x-auto overscroll-x-contain px-3 py-3 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n >\n {rows.map((r) => (\n <div key={r.id} data-map-id={r.id} className=\"w-[80%] max-w-72 shrink-0 snap-center\">\n <Item\n variant=\"outline\"\n size=\"sm\"\n title={r.title}\n description={r.meta}\n meta={r.aside ?? undefined}\n media={<Dot tone={r.tone} />}\n onClick={() => onPick(r.id)}\n aria-current={selected === r.id ? \"true\" : undefined}\n className={cx(\"h-full rounded-lg transition-colors duration-150\", selected === r.id && \"ring-1 ring-inset ring-primary\")}\n />\n </div>\n ))}\n </div>\n </div>\n );\n});\n"
32
+ },
33
+ {
34
+ "path": "registry/blocks/map/marks.jsx",
35
+ "type": "registry:component",
36
+ "target": "@components/makersclaw/blocks/map/marks.jsx",
37
+ "content": "\"use client\";\n\n// What sits ON the map: a place's marker, a cluster, the popup card, the\n// list of places that share one spot. React in kit tokens — maplibre only\n// positions them (react-map-gl's Marker and Popup portal them in).\nimport { IconButton, Item, cx } from \"../../ui\";\nimport { DOT } from \"./theme\";\n\n// A soft halo behind the selected dot, one class per tone.\nconst HALO = {\n primary: \"bg-primary/20\",\n success: \"bg-success/20\",\n warning: \"bg-warning/20\",\n destructive: \"bg-destructive/20\",\n info: \"bg-info/20\",\n \"chart-1\": \"bg-chart-1/20\",\n \"chart-2\": \"bg-chart-2/20\",\n \"chart-3\": \"bg-chart-3/20\",\n \"chart-4\": \"bg-chart-4/20\",\n \"chart-5\": \"bg-chart-5/20\",\n muted: \"bg-muted-foreground/20\",\n};\n\n/** A place: a dot in its tone (or the caller's own content), its name in\n * a small label on hover and when it is selected without a popup. */\nexport function PointMark({ title, tone, selected, hovered, showLabel, focusable, custom, onHover }) {\n // Its click is handled by the Marker around it (a native listener that\n // runs before maplibre's map click); this button gives it a name, focus\n // and Enter / Space.\n const lifted = selected || hovered;\n return (\n // raw: a map marker is a hit target positioned on the canvas by\n // maplibre; no kit part is a pin. Kit Button would force 44px boxes\n // that hide the map under them.\n <button\n type=\"button\"\n tabIndex={focusable ? 0 : -1}\n aria-label={title}\n aria-pressed={selected}\n data-lifted={lifted ? \"\" : undefined}\n onMouseEnter={() => onHover(true)}\n onMouseLeave={() => onHover(false)}\n onFocus={() => onHover(true)}\n onBlur={() => onHover(false)}\n className={cx(\n \"group relative grid cursor-pointer place-items-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n custom ? \"min-h-7 min-w-7\" : \"size-7\",\n )}\n >\n {custom ?? (\n <>\n <span\n aria-hidden=\"true\"\n className={cx(\n \"absolute inset-0 rounded-full transition-[transform,opacity] duration-150 motion-reduce:transition-none\",\n HALO[tone],\n selected ? \"scale-100 opacity-100\" : \"scale-50 opacity-0\",\n )}\n />\n <span\n aria-hidden=\"true\"\n className={cx(\n \"relative block size-3.5 rounded-full shadow-sm ring-2 ring-background transition-transform duration-150 motion-reduce:transition-none\",\n DOT[tone],\n selected ? \"scale-125\" : \"group-hover:scale-125\",\n )}\n />\n </>\n )}\n {showLabel ? (\n <span\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute bottom-full left-1/2 mb-1 max-w-48 -translate-x-1/2 truncate whitespace-nowrap rounded-md border border-border bg-popover px-2 py-1 text-xs font-medium text-popover-foreground shadow-sm\"\n >\n {title}\n </span>\n ) : null}\n </button>\n );\n}\n\n/** Several places too close to tell apart at this zoom: a count. */\nexport function ClusterMark({ count, label, holdsSelected }) {\n const size = Math.round(32 + Math.min(20, Math.log2(count) * 4));\n return (\n // raw: a cluster is a positioned hit target on the canvas (see PointMark).\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={label}\n data-lifted={holdsSelected ? \"\" : undefined}\n style={{ width: size, height: size }}\n className={cx(\n \"grid cursor-pointer place-items-center rounded-full bg-primary text-xs font-semibold tabular-nums text-primary-foreground shadow-md\",\n \"ring-4 ring-primary/20 outline-none transition-[box-shadow,transform] duration-150 hover:scale-105 hover:ring-primary/30\",\n \"focus-visible:ring-ring motion-reduce:transition-none motion-reduce:hover:scale-100\",\n holdsSelected && \"outline-2 outline-offset-2 outline-primary\",\n )}\n >\n {count > 999 ? `${Math.floor(count / 1000)}k` : count}\n </button>\n );\n}\n\n/** The popup's box: a kit-looking card with a close button. */\nexport function PopupCard({ onClose, closeLabel = \"Close\", children }) {\n return (\n <div className=\"relative w-64 max-w-[calc(100vw-3rem)] rounded-lg border border-border bg-popover p-3 text-sm text-popover-foreground shadow-lg\">\n <IconButton icon=\"x\" label={closeLabel} size=\"sm\" onClick={onClose} className=\"absolute right-1 top-1 text-muted-foreground\" />\n {children}\n </div>\n );\n}\n\n/** The popup's words when the caller brings none: name, meta line, aside. */\nexport function DefaultPopup({ title, meta, aside }) {\n return (\n <div className=\"grid gap-1 pr-8\">\n <p className=\"font-medium leading-5\">{title}</p>\n {meta ? <div className=\"text-muted-foreground\">{meta}</div> : null}\n {aside ? <div className=\"pt-1\">{aside}</div> : null}\n </div>\n );\n}\n\n/** Places that share one spot (a building, a campus): pick one. */\nexport function StackList({ rows, plural, onPick }) {\n return (\n <div className=\"grid gap-1\">\n <p className=\"pr-8 text-xs text-muted-foreground\">\n {rows.length} {plural} at this spot\n </p>\n <div className=\"-mx-1 max-h-56 overflow-y-auto\">\n {rows.map((r) => (\n <Item key={r.id} size=\"sm\" title={r.title} media={<Dot tone={r.tone} />} onClick={() => onPick(r.id)} className=\"rounded-md\" />\n ))}\n </div>\n </div>\n );\n}\n\n/** The tone dot the list rows and cards lead with. */\nexport function Dot({ tone, missing }) {\n if (missing) return <span aria-hidden=\"true\" className=\"block size-3 shrink-0 rounded-full border border-dashed border-muted-foreground\" />;\n return <span aria-hidden=\"true\" className={cx(\"block size-3 shrink-0 rounded-full\", DOT[tone])} />;\n}\n"
38
+ },
39
+ {
40
+ "path": "registry/blocks/map/maplibre.js",
41
+ "type": "registry:component",
42
+ "target": "@components/makersclaw/blocks/map/maplibre.js",
43
+ "content": "// The one door to MapLibre (maplibre-gl, BSD-3-Clause) and its React\n// binding (react-map-gl, MIT). Every file in this block reaches them\n// through here, and the blocks built on this one reach them through\n// MapView — never the packages directly.\n//\n// No server shim is needed: maplibre-gl 6 and react-map-gl 8 touch no\n// browser global while their modules load (checked in node), and the map\n// itself mounts only after the first effect (MapView's mount guard).\n//\n// Two things are passed explicitly instead of left to the libraries:\n//\n// mapLib react-map-gl would otherwise load maplibre-gl with an\n// import() of its own. Handing it the module we import here\n// keeps the app one static bundle.\n// WORKER_URL maplibre-gl 6 draws tiles in a Web Worker that it loads\n// from a file beside its own module (import.meta.url). Inside\n// an app bundle that URL is a build-time file:// path, so the\n// worker would never load. It is loaded from jsDelivr instead,\n// at exactly the version installed (getVersion()), so the\n// worker and the page always match. A map already fetches its\n// style, tiles and fonts at run time; this is one more file.\n// Self-host it with MapView's `workerUrl` prop.\nimport \"maplibre-gl/dist/maplibre-gl.css\";\nimport * as maplibregl from \"maplibre-gl\";\nimport { AttributionControl, Layer, Map as GLMap, Marker, Popup, Source } from \"react-map-gl/maplibre\";\n\nexport const MAPLIBRE_VERSION = maplibregl.getVersion();\nexport const WORKER_URL = `https://cdn.jsdelivr.net/npm/maplibre-gl@${MAPLIBRE_VERSION}/dist/maplibre-gl-worker.mjs`;\n\nexport { AttributionControl, GLMap, Layer, Marker, Popup, Source, maplibregl };\n"
44
+ },
45
+ {
46
+ "path": "registry/blocks/map/theme.js",
47
+ "type": "registry:component",
48
+ "target": "@components/makersclaw/blocks/map/theme.js",
49
+ "content": "// The map in the workspace theme — the one place its look is mapped.\n//\n// Three layers of look, three answers:\n//\n// the tiles a style URL. The default is OpenFreeMap (free, no key,\n// OpenStreetMap data): \"positron\" in a light scheme,\n// \"dark\" in a dark one, chosen from the scheme in effect\n// where the map sits and swapped when it changes.\n// our marks markers, clusters, popups, the list and the controls are\n// React, in the kit, in token classes — they follow the\n// theme like any other part of the page.\n// areas polygons drawn BY maplibre need concrete colours, so a\n// tone (\"primary\", \"chart-2\") is resolved from the CSS\n// variable at run time (resolveTones) and again when the\n// scheme changes — the sketch-canvas palette trick.\n//\n// maplibre's own chrome (the popup's box and tip, the attribution line,\n// the canvas focus ring) comes from its unlayered stylesheet, which beats\n// any Tailwind utility; the classes in CHROME end in `!` for that reason\n// (the same fix as sketch-canvas/theme.js).\n\n/** OpenFreeMap's styles (https://openfreemap.org), checked 2026-09-23. */\nexport const MAP_STYLES = {\n positron: \"https://tiles.openfreemap.org/styles/positron\",\n liberty: \"https://tiles.openfreemap.org/styles/liberty\",\n bright: \"https://tiles.openfreemap.org/styles/bright\",\n \"dark\": \"https://tiles.openfreemap.org/styles/dark\",\n fiord: \"https://tiles.openfreemap.org/styles/fiord\",\n};\n\n/** What `styleUrl` defaults to: a light and a dark style. */\nexport const DEFAULT_STYLE = { \"light\": MAP_STYLES.positron, \"dark\": MAP_STYLES.dark }; // keys quoted: the block lint reads `dark:` as a Tailwind variant\n\n/** The URL for a scheme, from a string (one style for both) or { light, dark }. */\nexport function styleFor(styleUrl, scheme) {\n if (typeof styleUrl === \"string\" && styleUrl) return styleUrl;\n const s = styleUrl && typeof styleUrl === \"object\" ? styleUrl : DEFAULT_STYLE;\n return (scheme === \"dark\" ? s.dark ?? s.light : s.light ?? s.dark) ?? DEFAULT_STYLE[scheme === \"dark\" ? \"dark\" : \"light\"];\n}\n\n/** The tones a marker, cluster or area may take: token names, never colours. */\nexport const TONES = [\"primary\", \"success\", \"warning\", \"destructive\", \"info\", \"chart-1\", \"chart-2\", \"chart-3\", \"chart-4\", \"chart-5\", \"muted\"];\n\n// Tailwind needs whole class names in the source: one per tone.\nexport const DOT = {\n primary: \"bg-primary\",\n success: \"bg-success\",\n warning: \"bg-warning\",\n destructive: \"bg-destructive\",\n info: \"bg-info\",\n \"chart-1\": \"bg-chart-1\",\n \"chart-2\": \"bg-chart-2\",\n \"chart-3\": \"bg-chart-3\",\n \"chart-4\": \"bg-chart-4\",\n \"chart-5\": \"bg-chart-5\",\n muted: \"bg-muted-foreground\",\n};\n\nexport const toneOrDefault = (tone) => (Object.hasOwn(DOT, tone ?? \"\") ? tone : \"primary\");\n\n/** maplibre's chrome, pointed at tokens. */\nexport const CHROME = [\n // the canvas: our focus ring, no browser outline\n \"[&_.maplibregl-canvas]:outline-none!\",\n \"[&_.maplibregl-canvas-container:has(.maplibregl-canvas:focus-visible)]:ring-2!\",\n \"[&_.maplibregl-canvas-container:has(.maplibregl-canvas:focus-visible)]:ring-inset!\",\n \"[&_.maplibregl-canvas-container:has(.maplibregl-canvas:focus-visible)]:ring-ring!\",\n // popups: the box and tip are ours (MapPopup draws a kit card)\n \"[&_.maplibregl-popup-content]:bg-transparent!\",\n \"[&_.maplibregl-popup-content]:p-0!\",\n \"[&_.maplibregl-popup-content]:shadow-none!\",\n \"[&_.maplibregl-popup-content]:rounded-none!\",\n \"[&_.maplibregl-popup-tip]:hidden!\",\n \"[&_.maplibregl-popup]:z-10!\",\n // the attribution line (OpenStreetMap's licence asks for it: it stays)\n \"[&_.maplibregl-ctrl-attrib]:bg-background/80!\",\n \"[&_.maplibregl-ctrl-attrib]:text-muted-foreground!\",\n \"[&_.maplibregl-ctrl-attrib]:text-[10px]!\",\n \"[&_.maplibregl-ctrl-attrib]:leading-4!\",\n \"[&_.maplibregl-ctrl-attrib]:rounded-tl-md!\",\n \"[&_.maplibregl-ctrl-attrib]:px-1.5!\",\n \"[&_.maplibregl-ctrl-attrib_a]:text-muted-foreground!\",\n \"[&_.maplibregl-ctrl-attrib_a:hover]:text-foreground!\",\n \"[&_.maplibregl-ctrl-bottom-right]:z-[1]!\",\n // markers sit above the canvas, the hovered/selected one above the rest\n \"[&_.maplibregl-marker:has([data-lifted])]:z-[2]!\",\n].join(\" \");\n\n// ---- the browser half -------------------------------------------------------\n\n/** The workspace's light or dark where `root` sits: a data-theme on an\n * ancestor, else the computed color-scheme, else the OS setting. */\nexport function schemeOf(root) {\n if (typeof window === \"undefined\" || !root) return \"light\";\n const scoped = root.closest?.(\"[data-theme]\")?.getAttribute(\"data-theme\");\n if (scoped === \"light\" || scoped === \"dark\") return scoped;\n const cs = getComputedStyle(root).colorScheme || \"\";\n if (/\\bdark\\b/.test(cs) && !/\\blight\\b/.test(cs)) return \"dark\";\n if (/\\blight\\b/.test(cs) && !/\\bdark\\b/.test(cs)) return \"light\";\n return window.matchMedia?.(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n}\n\nconst byte = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, \"0\");\n\n/** Each tone → a concrete colour (\"#rrggbb\"), read from the CSS variables\n * in effect at `root`. A probe takes `color: var(--color-<token>)`; the\n * browser resolves var(), light-dark() and color-mix(); a 1×1 canvas\n * turns whatever syntax comes back into bytes. */\nexport function resolveTones(root, tones = TONES) {\n const out = {};\n if (typeof document === \"undefined\" || !root) return out;\n const probe = document.createElement(\"span\");\n probe.setAttribute(\"aria-hidden\", \"true\");\n probe.style.cssText = \"position:absolute;visibility:hidden;pointer-events:none\";\n root.appendChild(probe);\n const canvas = document.createElement(\"canvas\");\n canvas.width = 1;\n canvas.height = 1;\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n try {\n for (const tone of tones) {\n probe.style.color = \"\";\n probe.style.color = `var(--color-${tone === \"muted\" ? \"muted-foreground\" : tone})`;\n const computed = getComputedStyle(probe).color;\n if (!computed || !ctx) continue;\n ctx.clearRect(0, 0, 1, 1);\n ctx.fillStyle = \"transparent\";\n ctx.fillStyle = computed;\n ctx.fillRect(0, 0, 1, 1);\n const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;\n if (a) out[tone] = \"#\" + byte(r) + byte(g) + byte(b);\n }\n } finally {\n probe.remove();\n }\n return out;\n}\n\n/** Does the viewer ask for less motion? Camera moves then jump. */\nexport function prefersReducedMotion() {\n return typeof window !== \"undefined\" && Boolean(window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches);\n}\n"
50
+ },
51
+ {
52
+ "path": "registry/blocks/map/geo.js",
53
+ "type": "registry:component",
54
+ "target": "@components/makersclaw/blocks/map/geo.js",
55
+ "content": "// The map's arithmetic: plain functions of plain objects, so they run in\n// node, on the server and in the blocks built on this one (location-map,\n// store-locator, the Map view of work-views). No maplibre in here.\n//\n// A position is { lat, lng } in degrees (WGS 84, what every geocoder and\n// GPS gives). Longitude first only where maplibre wants it ([lng, lat]).\n\nconst EARTH_KM = 6371.0088;\nconst MAX_LAT = 85.05112878; // web mercator stops here\n\nconst num = (v) => (typeof v === \"string\" && v.trim() !== \"\" ? Number(v) : v);\nconst isLat = (v) => typeof v === \"number\" && Number.isFinite(v) && v >= -90 && v <= 90;\nconst isLng = (v) => typeof v === \"number\" && Number.isFinite(v) && v >= -180 && v <= 180;\n\n/** A valid { lat, lng } from whatever a row holds, or null. Reads\n * `lat`/`lng`, `latitude`/`longitude`, `lon`, `location: { lat, lng }`\n * (the task-model shape), `position` and `[lng, lat]` pairs (GeoJSON order). */\nexport function readPosition(item) {\n if (!item || typeof item !== \"object\") return null;\n if (Array.isArray(item)) return toPos(num(item[1]), num(item[0]));\n const direct = toPos(num(item.lat ?? item.latitude), num(item.lng ?? item.lon ?? item.longitude));\n if (direct) return direct;\n for (const key of [\"location\", \"position\", \"coords\", \"geo\"]) {\n const inner = item[key];\n if (inner && typeof inner === \"object\" && inner !== item) {\n const p = readPosition(inner);\n if (p) return p;\n }\n }\n return null;\n}\n\nfunction toPos(lat, lng) {\n return isLat(lat) && isLng(lng) ? { lat, lng } : null;\n}\n\n/** [lng, lat] → a point on the web-mercator world at `zoom`, in pixels\n * (the world is 512 px wide at zoom 0, as maplibre draws it). */\nexport function project({ lat, lng }, zoom) {\n const size = 512 * 2 ** zoom;\n const clamped = Math.max(-MAX_LAT, Math.min(MAX_LAT, lat));\n const s = Math.sin((clamped * Math.PI) / 180);\n return {\n x: ((lng + 180) / 360) * size,\n y: (0.5 - Math.log((1 + s) / (1 - s)) / (4 * Math.PI)) * size,\n };\n}\n\n/** The box around some positions: { west, south, east, north }, or null\n * for none. One position gives a box of no size (fitBounds pads it). */\nexport function boundsOf(positions) {\n let west = Infinity;\n let south = Infinity;\n let east = -Infinity;\n let north = -Infinity;\n for (const p of positions ?? []) {\n if (!p) continue;\n if (p.lng < west) west = p.lng;\n if (p.lng > east) east = p.lng;\n if (p.lat < south) south = p.lat;\n if (p.lat > north) north = p.lat;\n }\n return west === Infinity ? null : { west, south, east, north };\n}\n\n/** Is the box a single spot (every position the same, give or take a metre)? */\nexport function isPointBounds(b) {\n return Boolean(b) && Math.abs(b.east - b.west) < 1e-5 && Math.abs(b.north - b.south) < 1e-5;\n}\n\n/** Does `b` hold the position? */\nexport function inBounds(b, p) {\n if (!b || !p) return false;\n const lngIn = b.west <= b.east ? p.lng >= b.west && p.lng <= b.east : p.lng >= b.west || p.lng <= b.east; // across the date line\n return lngIn && p.lat >= b.south && p.lat <= b.north;\n}\n\n/**\n * Group points that would overlap on screen at `zoom`.\n *\n * points [{ id, lat, lng }]\n * radius how close, in screen pixels, two points may sit before they\n * merge (default 48 — a marker and its label)\n *\n * Returns, in the input order of each group's first point:\n * { type: \"point\", id, lat, lng }\n * { type: \"cluster\", id, ids, count, lat, lng, bounds }\n *\n * Greedy, like supercluster's single level: each point not yet taken\n * gathers every free point within `radius` of it (a grid of radius-sized\n * cells finds them), and the group sits at their mean. It depends on the\n * zoom only, never on the pan, so the map recomputes it when the zoom\n * crosses a step, not on every frame. Thousands of points take a few ms.\n */\nexport function clusterPoints(points, zoom, radius = 48) {\n const list = (points ?? []).filter((p) => p && isLat(p.lat) && isLng(p.lng));\n const px = list.map((p) => project(p, zoom));\n const cell = Math.max(1, radius);\n const grid = new Map();\n px.forEach((q, i) => {\n const key = `${Math.floor(q.x / cell)}:${Math.floor(q.y / cell)}`;\n const bucket = grid.get(key);\n if (bucket) bucket.push(i);\n else grid.set(key, [i]);\n });\n const taken = new Uint8Array(list.length);\n const out = [];\n const r2 = radius * radius;\n for (let i = 0; i < list.length; i++) {\n if (taken[i]) continue;\n taken[i] = 1;\n const members = [i];\n const cx = Math.floor(px[i].x / cell);\n const cy = Math.floor(px[i].y / cell);\n for (let gx = cx - 1; gx <= cx + 1; gx++) {\n for (let gy = cy - 1; gy <= cy + 1; gy++) {\n for (const j of grid.get(`${gx}:${gy}`) ?? []) {\n if (taken[j]) continue;\n const dx = px[j].x - px[i].x;\n const dy = px[j].y - px[i].y;\n if (dx * dx + dy * dy <= r2) {\n taken[j] = 1;\n members.push(j);\n }\n }\n }\n }\n if (members.length === 1) {\n const p = list[i];\n out.push({ type: \"point\", id: p.id, lat: p.lat, lng: p.lng });\n continue;\n }\n const ps = members.map((m) => list[m]);\n out.push({\n type: \"cluster\",\n id: `cluster:${ps[0].id}:${ps.length}`,\n ids: ps.map((p) => p.id),\n count: ps.length,\n lat: ps.reduce((s, p) => s + p.lat, 0) / ps.length,\n lng: ps.reduce((s, p) => s + p.lng, 0) / ps.length,\n bounds: boundsOf(ps),\n });\n }\n return out;\n}\n\n/** The lowest zoom, from `zoom` up in half steps to `maxZoom`, at which\n * point `id` stands on its own — where the map must go to show it. Null\n * when it never does (it shares its spot with another point). */\nexport function zoomToReveal(points, id, zoom, { radius = 48, maxZoom = 18 } = {}) {\n for (let z = Math.max(0, Math.ceil(zoom * 2) / 2); z <= maxZoom; z += 0.5) {\n const groups = clusterPoints(points, z, radius);\n if (groups.some((g) => g.type === \"point\" && g.id === id)) return z;\n }\n return null;\n}\n\n/** Great-circle distance between two positions, in kilometres. */\nexport function distanceKm(a, b) {\n if (!a || !b) return NaN;\n const rad = Math.PI / 180;\n const dLat = (b.lat - a.lat) * rad;\n const dLng = (b.lng - a.lng) * rad;\n const h = Math.sin(dLat / 2) ** 2 + Math.cos(a.lat * rad) * Math.cos(b.lat * rad) * Math.sin(dLng / 2) ** 2;\n return 2 * EARTH_KM * Math.asin(Math.min(1, Math.sqrt(h)));\n}\n\n/** \"350 m\", \"4.2 km\", \"38 km\" — or miles with unit \"mi\" (\"0.3 mi\", \"24 mi\"). */\nexport function formatDistance(km, unit = \"km\") {\n if (!Number.isFinite(km)) return \"\";\n if (unit === \"mi\") {\n const mi = km / 1.609344;\n return mi < 10 ? `${mi.toFixed(1)} mi` : `${Math.round(mi).toLocaleString(\"en\")} mi`;\n }\n if (km < 1) return `${Math.max(10, Math.round((km * 1000) / 10) * 10)} m`;\n return km < 10 ? `${km.toFixed(1)} km` : `${Math.round(km).toLocaleString(\"en\")} km`;\n}\n\n/** Rows nearest first from `origin`, each with its `distanceKm`; rows\n * with no position go last (distance NaN). `positionOf` reads a row. */\nexport function sortByDistance(rows, origin, positionOf = readPosition) {\n return (rows ?? [])\n .map((row) => ({ row, distanceKm: distanceKm(origin, positionOf(row)) }))\n .sort((a, b) => (Number.isNaN(a.distanceKm) ? 1 : 0) - (Number.isNaN(b.distanceKm) ? 1 : 0) || a.distanceKm - b.distanceKm);\n}\n\n/** A cheap key for a set of ids: equal keys, the same set. */\nexport function idsKey(ids) {\n return [...ids].map(String).sort().join(\"|\");\n}\n\n/** Lower-cased words a row is found by: the title and the meta line. */\nexport function matches(text, query) {\n const q = String(query ?? \"\").trim().toLowerCase();\n if (!q) return true;\n const hay = String(text ?? \"\").toLowerCase();\n return q.split(/\\s+/).every((w) => hay.includes(w));\n}\n"
56
+ },
57
+ {
58
+ "path": "registry/blocks/map/README.md",
59
+ "type": "registry:file",
60
+ "target": "@components/makersclaw/blocks/map/README.md",
61
+ "content": "# MapView\n\nPlaces on an interactive map, with their list beside it. Every row that\nhas a position becomes a marker — a dot in a token tone, or your own kit\ncontent. Markers that crowd merge into a count; click it to zoom in. Click\na marker and a popup opens with kit parts. The list on the left is\nsearchable and shares one selection and one hover with the map: pick a\nrow and the map moves to it, pick a marker and the list scrolls to it.\nZoom buttons and \"Show all\" sit top right. Rows with no position are\nlisted under \"Not on the map\", never lost.\n\nOn a phone the map fills the block, a strip of cards sits under it, and a\n\"List\" button swaps to the full list.\n\nIt is [MapLibre GL JS](https://github.com/maplibre/maplibre-gl-js)\n(BSD-3-Clause) underneath, through\n[react-map-gl](https://github.com/visgl/react-map-gl) (MIT): its tiles,\ncamera, gestures and keyboard (arrows pan, + and − zoom). The markers,\nclusters, popup, list and controls are ours, in the kit and the\nworkspace's tokens. The blocks built on this one — `location-map`,\n`store-locator`, the Map view of `work-views` — import it, never the map\npackages.\n\n```jsx\nimport { MapView, useMapView } from \"../components/blocks/map\"; // a Makersclaw app (\"./blocks/map\" from components/)\nimport { MapView, useMapView } from \"@/components/makersclaw/blocks/map\"; // a shadcn project\n```\n\n`MapView` is a client component: render it from a `\"use client\"`\ncomponent, and load the rows in the server surface (README §4). The\nhelpers (`readPosition`, `boundsOf`, `distanceKm`…) are plain functions and\nwork on the server.\n\n`makersclaw add map` adds `maplibre-gl` and `react-map-gl` (pinned, exact)\nto package.json: run `npm install` after. They load with the page (no\n`import()`); the map mounts after the first render. Until then — on the\nserver — the frame, the list and a quiet \"Loading the map…\" show.\n\n## What it fetches at run time\n\nA map cannot ship its world inside the app. In the browser it fetches:\n\n- **The tiles, style and fonts** from the style URL. The default is\n [OpenFreeMap](https://openfreemap.org): free, no key, no sign-up,\n OpenStreetMap data. `positron` (light) or `dark`, picked from the\n workspace's scheme and swapped when it changes. The attribution line\n (bottom right) is the data licence's condition: it stays.\n- **MapLibre's tile worker** from jsDelivr, at exactly the installed\n version (`https://cdn.jsdelivr.net/npm/maplibre-gl@<version>/dist/maplibre-gl-worker.mjs`).\n MapLibre 6 finds its worker beside its own file, which a bundled app\n does not have. Host the file yourself and pass `workerUrl` to avoid\n the CDN.\n\nNothing else leaves the page: no geocoding, no analytics.\n\n## The data\n\nRows are your records. A row's position is read by `positionOf`; the\ndefault reads any of these:\n\n```js\n{ lat: 38.71, lng: -9.14 } // or latitude / longitude, or lon\n{ location: { lat: 38.71, lng: -9.14, label } } // the task-model shape\n{ position: [-9.14, 38.71] } // a GeoJSON pair: [lng, lat]\n```\n\nNumbers or numeric strings, in degrees (WGS 84 — what GPS and every\ngeocoder give). A row with no valid position is listed, not mapped.\n\nTones are token names, never colours: `primary` (the default), `success`,\n`warning`, `destructive`, `info`, `chart-1` … `chart-5`, `muted`. A tone\nmeans something — status, owner, type — so say it in words too (a badge\nin `asideOf`).\n\n## Props\n\n- `items` — the rows. Pass `useFixture(rows, SAMPLE).rows` to open with an\n example.\n- `positionOf(row)` — `{ lat, lng }` or null. Default: `readPosition` (above).\n- `idOf(row)` — default `row.id`. Ids are compared as strings.\n- `titleOf(row)` — the name on the marker's label, the list row and the\n popup. Default `row.title ?? row.name ?? row.label ?? row.location.label`.\n- `metaOf(row)` — a muted line under the title (list, card, popup): a\n city, an address. A string, or a node.\n- `asideOf(row)` — a node at the row's end: a `Badge`, a distance.\n- `toneOf(row)` — the marker's tone (above). Default `primary`.\n- `searchOf(row)` — the words the search finds a row by. Default: title\n and meta. Every word typed must appear.\n- `renderMarker(row, { selected, hovered, tone })` — your own marker\n content (an `Avatar`, a price, an icon), kit parts only. It sits inside\n the block's button, so clicks, labels and selection still work.\n- `renderPopup(row, { close })` — the popup's content: kit parts and\n tokens. Default: title, meta and aside. The box and its close button\n are the block's.\n- `selectedId` · `onSelect(id, row)` — control the selection (the row\n your form edits, a URL param). `id` is a string, or null when cleared.\n Leave `selectedId` out and the block keeps its own; `defaultSelectedId`\n sets where it starts. A selection set from outside moves the map to it.\n- `onMove(row, { lat, lng })` — markers become draggable. The marker stays\n where it was dropped at once; return `{ ok: true }` or\n `{ ok: false, error }` (a throw counts as failed) and a failed save puts\n it back with a toast saying why. The block keeps the new spot until\n `positionOf(row)` reports a different one.\n- `onMapClick({ lat, lng })` — a click on the map, not a marker (\"add a\n place here\"). The block also clears the selection.\n- `onViewChange({ center, zoom, bounds })` — after the map stops moving\n (and once on load). `bounds` is `{ west, south, east, north }`: search\n this area, load what is in view.\n- `areas` — tinted regions: `[{ id, label?, tone?, geometry }]`, where\n `geometry` is GeoJSON (`Polygon`, `MultiPolygon`, `LineString`). A soft\n fill and a line in the tone, repainted when the scheme changes.\n- `onAreaClick(area)` — an area is clicked.\n- `list` — `\"side\"` (default: the list beside the map, the card strip on\n a phone) or `\"none\"` (the map alone; markers then take Tab focus).\n- `search` — the search field over the list. Default `true`.\n- `cluster` — merge crowded markers. Default `true`. `clusterRadius` —\n how close in pixels, default `48`.\n- `popup` — open a popup for the selected row. Default `true`.\n- `styleUrl` — the tiles: one URL, or `{ light, dark }` to follow the\n scheme. Default `DEFAULT_STYLE` (OpenFreeMap positron / dark).\n `MAP_STYLES` has OpenFreeMap's five: `positron`, `liberty`, `bright`,\n `dark`, `fiord`. Any MapLibre style URL works (MapTiler, Stadia, your\n own), with the provider's key in the URL if it needs one.\n- `scheme` — `\"auto\"` (default: the workspace's light or dark), `\"light\"`\n or `\"dark\"`.\n- `workerUrl` — where MapLibre's worker loads from (above).\n- `initialView` — `{ lat, lng, zoom }` when there are no positions to\n fit. Default: the whole world.\n- `fitOnChange` — refit to the places when the set on the map changes\n (new rows, a search). Default `true`. `fitMaxZoom` — how close a fit\n may go, default `14`. `maxZoom` — default `18`.\n- `scrollZoom` — the wheel zooms. Default `true`; set `false` for a map\n inside a long page (buttons and pinch still zoom).\n- `controls` — the zoom and \"Show all\" buttons. Default `true`.\n- `height` — the block's height: a number (px) or a CSS length. Default\n `520`. The map needs one; the block never sizes itself to its content.\n- `noun` · `nounPlural` — what a row is called: \"account\" → \"Search\n accounts\", \"12 accounts\", \"No accounts yet\". Default `place`.\n- `emptyTitle` · `emptyBody` · `emptyAction` — the empty list's words and\n its one button (a `Button`), when `items` is empty.\n- `label` — what the region is called to a screen reader. Default `Map`.\n- `mapRef` — the handle (below).\n- `className` — layout around it (width, margin). Never colour.\n\n## The handle\n\n```jsx\nconst map = useMapView();\n<MapView mapRef={map} items={rows} />\nmap.current?.fitAll(); // every place on the map in view\nmap.current?.select(\"acc-07\"); // select and move to it\nmap.current?.flyTo({ lat: 48.86, lng: 2.35, zoom: 11 });\nmap.current?.fitBounds({ west, south, east, north });\nmap.current?.zoomIn(); map.current?.zoomOut();\nmap.current?.getView(); // { center, zoom, bounds } or null before load\nmap.current?.getMap(); // the maplibre Map, for what the props do not cover\n```\n\n## Helpers\n\n- `readPosition(row)` → `{ lat, lng }` or null.\n- `boundsOf(positions)` → `{ west, south, east, north }` or null;\n `inBounds(bounds, position)`.\n- `distanceKm(a, b)` — great-circle distance; `formatDistance(km, \"km\" | \"mi\")`\n → \"350 m\", \"4.2 km\", \"24 mi\".\n- `sortByDistance(rows, origin, positionOf?)` → `[{ row, distanceKm }]`,\n nearest first, rows without a position last.\n- `clusterPoints(points, zoom, radius?)` and `zoomToReveal(points, id, zoom)`\n — the clustering the map uses, for a list that groups the same way.\n\n## Example\n\n```jsx\n// components/Customers.jsx — client: the map, the list, the popup\n\"use client\";\nimport { Badge, Button } from \"./ui\";\nimport { FixtureNotice, useFixture } from \"./parts\";\nimport { MapView } from \"./blocks/map\";\n\nconst SAMPLE = [\n { name: \"Northwind Café\", city: \"Lisbon\", lat: 38.7107, lng: -9.1422, status: \"active\" },\n { name: \"Acme Bakery\", city: \"Porto\", lat: 41.1405, lng: -8.6132, status: \"at risk\" },\n];\n\nexport default function Customers({ customers }) {\n const fixture = useFixture(customers, SAMPLE);\n return (\n <div className=\"grid gap-4\">\n {fixture.isFixture ? <FixtureNotice onClear={fixture.clear} /> : null}\n <MapView\n items={fixture.rows}\n noun=\"customer\"\n titleOf={(c) => c.name}\n metaOf={(c) => c.city}\n toneOf={(c) => (c.status === \"active\" ? \"success\" : \"warning\")}\n asideOf={(c) => <Badge tone={c.status === \"active\" ? \"success\" : \"warning\"} variant=\"soft\">{c.status}</Badge>}\n renderPopup={(c) => (\n <div className=\"grid gap-2 pr-8\">\n <p className=\"font-medium\">{c.name}</p>\n <p className=\"text-muted-foreground\">{c.city}</p>\n <Button size=\"sm\" variant=\"primary\" href={`/customers/${c.id}`}>Open</Button>\n </div>\n )}\n />\n </div>\n );\n}\n```\n\n## What it will not do\n\n- Geocode. It maps coordinates; turning an address into them is a\n server job (store the result on the row).\n- Heatmaps, routes, 3D buildings, satellite imagery. Pass `styleUrl` for\n another base map; `getMap()` reaches maplibre for the rest.\n- Move a marker from the keyboard. Dragging is pointer-only; keep a\n coordinates field in the row's form for keyboard users.\n- Scale past a few thousand markers. Each marker is a DOM node; for tens\n of thousands, use maplibre layers through `getMap()`.\n- Work offline or without WebGL: the list stays, and the map says why it\n cannot show.\n"
62
+ }
63
+ ],
64
+ "categories": [
65
+ "map",
66
+ "maps",
67
+ "location",
68
+ "geo",
69
+ "markers",
70
+ "pins",
71
+ "cluster",
72
+ "maplibre",
73
+ "openstreetmap",
74
+ "store locator",
75
+ "territory",
76
+ "places",
77
+ "coordinates",
78
+ "latitude",
79
+ "longitude",
80
+ "distance"
81
+ ],
82
+ "docs": "Installed to components/makersclaw/blocks/map. Import: import { MapView, useMapView, MAP_STYLES, DEFAULT_STYLE, TONES, readPosition, boundsOf, inBounds, clusterPoints, zoomToReveal, distanceKm, formatDistance, sortByDistance } from \"@/components/makersclaw/blocks/map\". Props and an example: components/makersclaw/blocks/map/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
83
+ "meta": {
84
+ "makersclaw": {
85
+ "version": "0.3.0",
86
+ "useWhen": "A screen shows records that have a place: customers, stores, offices, field visits, deliveries, events, territories. The location-map and store-locator blocks and the Map view of work-views are built on it. Not for a chart of numbers by region (chart) or a hand-drawn diagram (sketch-canvas, flow-canvas)."
87
+ }
88
+ }
89
+ }
package/r/menubar.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "menubar",
4
+ "type": "registry:block",
5
+ "title": "Menubar",
6
+ "description": "A desktop app's menu bar — File, Edit, View, Help — each a menu of actions with shortcuts, checkboxes, radio groups and submenus; click to open, hover to switch, arrow keys along the bar; one menu button on a phone.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/core.json"
9
+ ],
10
+ "dependencies": [],
11
+ "files": [
12
+ {
13
+ "path": "registry/blocks/menubar/index.js",
14
+ "type": "registry:component",
15
+ "target": "@components/makersclaw/blocks/menubar/index.js",
16
+ "content": "// The Menubar block — `makersclaw add menubar`. Fixed: compose it through\n// its props (README.md beside this file); a menubar it cannot be is your\n// own component in components/.\nexport { Menubar } from \"./menubar\";\n"
17
+ },
18
+ {
19
+ "path": "registry/blocks/menubar/menubar.jsx",
20
+ "type": "registry:component",
21
+ "target": "@components/makersclaw/blocks/menubar/menubar.jsx",
22
+ "content": "\"use client\";\n\nimport { useId, useRef, useState } from \"react\";\nimport { Icon, Menu, MenuPanel, cx, panelClass, usePopoverPosition } from \"../../ui\";\n\n// Menubar — a desktop app's File · Edit · View · Help. Each top-level\n// menu is a trigger and the kit's MenuPanel (the same panel, keys and\n// `items` as Menu), opened as an auto popover: the browser closes the\n// others when one opens, and does the light dismiss and Escape.\n//\n// The WAI-ARIA menubar pattern: the bar is one Tab stop (roving\n// tabindex); ArrowLeft/Right move along it, and when a menu is open they\n// open the neighbour instead; ArrowDown, Enter or Space open a menu at its\n// first item, ArrowUp at its last. Once a menu is open, hovering another\n// trigger switches to it. Escape closes and puts focus back on the\n// trigger. Below `md` the bar is one menu button whose menus unfold as\n// submenus in place — Menu already does that on a phone.\n\nconst ITEMS = '[role^=\"menuitem\"]:not([disabled])';\n\n// Only this panel's items, not a submenu's inside it.\nfunction own(panel) {\n return panel ? [...panel.querySelectorAll(ITEMS)].filter((n) => n.closest('[role=\"menu\"]') === panel) : [];\n}\n\nconst focus = (n) => n?.focus({ preventScroll: true });\n\nexport function Menubar({ menus, label = \"Menu bar\", menuLabel = \"Menu\", className }) {\n const id = useId();\n const live = (menus ?? []).filter((m) => m && (m.items ?? []).some(Boolean));\n const [active, setActive] = useState(0);\n const [open, setOpen] = useState(-1);\n const triggers = useRef([]);\n const panels = useRef([]);\n const start = useRef(\"first\");\n\n if (live.length === 0) return null;\n const n = live.length;\n const tabStop = Math.min(active, n - 1);\n const isOpen = (i) => Boolean(panels.current[i]?.matches(\":popover-open\"));\n const openNow = () => live.findIndex((_, i) => isOpen(i));\n\n function focusStart(i) {\n const nodes = own(panels.current[i]);\n if (start.current === \"first\") focus(nodes[0]);\n else if (start.current === \"last\") focus(nodes[nodes.length - 1]);\n start.current = \"first\";\n }\n\n // Open menu i; an open sibling closes (auto popovers). `from` is where\n // focus goes: its first item, its last, or \"none\" — stay on the trigger.\n function show(i, from = \"first\") {\n const p = panels.current[i];\n if (!p) return;\n setActive(i);\n start.current = from;\n if (from === \"none\") focus(triggers.current[i]);\n if (isOpen(i)) focusStart(i);\n else p.showPopover();\n }\n\n function hide(i) {\n focus(triggers.current[i]);\n if (isOpen(i)) panels.current[i].hidePopover();\n }\n\n function onToggle(i, e) {\n // A submenu's toggle bubbles here; it is not this menu's.\n if (e.target !== e.currentTarget) return;\n if (e.newState === \"open\") {\n setOpen(i);\n setActive(i);\n focusStart(i);\n } else {\n setOpen((o) => (o === i ? -1 : o));\n const a = document.activeElement;\n if (!a || a === document.body || panels.current[i]?.contains(a)) focus(triggers.current[i]);\n }\n }\n\n function onTriggerKey(i, e) {\n const k = e.key;\n if (k === \"ArrowRight\" || k === \"ArrowLeft\") {\n const j = (i + (k === \"ArrowRight\" ? 1 : -1) + n) % n;\n if (openNow() >= 0) show(j, \"first\");\n else focus(triggers.current[j]);\n } else if (k === \"Home\") focus(triggers.current[0]);\n else if (k === \"End\") focus(triggers.current[n - 1]);\n else if (k === \"ArrowDown\") show(i, \"first\");\n else if (k === \"ArrowUp\") show(i, \"last\");\n else return; // Enter and Space click the trigger, which toggles its menu\n e.preventDefault();\n }\n\n // Keys the open panel does not use itself: ArrowRight on an item with\n // no submenu, ArrowLeft outside a submenu — the neighbours.\n function onPanelKey(i, e) {\n if (e.key !== \"ArrowRight\" && e.key !== \"ArrowLeft\") return;\n e.preventDefault();\n show((i + (e.key === \"ArrowRight\" ? 1 : -1) + n) % n, \"first\");\n }\n\n return (\n <div className={className}>\n <div role=\"menubar\" aria-label={label} aria-orientation=\"horizontal\" className=\"hidden h-10 w-fit max-w-full items-center gap-1 rounded-md border border-border bg-background p-1 shadow-xs md:flex\">\n {live.map((m, i) => (\n <Top\n key={m.key ?? `${i}-${m.label}`}\n menu={m}\n triggerId={`${id}t${i}`}\n panelId={`${id}m${i}`}\n tabIndex={i === tabStop ? 0 : -1}\n expanded={open === i}\n triggerRef={(el) => (triggers.current[i] = el)}\n panelRef={(el) => (panels.current[i] = el)}\n onFocus={() => setActive(i)}\n onPointerEnter={(e) => {\n if (e.pointerType !== \"mouse\") return;\n const o = openNow();\n if (o >= 0 && o !== i) show(i, \"none\");\n }}\n onPointerDown={() => (start.current = \"first\")}\n onTriggerKey={(e) => onTriggerKey(i, e)}\n onPanelKey={(e) => onPanelKey(i, e)}\n onToggle={(e) => onToggle(i, e)}\n onExit={() => hide(i)}\n />\n ))}\n </div>\n <div className=\"md:hidden\">\n <Menu\n items={live.map((m) => ({ label: m.label, icon: m.icon, items: m.items }))}\n label={menuLabel}\n align=\"start\"\n trigger={\n <>\n <Icon name=\"menu\" />\n <span>{menuLabel}</span>\n </>\n }\n className=\"h-11 border border-border bg-background px-3 font-medium shadow-xs\"\n />\n </div>\n </div>\n );\n}\n\n// One top-level menu: its trigger and its panel. A component of its own\n// because each panel keeps itself beside its trigger with a hook.\nfunction Top({ menu, triggerId, panelId, tabIndex, expanded, triggerRef, panelRef, onFocus, onPointerEnter, onPointerDown, onTriggerKey, onPanelKey, onToggle, onExit }) {\n const btn = useRef(null);\n const pop = useRef(null);\n usePopoverPosition(btn, pop, { side: \"bottom\", align: \"start\", offset: 6 });\n\n return (\n <div role=\"none\" onKeyDown={(e) => (e.target === btn.current ? onTriggerKey(e) : onPanelKey(e))}>\n {/* raw: a menubar item — role=menuitem, a roving tabindex and its\n panel's popoverTarget, drawn flat like Menu's own trigger; Button\n is a form control's look. */}\n <button\n ref={(el) => {\n btn.current = el;\n triggerRef(el);\n }}\n id={triggerId}\n type=\"button\"\n role=\"menuitem\"\n popoverTarget={panelId}\n aria-haspopup=\"menu\"\n aria-expanded={expanded}\n aria-controls={panelId}\n tabIndex={tabIndex}\n onFocus={onFocus}\n onPointerEnter={onPointerEnter}\n onPointerDown={onPointerDown}\n className={cx(\n \"inline-flex h-8 select-none items-center rounded-sm px-3 text-sm font-medium outline-none\",\n \"hover:bg-accent focus-visible:bg-accent focus-visible:ring-2 focus-visible:ring-ring aria-expanded:bg-accent aria-expanded:text-accent-foreground\",\n )}\n >\n {menu.label}\n </button>\n <MenuPanel\n id={panelId}\n items={menu.items}\n labelledBy={triggerId}\n panelRef={(el) => {\n pop.current = el;\n panelRef(el);\n }}\n popover=\"auto\"\n onToggle={onToggle}\n onExit={onExit}\n onClose={onExit}\n className={panelClass(\"auto\", \"min-w-52 max-h-[calc(100vh-1rem)] overflow-y-auto p-1\")}\n />\n </div>\n );\n}\n"
23
+ },
24
+ {
25
+ "path": "registry/blocks/menubar/README.md",
26
+ "type": "registry:file",
27
+ "target": "@components/makersclaw/blocks/menubar/README.md",
28
+ "content": "# Menubar\n\nA desktop app's menu bar: File, Edit, View, Help. Each menu is the kit's\nMenu panel, so it takes the same `items` — actions with shortcuts,\ncheckboxes, radio groups, labelled groups, separators and submenus. Click\na menu to open it; once one is open, hovering another switches to it.\nArrowLeft and ArrowRight move along the bar, and open the neighbour when\na menu is open. ArrowDown, Enter or Space open a menu at its first item.\nEscape closes it and puts focus back on its name. The bar is one Tab\nstop (roving focus), with the `menubar` and `menuitem` roles. Below the\n`md` breakpoint the bar is one menu button; each menu is a submenu in it\nthat unfolds in place.\n\n```jsx\nimport { Menubar } from \"../components/blocks/menubar\"; // a Makersclaw app (\"./blocks/menubar\" from components/)\nimport { Menubar } from \"@/components/makersclaw/blocks/menubar\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component.\n\n## The data\n\n`menus` is the bar, in order:\n\n```js\n{\n label: \"View\", // the name in the bar\n items: [ … ], // the menu: exactly Menu's `items` (components/ui/menu.jsx)\n icon?: \"eye\", // shown beside the name in the phone menu only\n key?: \"view\", // default: made from the position and label\n}\n```\n\n`items` entries, any mix:\n\n```js\n{ label: \"Save\", onSelect, shortcut: \"⌘S\", icon?, tone?: \"danger\", disabled?, description? } // an action: closes the menu\n{ type: \"checkbox\", label: \"Show ruler\", checked, onCheckedChange(next) } // stays open\n{ type: \"radio\", label: \"Zoom\", value, onValueChange(value), options: [\"75%\", \"100%\"] } // stays open\n{ group: \"Share\", items: [ … ] } // a labelled group\n{ type: \"separator\" }\n{ label: \"Export\", items: [ … ] } // a submenu\n```\n\nA falsy entry is skipped, so `cond && { … }` works. A menu with no items\nis left out of the bar. A shortcut is only drawn: the app binds the key\nitself.\n\n## Props\n\n- `menus` — the bar, as above.\n- `label` — what a screen reader calls the bar, default `\"Menu bar\"`.\n Name it after what it drives: `\"Document\"`.\n- `menuLabel` — the phone's menu button, default `\"Menu\"`.\n- `className` — layout around it (width, margin). Never colour.\n\n## Example\n\n```jsx\n// components/Editor.jsx — client: the bar over the document\n\"use client\";\nimport { useState } from \"react\";\nimport { toast } from \"./ui\";\nimport { Menubar } from \"./blocks/menubar\";\n\nexport default function Editor({ doc, onSave }) {\n const [ruler, setRuler] = useState(true);\n const [zoom, setZoom] = useState(\"100%\");\n return (\n <div className=\"grid gap-4\">\n <Menubar\n label=\"Document\"\n menus={[\n {\n label: \"File\",\n items: [\n { label: \"Save\", shortcut: \"⌘S\", onSelect: async () => ((await onSave(doc)).ok ? toast.success(\"Saved\") : toast.error(\"Could not save\")) },\n { label: \"Export\", items: [{ label: \"PDF\", onSelect: () => {} }, { label: \"Markdown\", onSelect: () => {} }] },\n ],\n },\n {\n label: \"View\",\n items: [\n { type: \"checkbox\", label: \"Show ruler\", checked: ruler, onCheckedChange: setRuler },\n { type: \"separator\" },\n { type: \"radio\", label: \"Zoom\", value: zoom, onValueChange: setZoom, options: [\"75%\", \"100%\", \"150%\"] },\n ],\n },\n ]}\n />\n {/* the document */}\n </div>\n );\n}\n```\n\n## What it will not do\n\n- Bind shortcuts: `shortcut` is a hint drawn in the menu. Listen for the\n key in your component when it should work without the menu.\n- Hold state: checkboxes and radios are controlled — keep `checked` and\n `value` in your component, as above.\n- Be a site's navigation: links belong in the navigation-menu block.\n- Open on hover before a click: like a desktop app, the first menu opens\n on a click or a key.\n\nA menubar it cannot be is your own component in `components/`, built from\nthe kit — not an edit to this block. Its files are fixed: `npm run check`\nrefuses a change, and `makersclaw add menubar` restores them.\n"
29
+ }
30
+ ],
31
+ "categories": [
32
+ "menubar",
33
+ "menu bar",
34
+ "menu",
35
+ "file menu",
36
+ "edit menu",
37
+ "view menu",
38
+ "commands",
39
+ "shortcuts",
40
+ "editor",
41
+ "toolbar",
42
+ "desktop",
43
+ "dropdown"
44
+ ],
45
+ "docs": "Installed to components/makersclaw/blocks/menubar. Import: import { Menubar } from \"@/components/makersclaw/blocks/menubar\". Props and an example: components/makersclaw/blocks/menubar/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
46
+ "meta": {
47
+ "makersclaw": {
48
+ "version": "0.3.0",
49
+ "useWhen": "A document-style tool with many commands grouped by kind: an editor, a canvas, a spreadsheet, an admin console that mimics a desktop app. Not for a site's navigation (a navigation menu) or one record's actions (a menu on the row)."
50
+ }
51
+ }
52
+ }