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,94 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "code-editor",
4
+ "type": "registry:block",
5
+ "title": "CodeEditor",
6
+ "description": "A code editor (CodeMirror 6) in the workspace theme: SQL with the database's tables and columns in autocomplete, JSON checked as you type with plain-words errors, or plain text; line numbers, problems underlined in the text with a dot in the gutter, find and replace, folding, and a run hook on Cmd/Ctrl+Enter that runs the selection or the statement under the cursor. A quiet status line shows problems, the cursor, the mode and a Run button. Helpers split SQL into statements and check or format JSON anywhere, the server included.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/core.json"
9
+ ],
10
+ "dependencies": [
11
+ "@codemirror/state@6.7.6",
12
+ "@codemirror/view@6.43.13",
13
+ "@codemirror/commands@6.11.1",
14
+ "@codemirror/language@6.12.4",
15
+ "@codemirror/autocomplete@6.20.3",
16
+ "@codemirror/search@6.7.2",
17
+ "@codemirror/lint@6.9.7",
18
+ "@codemirror/lang-sql@6.10.0",
19
+ "@codemirror/lang-json@6.0.2",
20
+ "@lezer/highlight@1.2.3"
21
+ ],
22
+ "files": [
23
+ {
24
+ "path": "registry/blocks/code-editor/index.js",
25
+ "type": "registry:component",
26
+ "target": "@components/makersclaw/blocks/code-editor/index.js",
27
+ "content": "// The CodeEditor block — `makersclaw add code-editor`. Fixed: compose it\n// through its props (README.md beside this file). The helpers read SQL and\n// JSON anywhere, the server included; the blocks built on this one\n// (json-editor, sql-console…) import it, never @codemirror/* directly.\nexport { CodeEditor, useCodeEditor, LANGUAGES, SQL_DIALECTS } from \"./code-editor\";\nexport { splitStatements, statementAt, sqlNamespace } from \"./sql\";\nexport { checkJson, formatJson, stringifyJson } from \"./json\";\nexport { lineCol, offsetOf, normalizeDiagnostics } from \"./text\";\n"
28
+ },
29
+ {
30
+ "path": "registry/blocks/code-editor/code-editor.jsx",
31
+ "type": "registry:component",
32
+ "target": "@components/makersclaw/blocks/code-editor/code-editor.jsx",
33
+ "content": "\"use client\";\n\n// CodeEditor — CodeMirror 6, in the workspace theme, behind one set of\n// props. SQL (with the database's tables for autocomplete), JSON (checked\n// as you type) or plain text; line numbers, problems drawn on the text,\n// find and replace, and a run hook on Cmd/Ctrl+Enter.\n//\n// Client only, one static bundle. CodeMirror needs the DOM, and the app's\n// check refuses import() (so no next/dynamic): the modules load with the\n// page, and the editor itself is created only after the first effect (the\n// mount guard). Until then — on the server and in the first client paint —\n// the same box shows the text with its line numbers, so nothing jumps.\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { Button, Icon, KbdGroup, cx } from \"../../ui\";\nimport {\n Compartment,\n Decoration,\n EditorSelection,\n EditorState,\n EditorView,\n MSSQL,\n MariaSQL,\n MySQL,\n PostgreSQL,\n Prec,\n SQLite,\n StandardSQL,\n StateEffect,\n StateField,\n autocompletion,\n bracketMatching,\n closeBrackets,\n closeBracketsKeymap,\n completeFromList,\n completionKeymap,\n crosshairCursor,\n defaultKeymap,\n diagnosticCount,\n drawSelection,\n dropCursor,\n foldGutter,\n foldKeymap,\n forceLinting,\n highlightActiveLineGutter,\n highlightSelectionMatches,\n highlightSpecialChars,\n history,\n historyKeymap,\n indentOnInput,\n indentUnit,\n indentWithTab,\n json,\n keymap,\n lineNumbers,\n lintGutter,\n lintKeymap,\n linter,\n nextDiagnostic,\n placeholder as placeholderExt,\n rectangularSelection,\n searchKeymap,\n sql,\n syntaxHighlighting,\n} from \"./codemirror\";\nimport { editorTheme, highlightStyle } from \"./theme\";\nimport { lineCol, normalizeDiagnostics, offsetOf } from \"./text\";\nimport { sqlNamespace, statementAt } from \"./sql\";\nimport { checkJson, formatJson } from \"./json\";\n\n/** The modes, and the name the footer shows for each. */\nexport const LANGUAGES = { sql: \"SQL\", json: \"JSON\", text: \"Plain text\" };\n\n/** SQL dialects: keywords, quoting and functions differ between them. */\nexport const SQL_DIALECTS = { postgres: PostgreSQL, mysql: MySQL, mariadb: MariaSQL, sqlite: SQLite, mssql: MSSQL, standard: StandardSQL };\n\n/** A ref for the editor's handle: `const ed = useCodeEditor()`, then\n * `<CodeEditor editorRef={ed} />` and `ed.current?.insert(\"now()\")`. */\nexport function useCodeEditor() {\n return useRef(null);\n}\n\nconst px = (v) => (typeof v === \"number\" ? `${v}px` : v);\n\n// The frame, the same for the placeholder and the live editor. Phones get\n// 16px text: iOS zooms into anything smaller when it takes focus.\nconst FRAME =\n \"relative flex min-w-0 flex-col overflow-hidden rounded-lg border border-border bg-card text-card-foreground \" +\n \"[--code-editor-size:16px] sm:[--code-editor-size:13px] transition-[border-color,box-shadow] duration-150 motion-reduce:transition-none \" +\n \"focus-within:border-ring/60 focus-within:ring-2 focus-within:ring-ring/15\";\n// CodeMirror's floating panels live inside the editor; they get the kit's shadow.\nconst FLOATING = \"[&_.cm-tooltip]:shadow-lg! [&_.cm-tooltip]:text-sm motion-reduce:[&_*]:transition-none!\";\n\nexport function CodeEditor(props) {\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n if (!mounted) return <CodePlaceholder {...props} />;\n return <LiveEditor {...props} />;\n}\n\n// The placeholder's line number, as wide as CodeMirror's gutter (its lint\n// dot column is the 14px the text is pushed by, beside its own 12px).\nconst NUMBER = \"w-[54px] shrink-0 pe-1 text-right tabular-nums text-muted-foreground opacity-70 select-none\";\n\n/** What the server renders (and the first client paint): the text with\n * its line numbers, in the editor's box. Not editable, not focusable. */\nfunction CodePlaceholder({ value, defaultValue, placeholder, height, minHeight = 120, maxHeight = 480, lineNumbers: numbers = true, footer = true, language = \"text\", onRun, className, frameless = false }) {\n const text = String(value ?? defaultValue ?? \"\");\n const lines = text ? text.split(\"\\n\").slice(0, 400) : [];\n return (\n <div className={cx(FRAME, frameless && \"rounded-none border-0 bg-transparent focus-within:ring-0\", className)} aria-busy=\"true\">\n <div\n className=\"min-h-0 flex-1 overflow-hidden py-2.5 font-mono text-[length:var(--code-editor-size)] leading-[1.65]\"\n style={height != null ? { height: px(height) } : { minHeight: px(minHeight), maxHeight: px(maxHeight) }}\n >\n {lines.length ? (\n lines.map((line, i) => (\n <div key={i} className=\"flex\">\n {numbers ? <span className={NUMBER}>{i + 1}</span> : null}\n <span className=\"ps-[26px] pe-4 whitespace-pre\">{line || \" \"}</span>\n </div>\n ))\n ) : (\n <div className=\"flex\">\n {numbers ? <span className={NUMBER}>1</span> : null}\n <span className=\"ps-[26px] text-muted-foreground\">{placeholder || \" \"}</span>\n </div>\n )}\n </div>\n {footer ? <StatusBar language={language} line={1} col={1} onRun={onRun ? () => {} : null} idle /> : null}\n </div>\n );\n}\n\n// Check again now: a prop the linter reads changed (outside problems, the\n// mode, the lint function). The linter re-runs on edits by itself; for\n// anything else it needs this effect, then forceLinting to skip its delay.\nconst relint = StateEffect.define();\nconst wantsRelint = (u) => u.transactions.some((tr) => tr.effects.some((e) => e.is(relint)));\nfunction lintNow(view) {\n if (!view) return;\n view.dispatch({ effects: relint.of(null) });\n forceLinting(view);\n}\n\n// The statement that ran, lit for a moment.\nconst flashEffect = StateEffect.define();\nconst flashMark = Decoration.mark({ class: \"cm-ran\" });\nconst flashField = StateField.define({\n create: () => Decoration.none,\n update(deco, tr) {\n deco = deco.map(tr.changes);\n for (const e of tr.effects) {\n if (!e.is(flashEffect)) continue;\n deco = e.value && e.value.to > e.value.from ? Decoration.set([flashMark.range(e.value.from, e.value.to)]) : Decoration.none;\n }\n return deco;\n },\n provide: (f) => EditorView.decorations.from(f),\n});\n\nfunction markerDOM(open) {\n const el = document.createElement(\"span\");\n el.className = \"cm-fold-marker\";\n el.dataset.open = String(open);\n el.setAttribute(\"aria-hidden\", \"true\");\n el.textContent = \"⌄\";\n return el;\n}\n\nfunction languageExtension({ language, dialect, schema, defaultSchema, completions }) {\n const out = [];\n if (language === \"sql\") {\n const { namespace } = sqlNamespace(schema);\n out.push(sql({ dialect: SQL_DIALECTS[dialect] ?? PostgreSQL, schema: namespace, defaultSchema: defaultSchema || undefined, upperCaseKeywords: true }));\n } else if (language === \"json\") {\n out.push(json(), foldGutter({ markerDOM }));\n }\n if (completions?.length) {\n const list = completions.map((c) => (typeof c === \"string\" ? { label: c, type: \"variable\" } : c));\n out.push(EditorState.languageData.of(() => [{ autocomplete: completeFromList(list) }]));\n }\n return out;\n}\n\nfunction sizeExtension({ height, minHeight = 120, maxHeight = 480 }) {\n if (height != null) return EditorView.theme({ \"&\": { height: px(height) }, \".cm-scroller\": { overflow: \"auto\" } });\n return EditorView.theme({\n \"&\": { maxHeight: px(maxHeight) },\n \".cm-scroller\": { overflow: \"auto\" },\n \".cm-content, .cm-gutter\": { minHeight: px(minHeight) },\n });\n}\n\nfunction LiveEditor({\n value,\n defaultValue,\n onChange,\n language = \"text\",\n dialect = \"postgres\",\n schema,\n defaultSchema,\n completions,\n onRun,\n onSave,\n runLabel = \"Run\",\n diagnostics,\n lint,\n validate = true,\n readOnly = false,\n lineNumbers: numbers = true,\n wrap = false,\n placeholder,\n height,\n minHeight = 120,\n maxHeight = 480,\n tabSize = 2,\n autoFocus = false,\n footer = true,\n frameless = false,\n label,\n editorRef,\n className,\n}) {\n const host = useRef(null);\n const viewRef = useRef(null);\n const applying = useRef(false);\n const [status, setStatus] = useState({ line: 1, col: 1, selected: 0, problems: 0 });\n\n // The latest props for the handlers CodeMirror holds on to.\n const live = useRef({});\n live.current = { onChange, onRun, onSave, lint, language, dialect, validate, diagnostics };\n\n // Problems handed in through `diagnostics` belong to the text they were\n // found in: they show until that text changes.\n const outside = useRef({ text: null, list: [] });\n\n const compartments = useMemo(\n () => ({ lang: new Compartment(), size: new Compartment(), ro: new Compartment(), wrap: new Compartment(), ph: new Compartment(), gutter: new Compartment(), aria: new Compartment(), tab: new Compartment() }),\n [],\n );\n\n function run(view, all = false) {\n const fn = live.current.onRun;\n if (!fn) return false;\n const text = view.state.doc.toString();\n const sel = view.state.selection.main;\n let range;\n if (all) range = { from: 0, to: text.length };\n else if (!sel.empty) range = { from: sel.from, to: sel.to };\n else if (live.current.language === \"sql\") range = statementAt(text, sel.head, { backslash: live.current.dialect === \"mysql\" || live.current.dialect === \"mariadb\" }) ?? { from: 0, to: text.length };\n else range = { from: 0, to: text.length };\n const statement = text.slice(range.from, range.to);\n if (!statement.trim()) return true;\n view.dispatch({ effects: flashEffect.of(range) });\n setTimeout(() => {\n if (viewRef.current === view) view.dispatch({ effects: flashEffect.of(null) });\n }, 450);\n fn({ text, statement, from: range.from, to: range.to, selection: !sel.empty, all });\n return true;\n }\n\n function format(view) {\n if (live.current.language !== \"json\") return false;\n const text = view.state.doc.toString();\n const out = formatJson(text);\n if (out == null || out === text) return true;\n view.dispatch({ changes: { from: 0, to: text.length, insert: out }, selection: EditorSelection.cursor(Math.min(view.state.selection.main.head, out.length)) });\n return true;\n }\n\n // Create the view once; props below reconfigure it.\n useEffect(() => {\n const source = async (view) => {\n const text = view.state.doc.toString();\n const { language: lang, validate: check, lint: fn } = live.current;\n const found = [];\n if (lang === \"json\" && check && text.trim()) {\n const r = checkJson(text);\n if (!r.ok) found.push({ from: r.error.from, to: r.error.to, message: r.error.message, severity: \"error\" });\n }\n if (fn) {\n try {\n found.push(...((await fn(text)) ?? []));\n } catch {\n /* a lint that throws marks nothing */\n }\n }\n if (outside.current.text === text) found.push(...outside.current.list);\n return normalizeDiagnostics(text, found);\n };\n\n const state = EditorState.create({\n doc: String(value ?? defaultValue ?? \"\"),\n extensions: [\n compartments.gutter.of(numbers ? [lineNumbers(), highlightActiveLineGutter()] : []),\n lintGutter(),\n highlightSpecialChars(),\n history(),\n drawSelection(),\n dropCursor(),\n EditorState.allowMultipleSelections.of(true),\n indentOnInput(),\n syntaxHighlighting(highlightStyle),\n bracketMatching(),\n closeBrackets(),\n autocompletion({ icons: true, activateOnTypingDelay: 80 }),\n rectangularSelection(),\n crosshairCursor(),\n highlightSelectionMatches(),\n flashField,\n linter(source, { delay: 300, needsRefresh: wantsRelint }),\n Prec.highest(\n keymap.of([\n { key: \"Mod-Enter\", run: (v) => run(v, false) },\n { key: \"Shift-Mod-Enter\", run: (v) => run(v, true) },\n {\n key: \"Mod-s\",\n preventDefault: true,\n run: (v) => {\n const fn = live.current.onSave;\n if (!fn) return false;\n fn(v.state.doc.toString());\n return true;\n },\n },\n { key: \"Shift-Alt-f\", run: (v) => format(v) },\n ]),\n ),\n keymap.of([...closeBracketsKeymap, ...defaultKeymap, ...searchKeymap, ...historyKeymap, ...foldKeymap, ...completionKeymap, ...lintKeymap, indentWithTab]),\n editorTheme,\n compartments.lang.of(languageExtension({ language, dialect, schema, defaultSchema, completions })),\n compartments.size.of(sizeExtension({ height, minHeight, maxHeight })),\n compartments.ro.of(EditorState.readOnly.of(readOnly)),\n compartments.wrap.of(wrap ? EditorView.lineWrapping : []),\n compartments.ph.of(placeholder ? placeholderExt(placeholder) : []),\n compartments.aria.of(EditorView.contentAttributes.of({ \"aria-label\": label || LANGUAGES[language] || \"Code\", ...(readOnly ? { \"aria-readonly\": \"true\" } : {}) })),\n compartments.tab.of([EditorState.tabSize.of(tabSize), indentUnit.of(\" \".repeat(tabSize))]),\n EditorView.updateListener.of((u) => {\n if (u.docChanged && !applying.current) live.current.onChange?.(u.state.doc.toString());\n const head = u.state.selection.main.head;\n const line = u.state.doc.lineAt(head);\n const selected = u.state.selection.ranges.reduce((n, r) => n + (r.to - r.from), 0);\n const problems = diagnosticCount(u.state);\n setStatus((s) =>\n s.line === line.number && s.col === head - line.from + 1 && s.selected === selected && s.problems === problems\n ? s\n : { line: line.number, col: head - line.from + 1, selected, problems },\n );\n }),\n ],\n });\n const view = new EditorView({ state, parent: host.current });\n viewRef.current = view;\n if (autoFocus) view.focus();\n return () => {\n viewRef.current = null;\n view.destroy();\n };\n // Created once: every prop it reads is reconfigured below.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const reconfigure = (compartment, ext) => viewRef.current?.dispatch({ effects: compartment.reconfigure(ext) });\n const schemaKey = JSON.stringify(schema ?? null) + JSON.stringify(completions ?? null);\n useEffect(() => {\n reconfigure(compartments.lang, languageExtension({ language, dialect, schema, defaultSchema, completions }));\n lintNow(viewRef.current);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [language, dialect, defaultSchema, schemaKey, validate]);\n useEffect(() => reconfigure(compartments.size, sizeExtension({ height, minHeight, maxHeight })), [height, minHeight, maxHeight, compartments]);\n useEffect(() => reconfigure(compartments.ro, EditorState.readOnly.of(readOnly)), [readOnly, compartments]);\n useEffect(() => reconfigure(compartments.wrap, wrap ? EditorView.lineWrapping : []), [wrap, compartments]);\n useEffect(() => reconfigure(compartments.ph, placeholder ? placeholderExt(placeholder) : []), [placeholder, compartments]);\n useEffect(() => reconfigure(compartments.gutter, numbers ? [lineNumbers(), highlightActiveLineGutter()] : []), [numbers, compartments]);\n useEffect(\n () => reconfigure(compartments.aria, EditorView.contentAttributes.of({ \"aria-label\": label || LANGUAGES[language] || \"Code\", ...(readOnly ? { \"aria-readonly\": \"true\" } : {}) })),\n [label, language, readOnly, compartments],\n );\n useEffect(() => reconfigure(compartments.tab, [EditorState.tabSize.of(tabSize), indentUnit.of(\" \".repeat(tabSize))]), [tabSize, compartments]);\n\n // Problems from outside: pinned to the text as it is now.\n const diagKey = JSON.stringify(diagnostics ?? null);\n useEffect(() => {\n const view = viewRef.current;\n if (!view) return;\n outside.current = { text: view.state.doc.toString(), list: diagnostics ?? [] };\n lintNow(view);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [diagKey]);\n // A lint function swapped in: check again.\n useEffect(() => lintNow(viewRef.current), [lint]);\n\n // Controlled: a `value` that is not the text replaces it (loading another\n // record); the one the editor just sent through onChange changes nothing.\n useEffect(() => {\n const view = viewRef.current;\n if (!view || value === undefined) return;\n const next = String(value ?? \"\");\n const now = view.state.doc.toString();\n if (next === now) return;\n applying.current = true;\n try {\n view.dispatch({ changes: { from: 0, to: now.length, insert: next }, selection: EditorSelection.cursor(Math.min(view.state.selection.main.head, next.length)) });\n } finally {\n applying.current = false;\n }\n }, [value]);\n\n // The handle a block reaches through editorRef / useCodeEditor().\n const handle = useMemo(\n () => ({\n get view() {\n return viewRef.current;\n },\n focus: () => viewRef.current?.focus(),\n getValue: () => viewRef.current?.state.doc.toString() ?? \"\",\n setValue: (text) => {\n const v = viewRef.current;\n if (!v) return;\n v.dispatch({ changes: { from: 0, to: v.state.doc.length, insert: String(text ?? \"\") } });\n },\n getSelection: () => {\n const v = viewRef.current;\n if (!v) return { from: 0, to: 0, text: \"\" };\n const { from, to } = v.state.selection.main;\n return { from, to, text: v.state.sliceDoc(from, to) };\n },\n replaceSelection: (text) => viewRef.current?.dispatch(viewRef.current.state.replaceSelection(String(text ?? \"\"))),\n insert: (text) => {\n const v = viewRef.current;\n if (!v) return;\n v.dispatch(v.state.replaceSelection(String(text ?? \"\")));\n v.focus();\n },\n select: (from, to = from) => {\n const v = viewRef.current;\n if (!v) return;\n const len = v.state.doc.length;\n v.dispatch({ selection: EditorSelection.range(Math.min(from, len), Math.min(to, len)), scrollIntoView: true });\n v.focus();\n },\n goTo: (line, col = 1) => {\n const v = viewRef.current;\n if (!v) return;\n const at = offsetOf(v.state.doc.toString(), line, col);\n v.dispatch({ selection: EditorSelection.cursor(at), scrollIntoView: true });\n v.focus();\n },\n statementAtCursor: () => {\n const v = viewRef.current;\n if (!v) return null;\n return statementAt(v.state.doc.toString(), v.state.selection.main.head);\n },\n run: (all = false) => (viewRef.current ? run(viewRef.current, all) : false),\n format: () => (viewRef.current ? format(viewRef.current) : false),\n cursor: () => {\n const v = viewRef.current;\n return v ? lineCol(v.state.doc.toString(), v.state.selection.main.head) : { line: 1, col: 1 };\n },\n }),\n // run/format read live props through refs.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [],\n );\n useEffect(() => {\n if (!editorRef) return;\n editorRef.current = handle;\n return () => {\n if (editorRef.current === handle) editorRef.current = null;\n };\n }, [editorRef, handle]);\n\n return (\n <div className={cx(FRAME, FLOATING, frameless && \"rounded-none border-0 bg-transparent focus-within:ring-0\", className)}>\n {/* raw: CodeMirror's mount point — it draws its own accessible textbox inside. */}\n <div ref={host} className=\"min-h-0 min-w-0 flex-1\" />\n {footer ? (\n <StatusBar\n language={language}\n line={status.line}\n col={status.col}\n selected={status.selected}\n problems={status.problems}\n onProblems={() => {\n const v = viewRef.current;\n if (v) {\n v.focus();\n nextDiagnostic(v);\n }\n }}\n onRun={onRun ? () => viewRef.current && run(viewRef.current) : null}\n runLabel={runLabel}\n readOnly={readOnly}\n />\n ) : null}\n </div>\n );\n}\n\n/** The quiet line under the editor: problems, where the cursor is, the\n * mode, and — when the editor runs — the Run button with its keys. */\nfunction StatusBar({ language, line, col, selected = 0, problems = 0, onProblems, onRun, runLabel = \"Run\", readOnly, idle = false }) {\n const [mac, setMac] = useState(true);\n useEffect(() => {\n setMac(/Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent || \"\"));\n }, []);\n return (\n <div className=\"flex min-h-9 items-center gap-3 border-t border-border ps-3 pe-1 text-xs text-muted-foreground\">\n {problems > 0 ? (\n <Button variant=\"quiet\" size=\"sm\" onClick={onProblems} disabled={idle} className=\"-ms-2 gap-1.5 px-2 text-xs text-foreground\">\n <span aria-hidden=\"true\" className=\"size-1.5 rounded-full bg-destructive\" />\n {problems === 1 ? \"1 problem\" : `${problems} problems`}\n </Button>\n ) : null}\n <span className=\"tabular-nums whitespace-nowrap\" aria-live=\"off\">\n Ln {line}, Col {col}\n {selected > 0 ? <span className=\"max-sm:hidden\"> · {selected} selected</span> : null}\n </span>\n <span className=\"ms-auto whitespace-nowrap max-sm:hidden\">\n {LANGUAGES[language] ?? language}\n {readOnly ? \" · Read only\" : \"\"}\n </span>\n {onRun ? (\n <Button variant=\"quiet\" size=\"sm\" onClick={onRun} disabled={idle} className=\"gap-2 px-2 text-xs text-foreground max-sm:ms-auto\">\n <Icon name=\"play\" size={14} />\n {runLabel}\n <KbdGroup keys={[mac ? \"⌘\" : \"Ctrl\", \"↵\"]} className=\"max-sm:hidden\" />\n </Button>\n ) : (\n <span className=\"w-1\" aria-hidden=\"true\" />\n )}\n </div>\n );\n}\n"
34
+ },
35
+ {
36
+ "path": "registry/blocks/code-editor/codemirror.js",
37
+ "type": "registry:component",
38
+ "target": "@components/makersclaw/blocks/code-editor/codemirror.js",
39
+ "content": "// The one door to CodeMirror 6 (MIT). Every file in this block reaches it\n// through here, and the blocks built on this one (json-editor,\n// sql-console…) reach it through CodeEditor — never the packages directly.\n//\n// The packages load on the server too (none of them touches `window`\n// while it loads — checked in node), so no ssr-shim is needed: the editor\n// itself is created only after the first effect (the mount guard in\n// code-editor.jsx), and the server renders the text as a placeholder.\nexport { EditorState, EditorSelection, Compartment, StateEffect, StateField, Prec } from \"@codemirror/state\";\nexport {\n EditorView,\n Decoration,\n keymap,\n lineNumbers,\n highlightActiveLineGutter,\n highlightSpecialChars,\n drawSelection,\n dropCursor,\n rectangularSelection,\n crosshairCursor,\n placeholder,\n} from \"@codemirror/view\";\nexport { defaultKeymap, history, historyKeymap, indentWithTab, insertNewlineAndIndent } from \"@codemirror/commands\";\nexport {\n HighlightStyle,\n syntaxHighlighting,\n indentOnInput,\n bracketMatching,\n foldGutter,\n foldKeymap,\n indentUnit,\n} from \"@codemirror/language\";\nexport { autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap, completeFromList } from \"@codemirror/autocomplete\";\nexport { searchKeymap, highlightSelectionMatches } from \"@codemirror/search\";\nexport { linter, lintGutter, lintKeymap, forceLinting, diagnosticCount, nextDiagnostic, forEachDiagnostic } from \"@codemirror/lint\";\nexport { sql, PostgreSQL, MySQL, SQLite, StandardSQL, MSSQL, MariaSQL } from \"@codemirror/lang-sql\";\nexport { json } from \"@codemirror/lang-json\";\nexport { tags } from \"@lezer/highlight\";\n"
40
+ },
41
+ {
42
+ "path": "registry/blocks/code-editor/theme.js",
43
+ "type": "registry:component",
44
+ "target": "@components/makersclaw/blocks/code-editor/theme.js",
45
+ "content": "// CodeMirror in the workspace theme — the one place its look is mapped.\n//\n// CodeMirror styles itself with CSS it writes at run time, so its theme is\n// CSS too: every colour below is `var(--color-…)`, the workspace's own\n// token, read by the browser. Nothing is resolved in JavaScript, so a\n// change of theme or of light/dark repaints the editor with no work here.\n//\n// Syntax colours are categories, not status, so they come from the chart\n// hues (--color-chart-1 is the primary): keywords chart-1 and a little\n// heavier (in a theme whose primary is its ink, weight still sets them\n// apart), text in quotes chart-2, numbers and true/false/null chart-3,\n// functions chart-4, names of columns, keys and types chart-5. Comments\n// and punctuation step back into muted-foreground.\nimport { EditorView, HighlightStyle, tags as t } from \"./codemirror\";\n\nconst v = (token) => `var(--color-${token})`;\n\nexport const editorTheme = EditorView.theme({\n \"&\": {\n color: v(\"foreground\"),\n backgroundColor: \"transparent\",\n fontSize: \"var(--code-editor-size, 13px)\",\n },\n \"&.cm-focused\": { outline: \"none\" },\n \".cm-scroller\": {\n fontFamily: \"var(--font-mono)\",\n lineHeight: \"1.65\",\n overflow: \"auto\",\n },\n \".cm-content\": {\n padding: \"10px 0\",\n caretColor: v(\"foreground\"),\n },\n \".cm-line\": { padding: \"0 16px 0 12px\" },\n \".cm-cursor, .cm-dropCursor\": { borderLeftColor: v(\"foreground\"), borderLeftWidth: \"2px\" },\n // Selection sits under the text (drawSelection), so an opaque tint reads.\n \".cm-selectionBackground\": { backgroundColor: v(\"accent\") },\n \"&.cm-focused .cm-selectionBackground, &.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-content ::selection\": {\n backgroundColor: v(\"accent\"),\n },\n \".cm-selectionMatch\": { backgroundColor: v(\"muted\"), borderRadius: \"2px\" },\n \"&.cm-focused .cm-matchingBracket\": { backgroundColor: \"transparent\", outline: `1px solid ${v(\"muted-foreground\")}`, borderRadius: \"2px\" },\n \"&.cm-focused .cm-nonmatchingBracket\": { backgroundColor: \"transparent\", outline: `1px solid ${v(\"destructive\")}`, borderRadius: \"2px\" },\n \".cm-placeholder\": { color: v(\"muted-foreground\"), fontStyle: \"normal\" },\n\n // The gutter: quiet numbers, the current one in ink.\n \".cm-gutters\": {\n backgroundColor: \"transparent\",\n color: v(\"muted-foreground\"),\n border: \"none\",\n fontVariantNumeric: \"tabular-nums\",\n },\n \".cm-lineNumbers .cm-gutterElement\": { padding: \"0 4px 0 14px\", minWidth: \"36px\", opacity: \"0.7\" },\n \".cm-activeLineGutter\": { backgroundColor: \"transparent\", color: v(\"foreground\") },\n \".cm-activeLineGutter.cm-gutterElement\": { opacity: \"1\" },\n \".cm-foldGutter .cm-gutterElement\": { padding: \"0 2px\", cursor: \"pointer\" },\n \".cm-fold-marker\": {\n display: \"inline-grid\",\n placeItems: \"center\",\n width: \"14px\",\n height: \"100%\",\n color: v(\"muted-foreground\"),\n transition: \"transform 150ms ease, color 150ms ease\",\n },\n \".cm-fold-marker:hover\": { color: v(\"foreground\") },\n \".cm-fold-marker[data-open='false']\": { transform: \"rotate(-90deg)\" },\n \".cm-foldPlaceholder\": {\n backgroundColor: v(\"muted\"),\n border: \"none\",\n color: v(\"muted-foreground\"),\n borderRadius: \"4px\",\n padding: \"0 6px\",\n margin: \"0 2px\",\n },\n\n // Problems: a wavy line under the text, a dot in the gutter.\n \".cm-lintRange\": { backgroundImage: \"none\", textDecorationLine: \"underline\", textDecorationStyle: \"wavy\", textDecorationSkipInk: \"none\", textUnderlineOffset: \"3px\" },\n \".cm-lintRange-error\": { textDecorationColor: v(\"destructive\") },\n \".cm-lintRange-warning\": { textDecorationColor: v(\"warning\") },\n \".cm-lintRange-info, .cm-lintRange-hint\": { textDecorationColor: v(\"info\") },\n \".cm-gutter-lint\": { width: \"14px\" },\n \".cm-gutter-lint .cm-gutterElement\": { padding: \"0\", display: \"grid\", placeItems: \"center\" },\n \".cm-lint-marker\": { content: \"normal\", width: \"6px\", height: \"6px\", borderRadius: \"9999px\" },\n // CodeMirror sets its icon as `content: url(…)` on each kind; ours is the dot.\n \".cm-lint-marker-error\": { content: \"normal\", backgroundColor: v(\"destructive\") },\n \".cm-lint-marker-warning\": { content: \"normal\", backgroundColor: v(\"warning\") },\n \".cm-lint-marker-info\": { content: \"normal\", backgroundColor: v(\"info\") },\n\n // Every floating panel: the popover surface and a hairline (the shadow\n // is the kit's shadow-lg, set on the wrapper in code-editor.jsx).\n \".cm-tooltip\": {\n backgroundColor: v(\"popover\"),\n color: v(\"popover-foreground\"),\n border: `1px solid ${v(\"border\")}`,\n borderRadius: \"var(--radius-md)\",\n },\n \".cm-tooltip.cm-tooltip-autocomplete > ul\": {\n fontFamily: \"var(--font-mono)\",\n maxHeight: \"16em\",\n padding: \"4px\",\n },\n \".cm-tooltip.cm-tooltip-autocomplete > ul > li\": {\n padding: \"3px 8px\",\n borderRadius: \"4px\",\n lineHeight: \"1.6\",\n display: \"flex\",\n alignItems: \"center\",\n gap: \"6px\",\n },\n \".cm-tooltip.cm-tooltip-autocomplete > ul > li[aria-selected]\": {\n backgroundColor: v(\"accent\"),\n color: v(\"accent-foreground\"),\n },\n \".cm-completionLabel\": { flex: \"0 1 auto\" },\n \".cm-completionMatchedText\": { textDecoration: \"none\", fontWeight: \"600\", color: v(\"foreground\") },\n \".cm-completionDetail\": { marginLeft: \"auto\", paddingLeft: \"12px\", fontStyle: \"normal\", color: v(\"muted-foreground\"), fontSize: \"0.9em\" },\n \".cm-completionIcon\": { width: \"1.1em\", opacity: \"0.7\", paddingRight: \"0\", color: v(\"muted-foreground\"), fontSize: \"0.85em\" },\n \".cm-completionIcon-keyword:after\": { content: \"'K'\" },\n \".cm-completionIcon-type:after\": { content: \"'T'\" },\n \".cm-completionIcon-property:after\": { content: \"'·'\" },\n \".cm-completionIcon-namespace:after\": { content: \"'S'\" },\n \".cm-completionInfo\": {\n backgroundColor: v(\"popover\"),\n color: v(\"popover-foreground\"),\n border: `1px solid ${v(\"border\")}`,\n borderRadius: \"var(--radius-md)\",\n padding: \"6px 10px\",\n fontFamily: \"var(--font-sans)\",\n maxWidth: \"280px\",\n },\n \".cm-tooltip-lint\": { padding: \"2px\" },\n \".cm-diagnostic\": { fontFamily: \"var(--font-sans)\", padding: \"6px 10px\", borderLeft: \"3px solid transparent\", borderRadius: \"4px\" },\n \".cm-diagnostic-error\": { borderLeftColor: v(\"destructive\") },\n \".cm-diagnostic-warning\": { borderLeftColor: v(\"warning\") },\n \".cm-diagnostic-info, .cm-diagnostic-hint\": { borderLeftColor: v(\"info\") },\n\n // Find and replace (Cmd/Ctrl+F): our fields and buttons, not the default grey.\n \".cm-panels\": { backgroundColor: v(\"card\"), color: v(\"foreground\") },\n \".cm-panels.cm-panels-top\": { borderBottom: `1px solid ${v(\"border\")}` },\n \".cm-panels.cm-panels-bottom\": { borderTop: `1px solid ${v(\"border\")}` },\n \".cm-panel.cm-search\": { padding: \"8px 12px\", fontFamily: \"var(--font-sans)\", fontSize: \"13px\", display: \"flex\", flexWrap: \"wrap\", alignItems: \"center\", gap: \"6px\" },\n \".cm-panel.cm-search br\": { display: \"none\" },\n \".cm-panel.cm-search label\": { display: \"inline-flex\", alignItems: \"center\", gap: \"4px\", color: v(\"muted-foreground\") },\n \".cm-panel.cm-search [name=close]\": { position: \"static\", marginLeft: \"auto\", color: v(\"muted-foreground\"), fontSize: \"18px\", padding: \"0 6px\" },\n \".cm-textfield\": {\n backgroundColor: v(\"background\"),\n color: v(\"foreground\"),\n border: `1px solid ${v(\"input\")}`,\n borderRadius: \"var(--radius-sm)\",\n padding: \"4px 8px\",\n margin: \"0\",\n fontSize: \"13px\",\n },\n \".cm-textfield:focus\": { outline: `2px solid ${v(\"ring\")}`, outlineOffset: \"0\" },\n \".cm-button\": {\n backgroundImage: \"none\",\n backgroundColor: v(\"secondary\"),\n color: v(\"secondary-foreground\"),\n border: `1px solid ${v(\"border\")}`,\n borderRadius: \"var(--radius-sm)\",\n padding: \"4px 10px\",\n margin: \"0\",\n fontSize: \"13px\",\n },\n \".cm-button:hover\": { backgroundColor: v(\"accent\") },\n \".cm-button:active\": { backgroundImage: \"none\" },\n \".cm-searchMatch\": { backgroundColor: v(\"muted\"), outline: `1px solid ${v(\"border\")}`, borderRadius: \"2px\" },\n \".cm-searchMatch.cm-searchMatch-selected\": { backgroundColor: v(\"accent\"), outline: `1px solid ${v(\"muted-foreground\")}` },\n\n // The statement that just ran, lit for a moment.\n \".cm-ran\": { backgroundColor: v(\"accent\"), borderRadius: \"2px\", transition: \"background-color 400ms ease\" },\n});\n\nexport const highlightStyle = HighlightStyle.define([\n { tag: [t.keyword, t.operatorKeyword, t.modifier, t.controlKeyword, t.definitionKeyword], color: v(\"chart-1\"), fontWeight: \"600\" },\n { tag: [t.string, t.special(t.string), t.regexp], color: v(\"chart-2\") },\n { tag: [t.number, t.bool, t.null, t.atom], color: v(\"chart-3\") },\n { tag: [t.function(t.variableName), t.function(t.propertyName), t.standard(t.name)], color: v(\"chart-4\") },\n { tag: [t.propertyName, t.typeName, t.className, t.special(t.name), t.labelName], color: v(\"chart-5\") },\n { tag: [t.comment, t.lineComment, t.blockComment], color: v(\"muted-foreground\"), fontStyle: \"italic\" },\n { tag: [t.punctuation, t.separator, t.bracket, t.operator, t.derefOperator], color: v(\"muted-foreground\") },\n { tag: t.invalid, color: v(\"destructive\") },\n { tag: t.strong, fontWeight: \"600\" },\n { tag: t.emphasis, fontStyle: \"italic\" },\n { tag: t.link, textDecoration: \"underline\" },\n]);\n"
46
+ },
47
+ {
48
+ "path": "registry/blocks/code-editor/sql.js",
49
+ "type": "registry:component",
50
+ "target": "@components/makersclaw/blocks/code-editor/sql.js",
51
+ "content": "// SQL helpers: statements, and the schema autocomplete reads.\n//\n// Plain functions of strings and objects, so they run in node and on the\n// server (sql-console runs `splitStatements` before it calls the database).\n\n/**\n * The statements in a script, in order: [{ from, to, text }]. `from`/`to`\n * are offsets into `sql` (the statement's own text, trimmed, without its\n * `;`). A `;` inside a string ('it''s'), a quoted name (\"a;b\", `a;b`), a\n * comment (-- …, /* … *\\/) or a Postgres dollar-quoted body ($$ … $$,\n * $fn$ … $fn$) does not end a statement. A stretch holding only comments\n * or whitespace is not a statement. `opts.backslash: true` also reads \\'\n * as an escaped quote inside '…' (MySQL); Postgres and SQLite do not.\n */\nexport function splitStatements(sql, opts = {}) {\n const s = String(sql ?? \"\");\n const out = [];\n let start = 0;\n let i = 0;\n let meaningful = false;\n const push = (end) => {\n if (meaningful) {\n let a = start;\n let b = end;\n while (a < b && /\\s/.test(s[a])) a++;\n while (b > a && /\\s/.test(s[b - 1])) b--;\n out.push({ from: a, to: b, text: s.slice(a, b) });\n }\n meaningful = false;\n };\n while (i < s.length) {\n const c = s[i];\n const n = s[i + 1];\n if (c === \"-\" && n === \"-\") {\n const e = s.indexOf(\"\\n\", i);\n i = e < 0 ? s.length : e + 1;\n continue;\n }\n if (c === \"/\" && n === \"*\") {\n const e = s.indexOf(\"*/\", i + 2);\n i = e < 0 ? s.length : e + 2;\n continue;\n }\n if (c === \"'\" || c === '\"' || c === \"`\") {\n meaningful = true;\n i++;\n while (i < s.length) {\n if (s[i] === c) {\n if (s[i + 1] === c) i += 2; // '' or \"\" is an escaped quote\n else break;\n } else if (s[i] === \"\\\\\" && c === \"'\" && opts.backslash) i += 2;\n else i++;\n }\n i++;\n continue;\n }\n if (c === \"$\") {\n const m = /^\\$([A-Za-z_][A-Za-z0-9_]*)?\\$/.exec(s.slice(i, i + 64));\n if (m) {\n meaningful = true;\n const tag = m[0];\n const e = s.indexOf(tag, i + tag.length);\n i = e < 0 ? s.length : e + tag.length;\n continue;\n }\n }\n if (c === \";\") {\n push(i);\n i++;\n start = i;\n continue;\n }\n if (!/\\s/.test(c)) meaningful = true;\n i++;\n }\n push(s.length);\n return out;\n}\n\n/**\n * The statement to run for a cursor at `pos`: the one the cursor is in,\n * or — when it sits between statements — the one it follows on the same\n * line (just after a `;`), else the next one, else the last. Null when the\n * script has no statement. `opts.backslash`: as splitStatements.\n */\nexport function statementAt(sql, pos, opts = {}) {\n const s = String(sql ?? \"\");\n const all = splitStatements(s, opts);\n if (!all.length) return null;\n const at = Math.max(0, Math.min(s.length, pos | 0));\n for (const st of all) if (at >= st.from && at <= st.to) return st;\n // Between statements: the previous one if nothing but `;`/spaces/comment\n // on the same line separates them.\n let prev = null;\n for (const st of all) if (st.to <= at) prev = st;\n if (prev && !s.slice(prev.to, at).includes(\"\\n\")) return prev;\n return all.find((st) => st.from >= at) ?? prev ?? all[all.length - 1];\n}\n\n/**\n * The tables autocomplete offers, as the editor's SQL mode wants them.\n *\n * tables [{ name, schema?, description?, columns: [\n * { name, type?, description?, primaryKey? } | \"name\" ] }]\n *\n * Returns { namespace, tables } for @codemirror/lang-sql's `schema`: a\n * table completes as a table (its description beside it), its columns\n * complete after `table.` and anywhere in a query that names it, each\n * with its type. Tables in a `schema` nest under it (`public.deals`).\n */\nexport function sqlNamespace(tables) {\n const namespace = {};\n const list = [];\n for (const t of tables ?? []) {\n if (!t || !t.name) continue;\n const columns = (t.columns ?? [])\n .map((c) => (typeof c === \"string\" ? { name: c } : c))\n .filter((c) => c && c.name)\n .map((c) => ({\n label: String(c.name),\n type: \"property\",\n detail: [c.type, c.primaryKey ? \"key\" : null].filter(Boolean).join(\" · \") || undefined,\n info: c.description || undefined,\n boost: c.primaryKey ? 1 : 0,\n }));\n const node = { self: { label: String(t.name), type: \"type\", detail: `${columns.length} column${columns.length === 1 ? \"\" : \"s\"}`, info: t.description || undefined }, children: columns };\n if (t.schema) {\n const holder = (namespace[t.schema] ??= { self: { label: String(t.schema), type: \"namespace\" }, children: {} });\n holder.children[t.name] = node;\n } else {\n namespace[t.name] = node;\n }\n list.push({ name: String(t.name), schema: t.schema ?? null, columns: columns.map((c) => c.label) });\n }\n return { namespace, tables: list };\n}\n"
52
+ },
53
+ {
54
+ "path": "registry/blocks/code-editor/json.js",
55
+ "type": "registry:component",
56
+ "target": "@components/makersclaw/blocks/code-editor/json.js",
57
+ "content": "// JSON helpers: a checker that says what is wrong in words people use, and\n// a formatter that keeps the text's own keys, order and numbers.\n//\n// Why our own parser: JSON.parse's messages differ by browser (\"Unexpected\n// token } in JSON at position 41\" in one, no position at all in another)\n// and never say how to fix the mistake. This one walks the text the same\n// everywhere and names the usual slips — a missing or trailing comma, a\n// key without quotes, single quotes, a comment. Plain functions: node and\n// the server run them too.\nimport { lineCol } from \"./text\";\n\nclass JsonSlip extends Error {\n constructor(message, from, to) {\n super(message);\n this.from = from;\n this.to = to;\n }\n}\n\nconst WS = new Set([\" \", \"\\t\", \"\\n\", \"\\r\"]);\nconst WORD = /[A-Za-z_$][\\w$]*/y;\nconst NUMBER = /-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?/y;\nconst LOOSE_NUMBER = /[-+]?(?:\\d[\\d_]*)?\\.?\\d*(?:[eE][+-]?\\d+)?/y;\n\n/**\n * Check a JSON text. { ok: true, value } or { ok: false, error: { message,\n * from, to, line, col } } — offsets into the text, line and col 1-based.\n * An empty text is an error (\"Nothing here yet…\") unless `allowEmpty`.\n */\nexport function checkJson(text, { allowEmpty = false } = {}) {\n const s = String(text ?? \"\");\n let i = 0;\n const opens = [];\n\n const slip = (message, from = i, to = from + 1) => {\n throw new JsonSlip(message, Math.min(from, s.length), Math.min(Math.max(to, from), s.length));\n };\n const skip = () => {\n for (;;) {\n while (i < s.length && WS.has(s[i])) i++;\n if (s[i] === \"/\" && (s[i + 1] === \"/\" || s[i + 1] === \"*\")) {\n const end = s[i + 1] === \"/\" ? s.indexOf(\"\\n\", i) : s.indexOf(\"*/\", i + 2) + 2;\n slip(\"JSON has no comments — remove this one\", i, end > i ? end : s.length);\n }\n return;\n }\n };\n const openedAt = (o) => `the ${o.ch === \"{\" ? \"object\" : \"list\"} opened on line ${lineCol(s, o.at).line}`;\n\n function string() {\n const start = i;\n i++;\n let out = \"\";\n while (i < s.length) {\n const c = s[i];\n if (c === '\"') {\n i++;\n return out;\n }\n if (c === \"\\n\") slip(\"This text is missing its closing quote\", start, i);\n if (c < \" \") slip(\"Tabs and line breaks inside text must be written \\\\t and \\\\n\", i, i + 1);\n if (c === \"\\\\\") {\n const e = s[i + 1];\n const map = { '\"': '\"', \"\\\\\": \"\\\\\", \"/\": \"/\", b: \"\\b\", f: \"\\f\", n: \"\\n\", r: \"\\r\", t: \"\\t\" };\n if (e in map) {\n out += map[e];\n i += 2;\n continue;\n }\n if (e === \"u\" && /^[0-9a-fA-F]{4}$/.test(s.slice(i + 2, i + 6))) {\n out += String.fromCharCode(parseInt(s.slice(i + 2, i + 6), 16));\n i += 6;\n continue;\n }\n slip(`\"\\\\${e ?? \"\"}\" is not an escape JSON knows — write \\\\\\\\ for a backslash`, i, i + 2);\n }\n out += c;\n i++;\n }\n slip(\"This text is missing its closing quote\", start, s.length);\n }\n\n function value() {\n skip();\n if (i >= s.length) {\n const o = opens[opens.length - 1];\n slip(o ? `The JSON ends too soon — close ${openedAt(o)}` : \"Nothing here yet — start with { } or [ ]\", s.length, s.length);\n }\n const c = s[i];\n if (c === \"{\") return object();\n if (c === \"[\") return array();\n if (c === '\"') return string();\n if (c === \"'\") slip(\"Use double quotes (\\\") for text, not single quotes\", i, closingSingle(i));\n NUMBER.lastIndex = i;\n const num = NUMBER.exec(s);\n if (num && num[0] && num[0] !== \"-\") {\n const after = s[i + num[0].length];\n if (!after || !/[\\w.]/.test(after)) {\n i += num[0].length;\n return Number(num[0]);\n }\n }\n if (/[-+.\\d]/.test(c)) {\n LOOSE_NUMBER.lastIndex = i;\n const loose = LOOSE_NUMBER.exec(s)?.[0] || c;\n slip(`\"${loose}\" is not a number JSON can read — no leading zeros, + signs or trailing dots`, i, i + loose.length);\n }\n WORD.lastIndex = i;\n const w = WORD.exec(s)?.[0];\n if (w === \"true\" || w === \"false\" || w === \"null\") {\n i += w.length;\n return w === \"true\" ? true : w === \"false\" ? false : null;\n }\n if (w) {\n const lower = w.toLowerCase();\n if (lower === \"true\" || lower === \"false\" || lower === \"null\") slip(`Write ${lower} in lower case`, i, i + w.length);\n if (w === \"undefined\" || w === \"NaN\" || w === \"Infinity\") slip(`${w} is not JSON — use null`, i, i + w.length);\n slip(`Text must be in double quotes: \"${w}\"`, i, i + w.length);\n }\n if (c === \"}\" || c === \"]\") slip(`A value is missing before this ${c}`, i);\n if (c === \",\") slip(\"A value is missing before this comma\", i);\n slip(`\"${c}\" cannot start a value`, i);\n }\n\n function closingSingle(from) {\n const end = s.indexOf(\"'\", from + 1);\n const nl = s.indexOf(\"\\n\", from + 1);\n return end > 0 && (nl < 0 || end < nl) ? end + 1 : from + 1;\n }\n\n function object() {\n opens.push({ ch: \"{\", at: i });\n i++;\n const out = {};\n skip();\n if (s[i] === \"}\") {\n i++;\n opens.pop();\n return out;\n }\n for (;;) {\n skip();\n if (i >= s.length) slip(`The JSON ends too soon — close ${openedAt(opens[opens.length - 1])} with }`, s.length, s.length);\n const c = s[i];\n if (c === \"}\") slip(\"Remove this trailing comma\", lastComma(i), lastComma(i) + 1);\n if (c === \"'\") slip(\"Keys need double quotes, not single quotes\", i, closingSingle(i));\n if (c !== '\"') {\n WORD.lastIndex = i;\n const w = WORD.exec(s)?.[0];\n if (w) slip(`Keys need double quotes: \"${w}\"`, i, i + w.length);\n slip(\"A key in double quotes goes here\", i);\n }\n const key = string();\n skip();\n if (s[i] !== \":\") slip(i >= s.length ? \"The JSON ends too soon — a : and a value go here\" : `A colon (:) goes between \"${key}\" and its value`, i, i + 1);\n i++;\n out[key] = value(); // a duplicate key: the last one wins, as in JSON.parse\n skip();\n if (s[i] === \",\") {\n i++;\n continue;\n }\n if (s[i] === \"}\") {\n i++;\n opens.pop();\n return out;\n }\n if (i >= s.length) slip(`The JSON ends too soon — close ${openedAt(opens[opens.length - 1])} with }`, s.length, s.length);\n if (s[i] === \"]\") slip(`This ] closes a list, but ${openedAt(opens[opens.length - 1])} needs a }`, i);\n slip(\"A comma is missing before this\", i, tokenEnd(i));\n }\n }\n\n function array() {\n opens.push({ ch: \"[\", at: i });\n i++;\n const out = [];\n skip();\n if (s[i] === \"]\") {\n i++;\n opens.pop();\n return out;\n }\n for (;;) {\n skip();\n if (s[i] === \"]\" && out.length) slip(\"Remove this trailing comma\", lastComma(i), lastComma(i) + 1);\n out.push(value());\n skip();\n if (s[i] === \",\") {\n i++;\n continue;\n }\n if (s[i] === \"]\") {\n i++;\n opens.pop();\n return out;\n }\n if (i >= s.length) slip(`The JSON ends too soon — close ${openedAt(opens[opens.length - 1])} with ]`, s.length, s.length);\n if (s[i] === \"}\") slip(`This } closes an object, but ${openedAt(opens[opens.length - 1])} needs a ]`, i);\n slip(\"A comma is missing before this\", i, tokenEnd(i));\n }\n }\n\n const lastComma = (before) => {\n let k = before - 1;\n while (k > 0 && s[k] !== \",\") k--;\n return k;\n };\n const tokenEnd = (from) => {\n if (s[from] === '\"') {\n const e = s.indexOf('\"', from + 1);\n return e > 0 ? e + 1 : from + 1;\n }\n let k = from;\n while (k < s.length && !WS.has(s[k]) && !\",:{}[]\".includes(s[k])) k++;\n return Math.max(k, from + 1);\n };\n\n try {\n skip();\n if (i >= s.length) {\n if (allowEmpty) return { ok: true, value: undefined, empty: true };\n slip(\"Nothing here yet — start with { } or [ ]\", 0, 0);\n }\n const v = value();\n skip();\n if (i < s.length) slip(opens.length ? \"Unexpected text\" : \"There is more after the end of the JSON — remove it, or wrap both in [ ]\", i, s.length);\n return { ok: true, value: v };\n } catch (e) {\n if (!(e instanceof JsonSlip)) throw e;\n const { line, col } = lineCol(s, e.from);\n return { ok: false, error: { message: e.message, from: e.from, to: e.to, line, col } };\n }\n}\n\n/**\n * Lay a JSON text out again, keeping its keys, their order and its numbers\n * exactly as written (JSON.stringify would reorder number-like keys and\n * round big numbers). `indent` 2 (default), a number of spaces or \"\\t\";\n * `indent: 0` minifies. Short arrays of plain values stay on one line when\n * they fit in `width` (80). Returns null when the text is not valid JSON.\n */\nexport function formatJson(text, { indent = 2, width = 80 } = {}) {\n const s = String(text ?? \"\");\n if (!checkJson(s).ok) return null;\n const tokens = tokenize(s);\n const pad = typeof indent === \"string\" ? indent : \" \".repeat(Math.max(0, indent | 0));\n if (!pad) return tokens.map((t) => t.v).join(\"\");\n let k = 0;\n function node(depth) {\n const t = tokens[k++];\n if (t.v !== \"{\" && t.v !== \"[\") return t.v;\n const close = t.v === \"{\" ? \"}\" : \"]\";\n if (tokens[k].v === close) {\n k++;\n return t.v + close;\n }\n const items = [];\n const plain = [];\n for (;;) {\n let key = \"\";\n if (t.v === \"{\") {\n key = tokens[k].v + \": \";\n k += 2;\n }\n const start = k;\n const v = node(depth + 1);\n plain.push(tokens[start].v !== \"{\" && tokens[start].v !== \"[\");\n items.push(key + v);\n const sep = tokens[k++].v;\n if (sep === close) break;\n }\n const inner = pad.repeat(depth + 1);\n if (t.v === \"[\" && plain.every(Boolean)) {\n const one = \"[\" + items.join(\", \") + \"]\";\n if (one.length + inner.length <= width) return one;\n }\n return t.v + \"\\n\" + items.map((x) => inner + x).join(\",\\n\") + \"\\n\" + pad.repeat(depth) + close;\n }\n return node(0);\n}\n\n/** A valid JSON text → its tokens: punctuation, strings, numbers, words. */\nfunction tokenize(s) {\n const out = [];\n let i = 0;\n while (i < s.length) {\n const c = s[i];\n if (WS.has(c)) {\n i++;\n continue;\n }\n if (\"{}[],:\".includes(c)) {\n out.push({ v: c });\n i++;\n continue;\n }\n if (c === '\"') {\n let j = i + 1;\n while (s[j] !== '\"') j += s[j] === \"\\\\\" ? 2 : 1;\n out.push({ v: s.slice(i, j + 1) });\n i = j + 1;\n continue;\n }\n let j = i;\n while (j < s.length && !WS.has(s[j]) && !\"{}[],:\".includes(s[j])) j++;\n out.push({ v: s.slice(i, j) });\n i = j;\n }\n return out;\n}\n\n/** A JS value → a JSON text, 2-space indented (the tree view's output). */\nexport function stringifyJson(value, indent = 2) {\n if (value === undefined) return \"\";\n return formatJson(JSON.stringify(value), { indent }) ?? JSON.stringify(value, null, indent);\n}\n"
58
+ },
59
+ {
60
+ "path": "registry/blocks/code-editor/text.js",
61
+ "type": "registry:component",
62
+ "target": "@components/makersclaw/blocks/code-editor/text.js",
63
+ "content": "// Positions in a text, and the problems (\"diagnostics\") drawn on it.\n//\n// Plain functions of strings, so they run in node and on the server. The\n// editor counts in offsets (0-based characters from the start); people and\n// databases count in lines and columns (1-based). Both are accepted here.\n\n/** Offset → { line, col }, both 1-based. */\nexport function lineCol(text, offset) {\n const s = String(text ?? \"\");\n const at = Math.max(0, Math.min(s.length, offset | 0));\n let line = 1;\n let start = 0;\n for (let i = 0; i < at; i++) {\n if (s.charCodeAt(i) === 10) {\n line++;\n start = i + 1;\n }\n }\n return { line, col: at - start + 1 };\n}\n\n/** { line, col } (1-based) → offset, clamped into the text. A missing col\n * is the start of the line. */\nexport function offsetOf(text, line, col = 1) {\n const s = String(text ?? \"\");\n let at = 0;\n for (let n = 1; n < Math.max(1, line | 0); n++) {\n const next = s.indexOf(\"\\n\", at);\n if (next < 0) return s.length;\n at = next + 1;\n }\n const end = s.indexOf(\"\\n\", at);\n const lineEnd = end < 0 ? s.length : end;\n return Math.min(lineEnd, at + Math.max(0, (col | 0) - 1));\n}\n\n/** The start and end offsets of line `line` (1-based). */\nexport function lineRange(text, line) {\n const s = String(text ?? \"\");\n const from = offsetOf(s, line, 1);\n const end = s.indexOf(\"\\n\", from);\n return { from, to: end < 0 ? s.length : end };\n}\n\nconst SEVERITIES = new Set([\"error\", \"warning\", \"info\", \"hint\"]);\n\n/**\n * The problems a caller hands the editor, made exact: each one gets\n * `from`/`to` offsets inside the text. A problem may say where it is as\n * { from, to } offsets, or\n * { line, col?, endLine?, endCol? } 1-based; no col = the whole line,\n * { position } a 1-based character position, as\n * Postgres reports an error\n * plus `message` and `severity` (\"error\" default · \"warning\" · \"info\").\n * An empty range is widened to the word under it (or one character), so\n * there is always something to underline.\n */\nexport function normalizeDiagnostics(text, list) {\n const s = String(text ?? \"\");\n const out = [];\n for (const d of list ?? []) {\n if (!d || !d.message) continue;\n let from;\n let to;\n if (Number.isFinite(d.from)) {\n from = d.from;\n to = Number.isFinite(d.to) ? d.to : d.from;\n } else if (Number.isFinite(d.position)) {\n from = d.position - 1;\n to = from;\n } else if (Number.isFinite(d.line)) {\n if (Number.isFinite(d.col)) {\n from = offsetOf(s, d.line, d.col);\n to = Number.isFinite(d.endLine) ? offsetOf(s, d.endLine, d.endCol ?? 1) : Number.isFinite(d.endCol) ? offsetOf(s, d.line, d.endCol) : from;\n } else {\n ({ from, to } = lineRange(s, d.line));\n }\n } else {\n from = 0;\n to = 0;\n }\n from = Math.max(0, Math.min(s.length, from));\n to = Math.max(from, Math.min(s.length, to));\n if (to === from) ({ from, to } = wordAt(s, from));\n out.push({ from, to, severity: SEVERITIES.has(d.severity) ? d.severity : \"error\", message: String(d.message), source: d.source });\n }\n return out;\n}\n\n/** The word (letters, digits, _ $ .) at `at`, else one character. */\nexport function wordAt(text, at) {\n const s = String(text ?? \"\");\n const W = /[\\w$.]/;\n let from = at;\n let to = at;\n while (from > 0 && W.test(s[from - 1])) from--;\n while (to < s.length && W.test(s[to])) to++;\n if (to === from) to = Math.min(s.length, from + 1);\n if (to === from && from > 0) from--;\n return { from, to };\n}\n"
64
+ },
65
+ {
66
+ "path": "registry/blocks/code-editor/README.md",
67
+ "type": "registry:file",
68
+ "target": "@components/makersclaw/blocks/code-editor/README.md",
69
+ "content": "# CodeEditor\n\nA code editor in the workspace theme. Three modes: **SQL** (keywords,\nfunctions, and your database's tables and columns in autocomplete),\n**JSON** (checked as you type; mistakes are underlined and said in plain\nwords — \"A comma is missing before this\", \"Keys need double quotes\") and\n**plain text**. Line numbers, find and replace (Cmd/Ctrl+F), folding in\nJSON, multiple cursors, and a run hook: Cmd/Ctrl+Enter runs the selection,\nor the SQL statement under the cursor. A quiet status line under the text\nshows problems (click to jump to the next one), the cursor's line and\ncolumn, the mode and — when there is a run hook — a Run button, which is\nhow a phone runs.\n\nIt is [CodeMirror 6](https://codemirror.net) (MIT) underneath. Every\ncolour is a workspace token read by the browser (`var(--color-…)`), so a\nchange of theme or of light/dark repaints it with no work. Syntax colours\ncome from the chart hues: keywords `chart-1` (the primary), text in\nquotes `chart-2`, numbers and true/false/null `chart-3`, functions\n`chart-4`, columns, keys and types `chart-5`; comments step back into\n`muted-foreground`. `json-editor` and `sql-console` are built on this\nblock: they import it, never `@codemirror/*`.\n\n```jsx\nimport { CodeEditor, useCodeEditor } from \"../components/blocks/code-editor\"; // a Makersclaw app (\"./blocks/code-editor\" from components/)\nimport { CodeEditor, useCodeEditor } from \"@/components/makersclaw/blocks/code-editor\"; // a shadcn project\n```\n\n`CodeEditor` is a client component: render it from a `\"use client\"`\ncomponent. The helpers (`splitStatements`, `statementAt`, `checkJson`,\n`formatJson`…) are plain functions and work on the server too.\n\n`makersclaw add code-editor` adds the CodeMirror packages\n(`@codemirror/state`, `view`, `commands`, `language`, `autocomplete`,\n`search`, `lint`, `lang-sql`, `lang-json`, and `@lezer/highlight` — pinned,\nexact) to package.json: run `npm install` after. They load with the page\n(no `import()`): the editor is created after the first render, and until\nthen — on the server — the box shows the text with its line numbers.\n\n## Props\n\n- `value` — the text, controlled. A new `value` that is not the editor's\n text replaces it (loading another query); pass back exactly what\n `onChange` gave you and nothing resets, the cursor included.\n- `defaultValue` — the starting text, uncontrolled. With neither, empty.\n- `onChange(text)` — every edit, with the whole text. Debounce a save\n yourself.\n- `language` — `\"text\"` (default) · `\"sql\"` · `\"json\"`.\n- `dialect` — SQL only: `\"postgres\"` (default) · `\"mysql\"` · `\"mariadb\"` ·\n `\"sqlite\"` · `\"mssql\"` · `\"standard\"`. Sets keywords, functions and\n quoting (and whether `\\'` escapes a quote when finding the statement to\n run: MySQL and MariaDB only).\n- `schema` — SQL only: the tables autocomplete offers.\n `[{ name, schema?, description?, columns: [{ name, type?, description?,\n primaryKey? } | \"name\"] }]`. A table completes with its column count\n beside it and its description in a side note; `deals.` offers its\n columns with their types (`uuid · key`); in a query that names a table,\n its columns complete bare. Tables with a `schema` nest under it\n (`auth.users`); `defaultSchema` (e.g. `\"public\"`) lets that schema's\n tables complete without the prefix. Change it and autocomplete follows.\n- `completions` — extra words to complete in any mode: strings, or\n `{ label, type?, detail?, info?, apply? }` (`type` picks the little icon:\n `\"variable\"`, `\"function\"`, `\"keyword\"`, `\"property\"`, `\"type\"`).\n Saved snippets, `{{variables}}`, function names.\n- `onRun({ text, statement, from, to, selection, all })` — Cmd/Ctrl+Enter\n and the Run button. `statement` is what to run: the selection when there\n is one; else, in SQL, the statement under the cursor (split on `;` —\n quotes, comments and `$$` bodies respected; a cursor just after a `;`\n runs the statement before it); else the whole text. Shift+Cmd/Ctrl+Enter\n runs everything (`all: true`). `from`/`to` are its offsets in `text`, so\n a caller can map an error position back. The range lights up for a\n moment. Without `onRun` there is no Run button and the keys do nothing.\n- `runLabel` — the button's word, default `\"Run\"` (`\"Run query\"`,\n `\"Send\"`).\n- `onSave(text)` — Cmd/Ctrl+S (the browser's save dialog is held back).\n Without it the keys are the browser's.\n- `diagnostics` — problems found outside the editor (a database error, a\n server-side check), drawn on the text: `[{ message, severity?, …where }]`\n where \"where\" is `{ from, to }` offsets, `{ line, col?, endLine?,\n endCol? }` (1-based; no `col` underlines the whole line) or\n `{ position }` (a 1-based character position, as Postgres reports\n `error.position`). `severity` is `\"error\"` (default), `\"warning\"` or\n `\"info\"`. They belong to the text they were found in: **they show until\n the text changes**, then clear themselves. Pass `[]` to clear sooner.\n- `lint(text) → problems` — a check of your own, run 300ms after typing\n stops; same shape as `diagnostics`; may be async.\n- `validate` — JSON only: `true` (default) checks the text as you type.\n- `readOnly` — `false` (default). `true` shows the text read-only: it can\n still be selected, searched and copied.\n- `lineNumbers` — `true` (default).\n- `wrap` — `false` (default): long lines scroll sideways. `true` wraps.\n- `placeholder` — shown while the text is empty: say what goes here\n (\"Write a query — try SELECT * FROM deals\").\n- `height` — a fixed height (px number or CSS length); the text scrolls\n inside. Without it the editor grows with its text from `minHeight`\n (120) to `maxHeight` (480), then scrolls.\n- `tabSize` — `2` (default). Tab indents; **Esc then Tab** leaves the\n editor, so it never traps the keyboard.\n- `autoFocus` — `false` (default).\n- `footer` — `true` (default) shows the status line. `false` hides it\n (a block that draws its own).\n- `frameless` — `false` (default). `true` drops the border and card fill,\n for an editor inside a panel that already has them.\n- `label` — what a screen reader calls the editor (default: the mode's\n name).\n- `editorRef` — a ref from `useCodeEditor()`; `.current` is the handle.\n- `className` — the frame's classes (a width, a margin, `h-full`).\n\n### The handle\n\n`const ed = useCodeEditor()`, `<CodeEditor editorRef={ed} />`, then:\n\n- `ed.current.getValue()` / `setValue(text)`\n- `getSelection()` → `{ from, to, text }` · `select(from, to?)` ·\n `replaceSelection(text)` · `insert(text)` (at the cursor, then focus)\n- `goTo(line, col?)` — move the cursor there, scroll, focus (a results\n panel's \"show me the error\")\n- `cursor()` → `{ line, col }`\n- `statementAtCursor()` → `{ from, to, text }` or null\n- `run(all?)` — as the keys; `format()` — JSON only, lays it out again\n (also Shift+Alt+F)\n- `focus()`; `view` — the CodeMirror view, for the rare thing no prop does\n\n### Keys\n\nCmd/Ctrl+Enter run · Shift+Cmd/Ctrl+Enter run all · Cmd/Ctrl+S save ·\nCmd/Ctrl+F find · Cmd/Ctrl+Z / Shift+Cmd/Ctrl+Z undo/redo ·\nCtrl+Space complete · Cmd/Ctrl+/ comment · Alt+drag a column ·\nShift+Alt+F format JSON · Esc, Tab leave the editor.\n\n## The helpers\n\nPlain functions, on the server too.\n\n- `splitStatements(sql, { backslash? })` → `[{ from, to, text }]` — the\n statements in a script, trimmed, without their `;`. Comment-only\n stretches are not statements.\n- `statementAt(sql, pos, { backslash? })` → the statement to run for a\n cursor at `pos`, or null.\n- `sqlNamespace(tables)` → `{ namespace, tables }` — the `schema` prop as\n the SQL mode reads it (you do not need it to use the editor).\n- `checkJson(text, { allowEmpty? })` → `{ ok: true, value }` or\n `{ ok: false, error: { message, from, to, line, col } }` — the same\n words in every browser.\n- `formatJson(text, { indent = 2, width = 80 })` → the text laid out\n again, keys, order and numbers exactly as written (`indent: 0`\n minifies); null when it is not valid JSON.\n- `stringifyJson(value)` → a value as 2-space JSON, short lists on one\n line.\n- `lineCol(text, offset)`, `offsetOf(text, line, col)`,\n `normalizeDiagnostics(text, problems)` — positions both ways.\n\n## Example\n\n```jsx\n\"use client\";\nimport { useState } from \"react\";\nimport { CodeEditor, useCodeEditor } from \"../components/blocks/code-editor\";\n\nconst TABLES = [\n { name: \"deals\", description: \"Every open and closed deal\", columns: [\n { name: \"id\", type: \"uuid\", primaryKey: true },\n { name: \"account_id\", type: \"uuid\" },\n { name: \"amount\", type: \"numeric\" },\n { name: \"stage\", type: \"text\", description: \"lead · qualified · proposal · won · lost\" },\n { name: \"closed_at\", type: \"timestamptz\" },\n ] },\n { name: \"accounts\", columns: [{ name: \"id\", type: \"uuid\", primaryKey: true }, { name: \"name\", type: \"text\" }] },\n];\n\nexport function QueryBox({ runQuery }) {\n const [sql, setSql] = useState(\"SELECT stage, sum(amount)\\nFROM deals\\nGROUP BY stage;\");\n const [problems, setProblems] = useState([]);\n const ed = useCodeEditor();\n\n async function run({ statement, from }) {\n const r = await runQuery(statement); // your server action: { ok, rows } or { ok: false, error, position }\n if (r.ok) setProblems([]);\n else setProblems([{ position: from + (r.position ?? 1), message: r.error }]);\n }\n\n return (\n <CodeEditor\n language=\"sql\"\n schema={TABLES}\n value={sql}\n onChange={setSql}\n onRun={run}\n diagnostics={problems}\n placeholder=\"Write a query — try SELECT * FROM deals\"\n editorRef={ed}\n />\n );\n}\n```\n\nJSON, read-only, fixed height:\n\n```jsx\n<CodeEditor language=\"json\" value={JSON.stringify(payload, null, 2)} readOnly height={240} />\n```\n\n## What it will not do\n\n- It is not an IDE: no language server, no go-to-definition, no type\n checking. SQL is checked by running it (hand the database's error back\n through `diagnostics`); JSON is checked here.\n- Modes are SQL, JSON and plain text. Another language is another mode\n in this block, not a package a screen imports.\n- It never runs anything: `onRun` hands you the statement.\n- No live collaboration (that needs a sync server).\n"
70
+ }
71
+ ],
72
+ "categories": [
73
+ "code",
74
+ "editor",
75
+ "codemirror",
76
+ "sql",
77
+ "query",
78
+ "json",
79
+ "syntax highlighting",
80
+ "autocomplete",
81
+ "lint",
82
+ "monospace",
83
+ "console",
84
+ "script",
85
+ "config"
86
+ ],
87
+ "docs": "Installed to components/makersclaw/blocks/code-editor. Import: import { CodeEditor, useCodeEditor, LANGUAGES, SQL_DIALECTS, splitStatements, statementAt, sqlNamespace, checkJson, formatJson, stringifyJson, lineCol, offsetOf, normalizeDiagnostics } from \"@/components/makersclaw/blocks/code-editor\". Props and an example: components/makersclaw/blocks/code-editor/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
88
+ "meta": {
89
+ "makersclaw": {
90
+ "version": "0.3.0",
91
+ "useWhen": "A screen edits code or structured text: a SQL console, a query in a report, a JSON config or payload, a webhook body, a template, a script. json-editor and sql-console are built on it. Not for prose (doc-editor), one line of text (the kit's Input), or showing a diagram (diagram)."
92
+ }
93
+ }
94
+ }
@@ -0,0 +1,64 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "comment-box",
4
+ "type": "registry:block",
5
+ "title": "CommentBox",
6
+ "description": "A minimal rich comment field (DocEditor in minimal mode): bold, italic, links and lists from the selection toolbar or markdown shortcuts, @mentions as chips, Cmd/Ctrl+Enter to post. It posts through an async onPost(doc) that answers { ok }, keeps the draft and says why when it fails, and clears when it lands. With `comments` it draws the thread above it: avatar, name, relative time, the rich body. Also exports the composer alone, the read-only body renderer and the timestamp.",
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/doc-editor.json"
10
+ ],
11
+ "dependencies": [],
12
+ "files": [
13
+ {
14
+ "path": "registry/blocks/comment-box/index.js",
15
+ "type": "registry:component",
16
+ "target": "@components/makersclaw/blocks/comment-box/index.js",
17
+ "content": "// The CommentBox block — `makersclaw add comment-box`. Fixed: compose it\n// through its props (README.md beside this file). CommentComposer is the\n// field alone, CommentBody draws a posted doc, CommentTime a timestamp;\n// record-notes is built from them. The editor is doc-editor's, in its\n// minimal mode.\nexport { CommentBox, CommentComposer } from \"./comment-box\";\nexport { CommentBody } from \"./comment-body\";\nexport { CommentTime } from \"./comment-time\";\n"
18
+ },
19
+ {
20
+ "path": "registry/blocks/comment-box/comment-box.jsx",
21
+ "type": "registry:component",
22
+ "target": "@components/makersclaw/blocks/comment-box/comment-box.jsx",
23
+ "content": "\"use client\";\n\n// CommentBox — a minimal rich comment field (DocEditor in minimal mode)\n// with @mentions and Cmd/Ctrl+Enter to post, and, when `comments` is\n// given, the thread above it. CommentComposer is the field alone: record-\n// notes uses it to add a note and to edit one in place.\n//\n// A post goes out through an async callback that answers { ok }. While it\n// runs the box is locked and Post spins; a refusal (or a throw) keeps the\n// draft and says why under the box. Only a post that came back ok clears.\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { Avatar, Button, KbdGroup, cx } from \"../../ui\";\nimport { DocEditor, isDocEmpty, useDocEditor } from \"../doc-editor\";\nimport { CommentBody } from \"./comment-body\";\nimport { CommentTime } from \"./comment-time\";\n\nconst nameOf = (author) => (typeof author === \"string\" ? author : author?.name) || \"Someone\";\nconst srcOf = (author) => (typeof author === \"object\" && author ? author.src : undefined);\n\n/** ⌘ on a Mac, Ctrl elsewhere — known only in the browser. */\nfunction useModKey() {\n const [mac, setMac] = useState(null);\n useEffect(() => {\n const p = navigator.userAgentData?.platform || navigator.platform || \"\";\n setMac(/mac|iphone|ipad/i.test(p));\n }, []);\n return mac === null ? null : mac ? \"⌘\" : \"Ctrl\";\n}\n\nexport function CommentComposer({\n onSubmit,\n author,\n mentions,\n defaultValue,\n placeholder = \"Write a comment…\",\n submitLabel = \"Post\",\n submitIcon = \"send\",\n busyLabel,\n failedText = \"Couldn't post. Your comment is still here; try again.\",\n onCancel,\n cancelLabel = \"Cancel\",\n label = \"Comment\",\n autoFocus = false,\n disabled = false,\n clearOnSubmit = true,\n className,\n}) {\n const box = useDocEditor();\n const [draft, setDraft] = useState(defaultValue ?? null);\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState(null);\n const running = useRef(false);\n const mod = useModKey();\n const errorId = useId();\n const empty = !draft || isDocEmpty(draft);\n\n async function submit(doc) {\n if (running.current || disabled || !doc || isDocEmpty(doc)) return;\n running.current = true;\n setBusy(true);\n setError(null);\n let res;\n try {\n res = await onSubmit?.(doc);\n } catch (e) {\n res = { ok: false, error: e?.message };\n }\n running.current = false;\n setBusy(false);\n if (res && res.ok === false) {\n setError(res.error || failedText);\n box.current?.focus();\n return;\n }\n if (clearOnSubmit) {\n box.current?.clear();\n setDraft(null);\n }\n box.current?.focus();\n }\n\n return (\n <div className={cx(\"flex items-start gap-3\", className)}>\n {author ? (\n <div className=\"mt-1 hidden shrink-0 sm:block\">\n <Avatar name={nameOf(author)} src={srcOf(author)} size=\"sm\" />\n </div>\n ) : null}\n <div className=\"min-w-0 flex-1\">\n <div\n className={cx(\n \"rounded-md border bg-background px-3 py-2 focus-within:ring-2 focus-within:ring-ring\",\n error ? \"border-destructive\" : \"border-input\",\n (busy || disabled) && \"opacity-60\",\n )}\n aria-describedby={error ? errorId : undefined}\n >\n <DocEditor\n minimal\n editorRef={box}\n defaultValue={defaultValue}\n onChange={(d) => {\n setDraft(d);\n if (error) setError(null);\n }}\n onSubmit={submit}\n mentions={mentions}\n placeholder={placeholder}\n label={label}\n autoFocus={autoFocus}\n editable={!busy && !disabled}\n />\n </div>\n {error ? (\n <p id={errorId} role=\"alert\" className=\"mt-1.5 text-xs text-destructive\">\n {error}\n </p>\n ) : null}\n <div className=\"mt-2 flex flex-wrap items-center justify-end gap-2\">\n {mod ? (\n <span className=\"me-auto hidden items-center gap-1.5 text-xs text-muted-foreground sm:inline-flex\">\n <KbdGroup keys={[mod, \"Enter\"]} />\n <span>to {submitLabel.toLowerCase()}</span>\n </span>\n ) : null}\n {onCancel ? (\n <Button variant=\"quiet\" onClick={onCancel} disabled={busy}>\n {cancelLabel}\n </Button>\n ) : null}\n <Button variant=\"primary\" icon={submitIcon || undefined} loading={busy} disabled={empty || disabled} onClick={() => submit(box.current?.getDoc() ?? draft)}>\n {busy && busyLabel ? busyLabel : submitLabel}\n </Button>\n </div>\n </div>\n </div>\n );\n}\n\n/** One comment in the thread: avatar, name, time, the body. */\nfunction CommentItem({ comment }) {\n const name = nameOf(comment.author);\n return (\n <li className=\"flex gap-3\">\n <Avatar name={name} src={srcOf(comment.author)} size=\"sm\" className=\"shrink-0\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"flex flex-wrap items-baseline gap-x-2 text-sm\">\n <span className=\"font-medium\">{name}</span>\n <CommentTime date={comment.createdAt} />\n {comment.edited ? <span className=\"text-xs text-muted-foreground\">(edited)</span> : null}\n </p>\n <CommentBody doc={comment.body} className=\"mt-0.5 text-foreground\" />\n </div>\n </li>\n );\n}\n\nexport function CommentBox({\n onPost,\n comments,\n author,\n mentions,\n placeholder = \"Write a comment…\",\n postLabel = \"Post\",\n label = \"Comment\",\n emptyText = \"No comments yet.\",\n threadLabel = \"Comments\",\n autoFocus = false,\n disabled = false,\n className,\n}) {\n const list = Array.isArray(comments) ? comments : null;\n return (\n <div className={cx(\"grid gap-4\", className)}>\n {list ? (\n list.length ? (\n <ul aria-label={threadLabel} className=\"grid gap-4\">\n {list.map((c, i) => (\n <CommentItem key={c.id ?? i} comment={c} />\n ))}\n </ul>\n ) : (\n <p className=\"text-sm text-muted-foreground\">{emptyText}</p>\n )\n ) : null}\n <CommentComposer\n onSubmit={onPost}\n author={author}\n mentions={mentions}\n placeholder={placeholder}\n submitLabel={postLabel}\n label={label}\n autoFocus={autoFocus}\n disabled={disabled}\n />\n </div>\n );\n}\n"
24
+ },
25
+ {
26
+ "path": "registry/blocks/comment-box/comment-body.jsx",
27
+ "type": "registry:component",
28
+ "target": "@components/makersclaw/blocks/comment-box/comment-body.jsx",
29
+ "content": "// CommentBody — a posted doc, drawn read-only as plain React.\n//\n// Why not a read-only DocEditor: a thread is many small docs, and one\n// BlockNote editor per comment is heavy and paints only after mount. This\n// walks the same JSON (the shapes doc-editor's README lists) and renders\n// on the server too. Mentions and fields look like the editor's chips.\n// A block type it does not know shows its text, never nothing.\nimport { Fragment } from \"react\";\nimport { Icon, Separator, cx } from \"../../ui\";\n\nconst noText = (content) => inlineOf(content).every((c) => c?.type === \"text\" && !(c.text ?? \"\").trim());\n\nconst CHIP = \"inline-flex items-baseline rounded-sm px-1 font-medium whitespace-nowrap\";\n\n/** The inline pieces of a block's content, whatever shape it came in. */\nfunction inlineOf(content) {\n if (typeof content === \"string\") return [{ type: \"text\", text: content, styles: {} }];\n return Array.isArray(content) ? content : [];\n}\n\nfunction styled(text, styles, key) {\n let node = text;\n const s = styles ?? {};\n if (s.code) node = <code className=\"rounded-sm bg-muted px-1 font-mono text-[0.9em]\">{node}</code>;\n if (s.bold) node = <strong className=\"font-semibold\">{node}</strong>;\n if (s.italic) node = <em>{node}</em>;\n if (s.underline) node = <u>{node}</u>;\n if (s.strike) node = <s>{node}</s>;\n return <Fragment key={key}>{node}</Fragment>;\n}\n\nconst safeHref = (href) => (typeof href === \"string\" && /^(https?:|mailto:|tel:|\\/|#)/i.test(href.trim()) ? href.trim() : undefined);\n\nfunction Inline({ content }) {\n return inlineOf(content).map((c, i) => {\n if (!c || typeof c !== \"object\") return null;\n if (c.type === \"text\") return styled(c.text ?? \"\", c.styles, i);\n if (c.type === \"link\")\n return (\n <a key={i} href={safeHref(c.href)} target=\"_blank\" rel=\"noreferrer noopener\" className=\"text-primary underline underline-offset-2\">\n <Inline content={c.content} />\n </a>\n );\n if (c.type === \"mention\")\n return (\n <span key={i} className={cx(CHIP, \"bg-primary/10 text-primary\")} data-mention={c.props?.id}>\n @{c.props?.label || c.props?.id}\n </span>\n );\n if (c.type === \"field\")\n return (\n <span key={i} className={cx(CHIP, \"border border-dashed border-border bg-muted text-muted-foreground\")} title={`{${c.props?.key ?? \"\"}}`}>\n {c.props?.label || c.props?.key}\n </span>\n );\n return Array.isArray(c.content) ? <Inline key={i} content={c.content} /> : null;\n });\n}\n\nconst LISTS = { bulletListItem: \"ul\", numberedListItem: \"ol\", checkListItem: \"ul\", toggleListItem: \"ul\" };\nconst LIST_CLASS = {\n bulletListItem: \"list-disc ps-5\",\n numberedListItem: \"list-decimal ps-5\",\n checkListItem: \"\",\n toggleListItem: \"list-disc ps-5\",\n};\n\nfunction Children({ blocks }) {\n return Array.isArray(blocks) && blocks.length ? (\n <div className=\"mt-1 grid gap-1 ps-4\">\n <Blocks blocks={blocks} />\n </div>\n ) : null;\n}\n\nfunction ListItem({ block }) {\n if (block.type === \"checkListItem\") {\n const done = !!block.props?.checked;\n return (\n <li className=\"flex items-start gap-2\">\n <Icon name={done ? \"check-circle\" : \"circle\"} size={16} className={cx(\"mt-0.5 shrink-0\", done ? \"text-primary\" : \"text-muted-foreground\")} />\n <span className=\"sr-only\">{done ? \"Done: \" : \"To do: \"}</span>\n <div className={cx(\"min-w-0\", done && \"text-muted-foreground line-through\")}>\n <Inline content={block.content} />\n <Children blocks={block.children} />\n </div>\n </li>\n );\n }\n return (\n <li>\n <Inline content={block.content} />\n <Children blocks={block.children} />\n </li>\n );\n}\n\nfunction tableRows(content) {\n return (content?.rows ?? []).map((r) => (r?.cells ?? []).map((cell) => cell?.content ?? cell));\n}\n\nfunction Block({ block }) {\n const { type, props } = block;\n switch (type) {\n case \"heading\":\n return (\n <p className={cx(\"font-semibold\", props?.level === 1 ? \"text-base\" : \"text-sm\")}>\n <Inline content={block.content} />\n </p>\n );\n case \"quote\":\n return (\n <blockquote className=\"border-s-2 border-border ps-3 text-muted-foreground\">\n <Inline content={block.content} />\n </blockquote>\n );\n case \"codeBlock\":\n return (\n <pre className=\"overflow-x-auto rounded-md bg-muted p-2 font-mono text-xs\">\n <code>\n <Inline content={block.content} />\n </code>\n </pre>\n );\n case \"divider\":\n return <Separator className=\"my-1\" />;\n case \"image\": {\n const src = safeHref(props?.url);\n return src ? (\n <a href={src} target=\"_blank\" rel=\"noreferrer noopener\" className=\"inline-flex items-center gap-1.5 text-primary underline underline-offset-2\">\n <Icon name=\"image\" size={14} />\n {props?.caption || props?.name || \"Image\"}\n </a>\n ) : null;\n }\n case \"table\": {\n const rows = tableRows(block.content);\n if (!rows.length) return null;\n return (\n <div className=\"overflow-x-auto\">\n {/* raw: a doc's table is data drawn as it was typed, not a kit Table of records */}\n <table className=\"text-xs\">\n <tbody>\n {rows.map((r, i) => (\n <tr key={i} className=\"border-b border-border last:border-0\">\n {r.map((cell, j) => (\n <td key={j} className={cx(\"px-2 py-1 align-top\", i === 0 && \"font-medium\")}>\n <Inline content={cell} />\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n }\n default: {\n const empty = noText(block.content);\n return (\n <>\n <p className={empty ? \"min-h-5\" : undefined}>\n <Inline content={block.content} />\n </p>\n <Children blocks={block.children} />\n </>\n );\n }\n }\n}\n\n/** Consecutive list items of one kind become one list. */\nfunction Blocks({ blocks }) {\n const out = [];\n let run = null;\n (Array.isArray(blocks) ? blocks : []).forEach((b, i) => {\n if (!b || typeof b !== \"object\") return;\n const tag = LISTS[b.type];\n if (tag) {\n if (!run || run.type !== b.type) {\n run = { type: b.type, tag, items: [], key: b.id ?? `l${i}` };\n out.push(run);\n }\n run.items.push(b);\n return;\n }\n run = null;\n out.push(b);\n });\n return out.map((x, i) => {\n if (x.items) {\n const Tag = x.tag;\n return (\n <Tag key={x.key} className={cx(\"grid gap-0.5\", LIST_CLASS[x.type])}>\n {x.items.map((b, j) => (\n <ListItem key={b.id ?? j} block={b} />\n ))}\n </Tag>\n );\n }\n return <Block key={x.id ?? `b${i}`} block={x} />;\n });\n}\n\n/** A doc to read: `doc` is BlockNote JSON, or a plain string (one\n * paragraph per line). Trailing empty paragraphs are dropped. */\nexport function CommentBody({ doc, className }) {\n let blocks = typeof doc === \"string\" ? doc.split(\"\\n\").map((line) => ({ type: \"paragraph\", content: line })) : Array.isArray(doc) ? doc : [];\n let end = blocks.length;\n while (end > 0 && blocks[end - 1]?.type === \"paragraph\" && noText(blocks[end - 1].content) && !blocks[end - 1].children?.length) end--;\n blocks = blocks.slice(0, end);\n return (\n <div className={cx(\"grid gap-1 break-words text-sm\", className)}>\n <Blocks blocks={blocks} />\n </div>\n );\n}\n"
30
+ },
31
+ {
32
+ "path": "registry/blocks/comment-box/comment-time.jsx",
33
+ "type": "registry:component",
34
+ "target": "@components/makersclaw/blocks/comment-box/comment-time.jsx",
35
+ "content": "\"use client\";\n\n// CommentTime — when a comment was written, as a <time>.\n//\n// Relative words (\"5 min ago\") need the clock, and the server's clock and\n// time zone are not the reader's. So the server and the first paint show\n// the absolute date; after mount it turns relative, and one shared minute\n// tick (not a timer per comment) keeps every label current. The full date\n// is always in the title.\nimport { useEffect, useState } from \"react\";\nimport { cx } from \"../../ui\";\n\nconst listeners = new Set();\nlet timer = null;\nfunction subscribe(fn) {\n listeners.add(fn);\n if (!timer) timer = setInterval(() => listeners.forEach((l) => l(Date.now())), 60_000);\n return () => {\n listeners.delete(fn);\n if (!listeners.size && timer) {\n clearInterval(timer);\n timer = null;\n }\n };\n}\n\n/** Date.now(), refreshed each minute — null until mounted. */\nexport function useNow() {\n const [now, setNow] = useState(null);\n useEffect(() => {\n setNow(Date.now());\n return subscribe(setNow);\n }, []);\n return now;\n}\n\nexport const toDate = (v) => {\n if (v === undefined || v === null || v === \"\") return null;\n const d = v instanceof Date ? v : new Date(v);\n return Number.isNaN(d.getTime()) ? null : d;\n};\n\nconst fmt = (opts) => new Intl.DateTimeFormat(undefined, opts);\nconst TIME = { hour: \"numeric\", minute: \"2-digit\" };\nconst startOfDay = (ms) => {\n const d = new Date(ms);\n d.setHours(0, 0, 0, 0);\n return d.getTime();\n};\n\n/** \"12 Sep 2026, 14:05\" — the title, and the label before mount. */\nexport function fullDate(d) {\n return fmt({ day: \"numeric\", month: \"short\", year: \"numeric\", ...TIME }).format(d);\n}\n\n/** \"just now\" · \"5 min ago\" · \"3 h ago\" (today) · \"Yesterday, 14:05\" · \"12 Sep, 14:05\" · \"12 Sep 2025\". */\nexport function relativeDate(d, now) {\n const diff = now - d.getTime();\n if (diff < 45_000 && diff > -60_000) return \"just now\";\n const today = startOfDay(now);\n if (diff > 0 && diff < 60 * 60_000) return `${Math.round(diff / 60_000)} min ago`;\n if (d.getTime() >= today && diff > 0) return `${Math.floor(diff / 3_600_000)} h ago`;\n if (d.getTime() >= today - 86_400_000 && d.getTime() < today) return `Yesterday, ${fmt(TIME).format(d)}`;\n if (new Date(d).getFullYear() === new Date(now).getFullYear()) return fmt({ day: \"numeric\", month: \"short\", ...TIME }).format(d);\n return fmt({ day: \"numeric\", month: \"short\", year: \"numeric\" }).format(d);\n}\n\nexport function CommentTime({ date, className }) {\n const now = useNow();\n const d = toDate(date);\n if (!d) return null;\n return (\n <time dateTime={d.toISOString()} title={fullDate(d)} suppressHydrationWarning className={cx(\"text-xs text-muted-foreground\", className)}>\n {now === null ? fullDate(d) : relativeDate(d, now)}\n </time>\n );\n}\n"
36
+ },
37
+ {
38
+ "path": "registry/blocks/comment-box/README.md",
39
+ "type": "registry:file",
40
+ "target": "@components/makersclaw/blocks/comment-box/README.md",
41
+ "content": "# CommentBox\n\nA comment field that takes rich text: select a word for bold, italic or a\nlink, start a line with `-` for a list, type `@` to mention a teammate\n(they go in as a chip). Cmd/Ctrl+Enter posts, as does the Post button.\nWhile a post is on its way the box is locked; when it fails the words\nstay and the box says why; when it lands the box clears. Give it\n`comments` and it draws the thread above: avatar, name, \"5 min ago\", the\nbody with its formatting and mentions.\n\nIt is `doc-editor` in its minimal mode (no `/` menu, no drag handles), so\na comment is BlockNote JSON — the same doc every text block uses.\n\n```jsx\nimport { CommentBox } from \"../components/blocks/comment-box\"; // a Makersclaw app (\"./blocks/comment-box\" from components/)\nimport { CommentBox } from \"@/components/makersclaw/blocks/comment-box\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component, and\nload the thread in the server surface (README §4). `makersclaw add\ncomment-box` adds `doc-editor` with it (and its npm packages: run\n`npm install` after).\n\n## The data\n\nA comment in `comments`:\n\n```js\n{\n id: \"c-81\",\n author: { id: \"u-lena\", name: \"Lena Fischer\", src: \"https://…/lena.jpg\" }, // or just \"Lena Fischer\"\n body: [ /* the doc onPost gave you */ ], // or a plain string\n createdAt: \"2026-09-23T09:12:00Z\", // ISO string, Date or ms\n edited: false, // true adds \"(edited)\"\n}\n```\n\nStore `body` as it came out of `onPost` (a `jsonb` column). Keep a plain\ncopy beside it for search and notifications: `docToText(doc)` from\n`doc-editor`. The people mentioned are `docMentions(doc)` — notify them.\n\n## Props\n\n- `onPost(doc)` — required. `async`, returns `{ ok: true }` or\n `{ ok: false, error? }` (`error` is shown under the box; without it,\n \"Couldn't post…\"). A throw counts as a failure. Only an ok clears the\n box. An empty box never posts.\n- `comments` — the thread, oldest first, drawn above the box. Leave it out\n for the field alone; `[]` shows `emptyText`.\n- `author` — `{ name, src? }`, who is writing: their avatar sits beside\n the box from 640px.\n- `mentions` — `async (query) => [{ id, label, description? }]`: turns on\n `@` (doc-editor's `mentions`).\n- `placeholder` — `\"Write a comment…\"`.\n- `postLabel` — the button, `\"Post\"` (\"Reply\", \"Send\").\n- `label` — the field's accessible name, `\"Comment\"`.\n- `threadLabel` — the list's accessible name, `\"Comments\"`.\n- `emptyText` — `\"No comments yet.\"`.\n- `autoFocus` — `false`.\n- `disabled` — `false`; `true` locks the box (no edit, no post).\n- `className` — layout around it.\n\nThe other exports, for a thread of your own:\n\n- `CommentComposer` — the field and its buttons: `onSubmit(doc)` (same\n answer as `onPost`), `author`, `mentions`, `defaultValue` (a doc to\n start from — editing a comment), `placeholder`, `submitLabel`\n (`\"Post\"`), `submitIcon` (a kit icon name, `\"send\"`; `null` for none),\n `busyLabel` (shown while it runs), `failedText`,\n `onCancel` (adds a Cancel button), `cancelLabel`, `label`,\n `autoFocus`, `disabled`, `clearOnSubmit` (`true`; `false` when the\n composer goes away after a save), `className`.\n- `CommentBody` — `{ doc, className }`: a doc drawn read-only as plain\n HTML (server too): paragraphs, headings, lists, checklists, quotes,\n code, tables, links, image links, mentions and fields as chips.\n- `CommentTime` — `{ date, className }`: a `<time>`; the absolute date on\n the server, \"5 min ago\" once mounted, kept current each minute.\n\n## Example\n\n```jsx\n// components/TaskComments.jsx — client\n\"use client\";\nimport { useState } from \"react\";\nimport { insertRow } from \"../lib/actions\";\nimport { CommentBox } from \"./blocks/comment-box\";\nimport { docToText } from \"./blocks/doc-editor\";\n\nexport default function TaskComments({ task, me, team, initial }) {\n const [comments, setComments] = useState(initial);\n const findPeople = async (q) =>\n team.filter((p) => p.name.toLowerCase().includes(q.toLowerCase())).map((p) => ({ id: p.id, label: p.name }));\n\n async function post(doc) {\n const res = await insertRow({\n source: \"public.comments\",\n values: { task_id: task.id, author_id: me.id, body: doc, body_text: docToText(doc) },\n });\n if (!res.ok) return { ok: false, error: res.error };\n setComments((c) => [...c, { id: res.id, author: me, body: doc, createdAt: new Date() }]);\n return { ok: true };\n }\n\n return <CommentBox comments={comments} onPost={post} author={me} mentions={findPeople} />;\n}\n```\n\n## What it will not do\n\n- Edit or delete a posted comment, react, or reply in a nested thread: the\n thread is read-only. Notes you keep, edit and delete on a record are\n `record-notes`; build a richer thread from `CommentComposer` +\n `CommentBody` in your own component.\n- Attach files or images, or offer a `/` menu: it is a comment, not a\n document (`doc-editor` for that).\n- Notify anyone: read the mentions off the doc (`docMentions`) and send\n them yourself.\n- Load older comments or scroll the thread: it draws what it is given;\n put it in a scrolling box and page it yourself.\n\nAn editor it cannot be is your own component in `components/` — not an\nedit to this block. Its files are fixed: `npm run check` refuses a\nchange, and `makersclaw add comment-box` restores them.\n"
42
+ }
43
+ ],
44
+ "categories": [
45
+ "comment",
46
+ "comments",
47
+ "thread",
48
+ "reply",
49
+ "mention",
50
+ "composer",
51
+ "discussion",
52
+ "feedback",
53
+ "rich text",
54
+ "blocknote",
55
+ "cmd enter"
56
+ ],
57
+ "docs": "Installed to components/makersclaw/blocks/comment-box. Import: import { CommentBox, CommentComposer, CommentBody, CommentTime } from \"@/components/makersclaw/blocks/comment-box\". Props and an example: components/makersclaw/blocks/comment-box/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
58
+ "meta": {
59
+ "makersclaw": {
60
+ "version": "0.3.0",
61
+ "useWhen": "People comment on something in the app: a task, a design, a document, an approval, a support ticket; a reply field with @mentions under a thread. Not dated notes kept on a record with edit and delete (record-notes), not a conversation with an assistant (chat), not a long document (doc-editor, doc-page)."
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,53 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "contact-section",
4
+ "type": "registry:block",
5
+ "title": "Contact Section",
6
+ "description": "A public \"contact us\" section: a form (name, email, message; optional phone, company, subject, a topic as chips or radios, a consent box) and/or the ways to reach you (email, phone, address, hours, offices). The form alone, beside an info panel (plain or filled), or with info cards above or beside it; an optional picture beside or behind the form. Validates in place, submits through an async onSubmit that returns { ok }, and shows the failure or a sent state.",
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/contact-section/index.js",
14
+ "type": "registry:component",
15
+ "target": "@components/makersclaw/blocks/contact-section/index.js",
16
+ "content": "// The ContactSection block — `makersclaw add contact-section`. Fixed:\n// compose it through its props (README.md beside this file); a section\n// it cannot be is your own component in components/.\nexport { ContactSection } from \"./contact-section\";\n"
17
+ },
18
+ {
19
+ "path": "registry/blocks/contact-section/contact-section.jsx",
20
+ "type": "registry:component",
21
+ "target": "@components/makersclaw/blocks/contact-section/contact-section.jsx",
22
+ "content": "\"use client\";\n\nimport { useId, useState } from \"react\";\nimport { Button, Checkbox, Field, Icon, Image, Input, Kicker, Notice, RadioGroup, Textarea, Toggle, cx } from \"../../ui\";\n\n// ContactSection — a public \"contact us\" section: a form (name, email,\n// message; optional phone, company, subject, a topic picked from chips\n// or radios, a consent box) and/or the ways to reach you (email, phone,\n// address, hours, offices). Three layouts: the form alone, the form\n// beside an info panel, and info cards (alone, above the form or beside\n// it). The form validates in place, submits through `onSubmit(values)`\n// — async, returning { ok } — and shows the failure, or a sent state.\n//\n// <ContactSection title=\"Talk to us\" details={DETAILS}\n// onSubmit={async (v) => sendMessage(v)} />\n//\n// README.md beside this file lists every prop.\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst OPTIONAL = [\"phone\", \"company\", \"subject\"];\n\nconst optionOf = (o) => (typeof o === \"string\" ? { value: o, label: o } : o);\n\nfunction Header({ kicker, title, description, center }) {\n if (!kicker && !title && !description) return null;\n return (\n <div className={cx(\"grid gap-3\", center ? \"mx-auto max-w-2xl justify-items-center text-center\" : \"max-w-2xl\")}>\n {kicker ? <Kicker>{kicker}</Kicker> : null}\n {title ? <h2 className=\"text-balance text-3xl font-semibold tracking-tight sm:text-4xl\">{title}</h2> : null}\n {description ? <p className=\"text-pretty text-base text-muted-foreground sm:text-lg\">{description}</p> : null}\n </div>\n );\n}\n\n// A detail's value: a link when it has an href (mailto:, tel:, a map).\n// raw: a plain <a>, not Button: it inherits the colour of the panel it\n// sits on (a filled panel is primary), and these hrefs leave the app.\nfunction Value({ detail, className }) {\n if (!detail.href) return <span className={className}>{detail.value}</span>;\n return (\n <a href={detail.href} className={cx(\"break-words rounded-sm underline-offset-4 outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring\", className)}>\n {detail.value}\n </a>\n );\n}\n\nfunction DetailRows({ details, filled }) {\n if (!details?.length) return null;\n return (\n <ul className=\"grid gap-5\">\n {details.map((d, i) => (\n <li key={d.label ?? i} className=\"flex min-w-0 items-start gap-4\">\n <span className={cx(\"grid size-10 shrink-0 place-items-center rounded-lg\", filled ? \"bg-primary-foreground/15\" : \"bg-muted text-foreground\")}>\n <Icon name={d.icon ?? \"info\"} size={18} />\n </span>\n <div className=\"grid min-w-0 gap-0.5\">\n {d.label ? <p className={cx(\"text-sm\", filled ? \"opacity-80\" : \"text-muted-foreground\")}>{d.label}</p> : null}\n <Value detail={d} className=\"font-medium\" />\n {d.description ? <p className={cx(\"text-sm\", filled ? \"opacity-80\" : \"text-muted-foreground\")}>{d.description}</p> : null}\n </div>\n </li>\n ))}\n </ul>\n );\n}\n\nfunction Offices({ offices, filled }) {\n if (!offices?.length) return null;\n return (\n <div className=\"grid gap-6 sm:grid-cols-2\">\n {offices.map((o, i) => (\n <div key={o.name ?? i} className=\"grid min-w-0 content-start gap-1\">\n <p className=\"flex items-center gap-2 font-semibold\">\n <Icon name={o.icon ?? \"building\"} size={16} className={filled ? \"opacity-80\" : \"text-muted-foreground\"} />\n {o.name}\n </p>\n {(o.lines ?? []).map((line, k) => (\n <p key={k} className={cx(\"text-sm\", filled ? \"opacity-80\" : \"text-muted-foreground\")}>\n {line}\n </p>\n ))}\n </div>\n ))}\n </div>\n );\n}\n\nfunction Panel({ title, body, details, offices, look }) {\n const filled = look === \"filled\";\n return (\n <aside\n className={cx(\n \"grid min-w-0 content-start gap-8 rounded-xl p-6 sm:p-8\",\n filled ? \"bg-primary text-primary-foreground\" : \"border border-border bg-muted/40\",\n )}\n >\n {title || body ? (\n <div className=\"grid gap-2\">\n {title ? <h3 className=\"text-xl font-semibold\">{title}</h3> : null}\n {body ? <p className={cx(\"text-pretty\", filled ? \"opacity-80\" : \"text-muted-foreground\")}>{body}</p> : null}\n </div>\n ) : null}\n <DetailRows details={details} filled={filled} />\n <Offices offices={offices} filled={filled} />\n </aside>\n );\n}\n\nfunction DetailCards({ details, center, columns }) {\n if (!details?.length) return null;\n const cols = columns ?? Math.min(details.length, 3);\n return (\n <ul className={cx(\"grid gap-4\", cols === 1 ? \"grid-cols-1\" : cols === 2 ? \"sm:grid-cols-2\" : cols === 4 ? \"sm:grid-cols-2 lg:grid-cols-4\" : \"sm:grid-cols-2 lg:grid-cols-3\")}>\n {details.map((d, i) => (\n <li\n key={d.label ?? i}\n className={cx(\"grid min-w-0 content-start gap-3 rounded-lg border border-border bg-card p-6 text-card-foreground\", center && \"justify-items-center text-center\")}\n >\n <span className={cx(\"grid size-12 place-items-center bg-primary text-primary-foreground\", center ? \"rounded-full\" : \"rounded-lg\")}>\n <Icon name={d.icon ?? \"info\"} size={22} />\n </span>\n {d.label ? <h3 className=\"font-semibold\">{d.label}</h3> : null}\n {d.description ? <p className=\"text-pretty text-sm text-muted-foreground\">{d.description}</p> : null}\n {d.value ? <Value detail={d} className=\"font-medium text-primary\" /> : null}\n </li>\n ))}\n </ul>\n );\n}\n\n// The phone, email and hours on one line above a form.\nfunction DetailLine({ details, center }) {\n if (!details?.length) return null;\n return (\n <ul className={cx(\"flex flex-wrap gap-x-6 gap-y-2 text-sm\", center && \"justify-center\")}>\n {details.map((d, i) => (\n <li key={d.label ?? i} className=\"flex min-w-0 items-center gap-2\">\n <Icon name={d.icon ?? \"info\"} size={16} className=\"shrink-0 text-muted-foreground\" />\n {d.label ? <span className=\"sr-only\">{d.label}: </span> : null}\n <Value detail={d} className=\"font-medium\" />\n </li>\n ))}\n </ul>\n );\n}\n\nfunction blank(names, fields, topics) {\n const v = { email: \"\", message: \"\" };\n if (names === \"one\") v.name = \"\";\n else Object.assign(v, { firstName: \"\", lastName: \"\" });\n for (const f of fields) v[f] = \"\";\n if (topics) v.topic = \"\";\n return v;\n}\n\nfunction ContactForm({\n onSubmit,\n names,\n fields,\n topics,\n consent,\n messageRows,\n messageLabel,\n submitLabel,\n sent,\n defaultValues,\n labels,\n}) {\n const uid = useId();\n const id = (k) => `${uid}-${k}`;\n const start = () => ({ ...blank(names, fields, topics), ...defaultValues, ...(consent ? { consent: false } : {}) });\n const [values, setValues] = useState(start);\n const [errors, setErrors] = useState({});\n const [status, setStatus] = useState(\"idle\");\n const [failure, setFailure] = useState(null);\n const L = { firstName: \"First name\", lastName: \"Last name\", name: \"Name\", email: \"Email\", phone: \"Phone\", company: \"Company\", subject: \"Subject\", ...labels };\n\n const set = (k, v) => {\n setValues((prev) => ({ ...prev, [k]: v }));\n if (errors[k]) setErrors((prev) => ({ ...prev, [k]: undefined }));\n };\n\n function validate() {\n const e = {};\n if (names === \"one\" && !values.name?.trim()) e.name = \"Tell us your name.\";\n if (names !== \"one\" && !values.firstName?.trim()) e.firstName = \"Tell us your first name.\";\n if (!values.email?.trim()) e.email = \"We need an email to reply to.\";\n else if (!EMAIL.test(values.email.trim())) e.email = \"That email does not look right.\";\n if (topics?.required && !values.topic) e.topic = \"Pick one.\";\n if (!values.message?.trim()) e.message = \"Write a line or two.\";\n if (consent && !values.consent) e.consent = \"Please agree before sending.\";\n return e;\n }\n\n async function submit(ev) {\n ev.preventDefault();\n if (status === \"sending\") return;\n const e = validate();\n setErrors(e);\n const first = Object.keys(e)[0];\n if (first) {\n // The first problem gets the focus, so a keyboard user lands on it.\n const el = document.getElementById(id(first));\n (el?.matches(\"input, textarea\") ? el : el?.querySelector(\"input, button, textarea\"))?.focus();\n return;\n }\n setStatus(\"sending\");\n setFailure(null);\n const out = { ...values };\n for (const k of Object.keys(out)) if (typeof out[k] === \"string\") out[k] = out[k].trim();\n let res;\n try {\n res = await onSubmit(out);\n } catch (err) {\n res = { ok: false, error: err?.message };\n }\n if (res?.ok) {\n setStatus(\"sent\");\n } else {\n setStatus(\"idle\");\n setFailure(res?.error || \"Your message was not sent. Check your connection and try again.\");\n }\n }\n\n if (status === \"sent\")\n return (\n <div className=\"grid gap-4\" aria-live=\"polite\">\n <Notice tone=\"success\" title={sent?.title ?? \"Message sent\"}>\n {sent?.body ?? \"Thanks — we read every message and reply within one working day.\"}\n </Notice>\n <div>\n <Button\n variant=\"secondary\"\n icon=\"repeat\"\n onClick={() => {\n setValues(start());\n setErrors({});\n setStatus(\"idle\");\n }}\n >\n Send another\n </Button>\n </div>\n </div>\n );\n\n const topicOptions = (topics?.options ?? []).map(optionOf);\n const sending = status === \"sending\";\n return (\n <form noValidate onSubmit={submit} className=\"grid gap-5\" aria-busy={sending || undefined}>\n {topics && topics.style !== \"radio\" ? (\n <div className=\"grid gap-2\">\n <p id={id(\"topic-label\")} className=\"text-sm font-medium\">\n {topics.label ?? \"What is it about?\"}\n {topics.required ? <span aria-hidden=\"true\" className=\"text-destructive\"> *</span> : null}\n </p>\n <div id={id(\"topic\")} role=\"group\" aria-labelledby={id(\"topic-label\")} className=\"flex flex-wrap gap-2\">\n {topicOptions.map((o) => (\n <Toggle\n key={o.value}\n size=\"sm\"\n pressed={values.topic === o.value}\n onPressedChange={(on) => set(\"topic\", on ? o.value : \"\")}\n className=\"max-w-full\"\n >\n {o.label}\n </Toggle>\n ))}\n </div>\n {errors.topic ? (\n <p role=\"alert\" className=\"text-xs text-destructive\">\n {errors.topic}\n </p>\n ) : null}\n </div>\n ) : null}\n\n {names === \"one\" ? (\n <Field id={id(\"name\")} label={L.name} required error={errors.name}>\n <Input id={id(\"name\")} name=\"name\" autoComplete=\"name\" value={values.name} onChange={(e) => set(\"name\", e.target.value)} invalid={!!errors.name} />\n </Field>\n ) : (\n <div className=\"grid gap-5 sm:grid-cols-2\">\n <Field id={id(\"firstName\")} label={L.firstName} required error={errors.firstName}>\n <Input\n id={id(\"firstName\")}\n name=\"firstName\"\n autoComplete=\"given-name\"\n value={values.firstName}\n onChange={(e) => set(\"firstName\", e.target.value)}\n invalid={!!errors.firstName}\n />\n </Field>\n <Field id={id(\"lastName\")} label={L.lastName}>\n <Input id={id(\"lastName\")} name=\"lastName\" autoComplete=\"family-name\" value={values.lastName} onChange={(e) => set(\"lastName\", e.target.value)} />\n </Field>\n </div>\n )}\n\n <div className={cx(\"grid gap-5\", fields.includes(\"phone\") && \"sm:grid-cols-2\")}>\n <Field id={id(\"email\")} label={L.email} required error={errors.email}>\n <Input\n id={id(\"email\")}\n name=\"email\"\n type=\"email\"\n inputMode=\"email\"\n autoComplete=\"email\"\n placeholder=\"you@company.com\"\n value={values.email}\n onChange={(e) => set(\"email\", e.target.value)}\n invalid={!!errors.email}\n />\n </Field>\n {fields.includes(\"phone\") ? (\n <Field id={id(\"phone\")} label={L.phone} hint=\"Optional\">\n <Input id={id(\"phone\")} name=\"phone\" type=\"tel\" inputMode=\"tel\" autoComplete=\"tel\" value={values.phone} onChange={(e) => set(\"phone\", e.target.value)} />\n </Field>\n ) : null}\n </div>\n\n {fields.includes(\"company\") ? (\n <Field id={id(\"company\")} label={L.company} hint=\"Optional\">\n <Input id={id(\"company\")} name=\"company\" autoComplete=\"organization\" value={values.company} onChange={(e) => set(\"company\", e.target.value)} />\n </Field>\n ) : null}\n\n {topics && topics.style === \"radio\" ? (\n <Field id={id(\"topic\")} label={topics.label ?? \"What is it about?\"} required={topics.required} error={errors.topic}>\n <RadioGroup\n id={id(\"topic\")}\n name=\"topic\"\n orientation=\"horizontal\"\n options={topicOptions}\n value={values.topic}\n onChange={(v) => set(\"topic\", v)}\n invalid={!!errors.topic}\n />\n </Field>\n ) : null}\n\n {fields.includes(\"subject\") ? (\n <Field id={id(\"subject\")} label={L.subject}>\n <Input id={id(\"subject\")} name=\"subject\" value={values.subject} onChange={(e) => set(\"subject\", e.target.value)} />\n </Field>\n ) : null}\n\n <Field id={id(\"message\")} label={messageLabel ?? \"Message\"} required error={errors.message}>\n <Textarea\n id={id(\"message\")}\n name=\"message\"\n rows={messageRows}\n placeholder=\"What can we help with?\"\n value={values.message}\n onChange={(e) => set(\"message\", e.target.value)}\n invalid={!!errors.message}\n />\n </Field>\n\n {consent ? (\n <div className=\"grid gap-1\">\n <Checkbox\n id={id(\"consent\")}\n name=\"consent\"\n checked={!!values.consent}\n onChange={(e) => set(\"consent\", e.target.checked)}\n aria-invalid={errors.consent ? true : undefined}\n aria-describedby={errors.consent ? id(\"consent-error\") : undefined}\n label={consent}\n />\n {errors.consent ? (\n <p id={id(\"consent-error\")} role=\"alert\" className=\"text-xs text-destructive\">\n {errors.consent}\n </p>\n ) : null}\n </div>\n ) : null}\n\n {failure ? (\n <Notice tone=\"danger\" title=\"Not sent\">\n {failure}\n </Notice>\n ) : null}\n\n <div>\n <Button type=\"submit\" variant=\"primary\" icon=\"send\" loading={sending} className=\"w-full sm:w-auto\">\n {submitLabel ?? \"Send message\"}\n </Button>\n </div>\n </form>\n );\n}\n\nexport function ContactSection({\n layout = \"split\",\n kicker,\n title = \"Get in touch\",\n description,\n align,\n details,\n offices,\n panel = \"plain\",\n panelTitle,\n panelBody,\n cardsPosition = \"above\",\n cardColumns,\n image,\n imageAlt = \"\",\n imagePosition = \"side\",\n onSubmit,\n nameFields = \"split\",\n fields = [],\n topics,\n consent,\n messageLabel,\n messageRows = 5,\n submitLabel,\n sent,\n defaultValues,\n labels,\n label,\n className,\n}) {\n const extra = (fields ?? []).filter((f) => OPTIONAL.includes(f));\n const center = (align ?? (layout === \"split\" ? \"start\" : \"center\")) === \"center\";\n const form = onSubmit ? (\n <ContactForm\n onSubmit={onSubmit}\n names={nameFields === \"one\" ? \"one\" : \"split\"}\n fields={extra}\n topics={topics?.options?.length ? topics : null}\n consent={consent}\n messageRows={messageRows}\n messageLabel={messageLabel}\n submitLabel={submitLabel}\n sent={sent}\n defaultValues={defaultValues}\n labels={labels}\n />\n ) : null;\n const formCard = form ? <div className=\"min-w-0 rounded-xl border border-border bg-card p-5 text-card-foreground sm:p-8\">{form}</div> : null;\n\n let body;\n if (layout === \"cards\") {\n const cards = <DetailCards details={details} center={center && cardsPosition !== \"beside\"} columns={cardsPosition === \"beside\" && form ? 1 : cardColumns} />;\n body = !form ? (\n cards\n ) : cardsPosition === \"beside\" ? (\n <div className=\"grid gap-8 lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]\">\n {cards}\n {formCard}\n </div>\n ) : (\n <div className=\"grid gap-10\">\n {cards}\n <div className=\"mx-auto w-full max-w-2xl\">{formCard}</div>\n </div>\n );\n } else if (layout === \"form\") {\n if (image && imagePosition === \"background\" && form)\n body = (\n <div className=\"relative isolate overflow-hidden rounded-xl\">\n <div className=\"absolute inset-0 -z-10\">\n <Image src={image} alt={imageAlt} rounded=\"none\" className=\"h-full w-full\" />\n </div>\n <div className=\"grid justify-items-center bg-background/70 px-4 py-10 sm:px-8 sm:py-16\">\n <div className=\"w-full max-w-xl\">{formCard}</div>\n </div>\n </div>\n );\n else if (image && form)\n body = (\n <div className=\"grid items-stretch gap-8 md:grid-cols-2\">\n <div className=\"hidden md:block\">\n <Image src={image} alt={imageAlt} rounded=\"lg\" className=\"h-full min-h-96 w-full\" />\n </div>\n <div className=\"grid min-w-0 content-start gap-6\">\n <DetailLine details={details} />\n {formCard}\n </div>\n </div>\n );\n else\n body = (\n <div className=\"mx-auto grid w-full max-w-2xl gap-6\">\n <DetailLine details={details} center={center} />\n {formCard}\n </div>\n );\n } else {\n const side = <Panel title={panelTitle} body={panelBody} details={details} offices={offices} look={panel} />;\n body = form ? (\n <div className=\"grid gap-8 lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]\">\n {formCard}\n {side}\n </div>\n ) : (\n side\n );\n }\n\n return (\n <section aria-label={label ?? (typeof title === \"string\" ? title : \"Contact\")} className={cx(\"px-4 py-16 sm:px-6 sm:py-20\", className)}>\n <div className=\"mx-auto grid w-full max-w-6xl gap-10 sm:gap-12\">\n <Header kicker={kicker} title={title} description={description} center={center} />\n {body}\n </div>\n </section>\n );\n}\n"
23
+ },
24
+ {
25
+ "path": "registry/blocks/contact-section/README.md",
26
+ "type": "registry:file",
27
+ "target": "@components/makersclaw/blocks/contact-section/README.md",
28
+ "content": "# Contact Section\n\nA public \"contact us\" section. It holds a form, the ways to reach you, or\nboth. The form asks for a name, an email and a message, and optionally a\nphone, a company, a subject, a topic (chips or radios) and a consent box.\nThe ways to reach you are details (email, phone, address, hours) and\noffices.\n\nThree layouts:\n- **form** — the form alone, with the details on one line above it, or\n a picture beside or behind it;\n- **split** — the form beside an info panel (plain, or filled with the\n primary colour);\n- **cards** — the details as icon cards, alone, above the form or beside\n it.\n\nThe form checks itself as it is sent. A missing or malformed field gets\nits message under it, and the focus moves to the first one. It then\ncalls `onSubmit(values)`, which is async and returns `{ ok }`. While it\nruns, the button spins and a second press does nothing. `{ ok: false,\nerror }` (or a throw) shows the error above the button and keeps\neverything typed. `{ ok: true }` replaces the form with a \"Message\nsent\" notice and a \"Send another\" button. It is a full-width section: it\npads itself and centres its content.\n\n```jsx\nimport { ContactSection } from \"../components/blocks/contact-section\"; // a Makersclaw app (\"./blocks/contact-section\" from components/)\nimport { ContactSection } from \"@/components/makersclaw/blocks/contact-section\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component.\n\n## The data\n\n`details` is the ways to reach you:\n\n```js\n{\n icon: \"mail\", // a name from the kit's Icon\n label: \"Email\", // what it is (\"Call sales\" on a card)\n value: \"hello@fieldline.io\", // what is shown\n href?: \"mailto:hello@fieldline.io\", // a link: mailto:, tel:, a map, a page\n description?: \"A person replies within a working day.\", // a card's and a panel row's second line\n}\n```\n\n`offices` (split only): `[{ name: \"Lisbon\", lines: [\"Rua da Prata 88\", …], icon? }]`.\n\n`onSubmit` receives the values, trimmed:\n\n```js\n{\n firstName, lastName, // or `name` with nameFields=\"one\"\n email, message,\n phone?, company?, subject?, // the ones named in `fields`\n topic?, // the chosen option's value, \"\" for none\n consent?, // true: it was required and ticked\n}\n```\n\n## Props\n\n- `onSubmit(values)` — async, returns `{ ok: true }` or `{ ok: false,\n error? }`. Without it there is no form (a cards-only or panel-only\n section).\n- `layout` — `split` (default) · `form` · `cards`.\n- `kicker`, `title`, `description` — the header. `title` defaults to\n \"Get in touch\".\n- `align` — the header, and the cards' content: `center` (default for\n form and cards) · `start` (default for split).\n- `details` — the ways to reach you, as above. A panel's rows in\n `split`, one line above the form in `form`, the cards in `cards`.\n- `offices` — `split`: the offices, under the details in the panel.\n- `panel` — `split`: `plain` (default, a quiet bordered box) · `filled`\n (the primary colour).\n- `panelTitle`, `panelBody` — `split`: the panel's heading and a line\n under it.\n- `cardsPosition` — `cards` with a form: `above` (default) · `beside`\n (the cards stacked in a column beside the form from `lg` up).\n- `cardColumns` — `cards`: `1` · `2` · `3` · `4`. Default the number of\n cards, at most 3.\n- `image`, `imageAlt` — `form`: a picture. `imageAlt` defaults to \"\"\n (decoration).\n- `imagePosition` — `side` (default: beside the form from `md` up,\n hidden on a phone) · `background` (behind the form card, under a veil\n of the background colour).\n- `nameFields` — `split` (default: first name, required, and last name)\n · `one` (a single \"Name\").\n- `fields` — the optional fields to add: any of `\"phone\"`, `\"company\"`,\n `\"subject\"`. Phone and company are marked optional; subject is not\n required either.\n- `topics` — `{ label?, options, style?, required? }`: `options` are\n strings or `{ value, label }`; `style` `chips` (default: pressable\n chips above the name) · `radio` (a radio row after the email).\n- `consent` — the label of a consent checkbox (a string or a node with\n your policy link). When given, it must be ticked to send.\n- `messageLabel` — default \"Message\". `messageRows` — default `5`.\n- `submitLabel` — default \"Send message\".\n- `sent` — `{ title?, body? }` for the sent notice.\n- `defaultValues` — values to start with (a signed-in visitor's name and\n email).\n- `labels` — override field labels: `{ firstName, lastName, name, email,\n phone, company, subject }`.\n- `label` — what a screen reader calls the section. Default the title.\n- `className` — the section's spacing and width. Never colour.\n\n## Example\n\n```jsx\n// components/Contact.jsx\n\"use client\";\nimport { ContactSection } from \"./blocks/contact-section\";\nimport { sendContactMessage } from \"../lib/actions\"; // your server action\n\nexport default function Contact() {\n return (\n <ContactSection\n title=\"Talk to a person\"\n description=\"We reply within one working day.\"\n details={[\n { icon: \"mail\", label: \"Email\", value: \"hello@fieldline.io\", href: \"mailto:hello@fieldline.io\" },\n { icon: \"phone\", label: \"Phone\", value: \"+44 20 7946 0321\", href: \"tel:+442079460321\" },\n { icon: \"clock\", label: \"Hours\", value: \"Mon–Fri, 9:00–18:00 GMT\" },\n ]}\n topics={{ options: [\"Sales\", \"Support\", \"Partnerships\"], required: true }}\n consent=\"I agree to the privacy policy.\"\n onSubmit={async (values) => {\n const res = await sendContactMessage(values);\n return res.ok ? { ok: true } : { ok: false, error: res.error };\n }}\n />\n );\n}\n```\n\n## What it will not do\n\n- Send anything: `onSubmit` is yours (save a row, email the team).\n- Stop spam: add a captcha or a honeypot in your own `onSubmit` path.\n- Upload files, or take a card number.\n- Draw a map: link the address (`href`) to one.\n\nA section it cannot be is your own component in `components/`, built\nfrom the kit — not an edit to this block. Its files are fixed: `npm run\ncheck` refuses a change, and `makersclaw add contact-section` restores\nthem.\n\n## Credits\n\nLayouts ported from Creative Tim UI's contact-section blocks\n(https://github.com/creativetimofficial/ui, MIT), rewritten on the\nMakersclaw kit.\n"
29
+ }
30
+ ],
31
+ "categories": [
32
+ "contact",
33
+ "contact us",
34
+ "form",
35
+ "enquiry",
36
+ "support",
37
+ "get in touch",
38
+ "email",
39
+ "phone",
40
+ "address",
41
+ "offices",
42
+ "marketing",
43
+ "landing page",
44
+ "lead"
45
+ ],
46
+ "docs": "Installed to components/makersclaw/blocks/contact-section. Import: import { ContactSection } from \"@/components/makersclaw/blocks/contact-section\". Props and an example: components/makersclaw/blocks/contact-section/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
47
+ "meta": {
48
+ "makersclaw": {
49
+ "version": "0.3.0",
50
+ "useWhen": "The contact page or contact section of a public site: sales enquiries, support requests, a studio's \"start a project\" form. Not for a signed-in user's support inbox or ticket thread, or a general-purpose record form (use a form of your own from the kit)."
51
+ }
52
+ }
53
+ }