makersclaw 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (475) 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/cli.mjs +19 -6
  430. package/src/install.mjs +18 -0
  431. package/src/registry.mjs +38 -4
  432. package/src/shadcn.mjs +14 -1
  433. package/templates/next/files/README.md +145 -11
  434. package/templates/next/files/app/globals.css +55 -4
  435. package/templates/next/files/check/run.mjs +3 -3
  436. package/templates/next/files/components/ui/avatar.jsx +141 -0
  437. package/templates/next/files/components/ui/breadcrumb.jsx +149 -0
  438. package/templates/next/files/components/ui/button-group.jsx +70 -0
  439. package/templates/next/files/components/ui/button.jsx +132 -12
  440. package/templates/next/files/components/ui/calendar.jsx +609 -0
  441. package/templates/next/files/components/ui/carousel.jsx +281 -0
  442. package/templates/next/files/components/ui/collapsible.jsx +208 -0
  443. package/templates/next/files/components/ui/combobox.jsx +608 -0
  444. package/templates/next/files/components/ui/command.jsx +577 -0
  445. package/templates/next/files/components/ui/context-menu.jsx +191 -0
  446. package/templates/next/files/components/ui/date-picker.jsx +360 -0
  447. package/templates/next/files/components/ui/distribution.jsx +117 -0
  448. package/templates/next/files/components/ui/feedback.jsx +143 -0
  449. package/templates/next/files/components/ui/field.jsx +115 -3
  450. package/templates/next/files/components/ui/file-input.jsx +234 -0
  451. package/templates/next/files/components/ui/icon.jsx +130 -8
  452. package/templates/next/files/components/ui/image.jsx +143 -0
  453. package/templates/next/files/components/ui/index.js +47 -2
  454. package/templates/next/files/components/ui/input-group.jsx +213 -0
  455. package/templates/next/files/components/ui/input-otp.jsx +170 -0
  456. package/templates/next/files/components/ui/item.jsx +206 -0
  457. package/templates/next/files/components/ui/menu.jsx +454 -82
  458. package/templates/next/files/components/ui/notice.jsx +126 -14
  459. package/templates/next/files/components/ui/pagination.jsx +156 -0
  460. package/templates/next/files/components/ui/popover.jsx +284 -0
  461. package/templates/next/files/components/ui/radio.jsx +193 -0
  462. package/templates/next/files/components/ui/rating.jsx +131 -0
  463. package/templates/next/files/components/ui/resizable.jsx +338 -0
  464. package/templates/next/files/components/ui/scroll-area.jsx +123 -0
  465. package/templates/next/files/components/ui/separator.jsx +119 -0
  466. package/templates/next/files/components/ui/sheet.jsx +96 -11
  467. package/templates/next/files/components/ui/slider.jsx +174 -0
  468. package/templates/next/files/components/ui/stat.jsx +574 -0
  469. package/templates/next/files/components/ui/steps.jsx +338 -0
  470. package/templates/next/files/components/ui/swatch.jsx +33 -0
  471. package/templates/next/files/components/ui/table.jsx +130 -0
  472. package/templates/next/files/components/ui/toast.jsx +440 -0
  473. package/templates/next/files/components/ui/toggle.jsx +195 -0
  474. package/templates/next/files/components/ui/tooltip.jsx +270 -0
  475. package/templates/next/template.json +264 -8
package/r/chart.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "chart",
4
+ "type": "registry:block",
5
+ "title": "Chart",
6
+ "description": "Line, area (stacked), bar (grouped or stacked, columns or bars, value labels at the ends, a track behind), a muted or dashed series for a previous period to compare against, pie and donut charts, sparklines and heatmaps (a grid shaded by value, with a scale legend), drawn in SVG (the heatmap as an HTML table) with the workspace's chart colours: round ticks, gridlines, a tooltip and crosshair under the pointer or the arrow keys, a legend that hides a series, and a data table for screen readers.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.1/r/core.json"
9
+ ],
10
+ "dependencies": [],
11
+ "files": [
12
+ {
13
+ "path": "registry/blocks/chart/index.js",
14
+ "type": "registry:component",
15
+ "target": "@components/makersclaw/blocks/chart/index.js",
16
+ "content": "// The Chart block — `makersclaw add chart`. Fixed: compose it through its\n// props (README.md beside this file); a chart it cannot be is your own\n// component in components/.\nexport { LineChart, AreaChart, BarChart } from \"./cartesian\";\nexport { PieChart } from \"./pie\";\nexport { Sparkline } from \"./sparkline\";\nexport { ChartLegend } from \"./core\";\nexport { HeatmapChart, ScaleLegend, scaleStep, scaleShade } from \"./heatmap\";\n"
17
+ },
18
+ {
19
+ "path": "registry/blocks/chart/core.jsx",
20
+ "type": "registry:component",
21
+ "target": "@components/makersclaw/blocks/chart/core.jsx",
22
+ "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { Empty, Toggle, cx } from \"../../ui\";\n\n// What every chart in this block shares: the series colours as classes,\n// nice ticks, the width of the box it sits in, the reveal, the legend, the\n// tooltip box and the hidden data table a screen reader reads. The charts\n// themselves are in cartesian.jsx, pie.jsx and sparkline.jsx.\n\n// ---- colours ---------------------------------------------------------------\n// Written out whole so Tailwind finds every class. chart-1…5 are the series\n// colours (app/globals.css); the rest are the theme's own tones, for the\n// one series that means a status (churn in the destructive tone).\nconst TONES = {\n \"chart-1\": { fill: \"fill-chart-1\", stroke: \"stroke-chart-1\", bg: \"bg-chart-1\", text: \"text-chart-1\" },\n \"chart-2\": { fill: \"fill-chart-2\", stroke: \"stroke-chart-2\", bg: \"bg-chart-2\", text: \"text-chart-2\" },\n \"chart-3\": { fill: \"fill-chart-3\", stroke: \"stroke-chart-3\", bg: \"bg-chart-3\", text: \"text-chart-3\" },\n \"chart-4\": { fill: \"fill-chart-4\", stroke: \"stroke-chart-4\", bg: \"bg-chart-4\", text: \"text-chart-4\" },\n \"chart-5\": { fill: \"fill-chart-5\", stroke: \"stroke-chart-5\", bg: \"bg-chart-5\", text: \"text-chart-5\" },\n primary: { fill: \"fill-primary\", stroke: \"stroke-primary\", bg: \"bg-primary\", text: \"text-primary\" },\n info: { fill: \"fill-info\", stroke: \"stroke-info\", bg: \"bg-info\", text: \"text-info\" },\n success: { fill: \"fill-success\", stroke: \"stroke-success\", bg: \"bg-success\", text: \"text-success\" },\n warning: { fill: \"fill-warning\", stroke: \"stroke-warning\", bg: \"bg-warning\", text: \"text-warning\" },\n danger: { fill: \"fill-destructive\", stroke: \"stroke-destructive\", bg: \"bg-destructive\", text: \"text-destructive\" },\n muted: { fill: \"fill-muted-foreground\", stroke: \"stroke-muted-foreground\", bg: \"bg-muted-foreground\", text: \"text-muted-foreground\" },\n};\n\n/** The tone a series (or a slice) wears: its own `tone` (chart-1…5, 1…5,\n * or a theme tone), else the next chart colour in order. */\nexport function toneOf(item, index) {\n const t = typeof item?.tone === \"number\" ? `chart-${item.tone}` : item?.tone;\n return TONES[t] ? t : `chart-${(index % 5) + 1}`;\n}\n\nexport function tone(t, kind) {\n return (TONES[t] ?? TONES[\"chart-1\"])[kind];\n}\n\n// ---- numbers ---------------------------------------------------------------\n\nconst COMPACT = new Intl.NumberFormat(undefined, { notation: \"compact\", maximumFractionDigits: 1 });\nconst PLAIN = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 });\n\n/** The default value formatter: 1,250 · 48.2K · 1.3M. */\nexport function defaultFormat(v) {\n if (v == null || Number.isNaN(v)) return \"—\";\n return Math.abs(v) >= 10000 ? COMPACT.format(v) : PLAIN.format(v);\n}\n\nexport function num(v) {\n const n = typeof v === \"number\" ? v : Number(v);\n return Number.isFinite(n) ? n : null;\n}\n\n/** Round numbers that cover [min, max] in about `count` steps. */\nexport function niceTicks(min, max, count = 5) {\n if (!Number.isFinite(min) || !Number.isFinite(max)) return [0, 1];\n if (min === max) {\n if (min === 0) return [0, 1];\n min = Math.min(0, min);\n max = Math.max(0, max);\n }\n const raw = (max - min) / Math.max(1, count);\n const mag = 10 ** Math.floor(Math.log10(raw));\n const err = raw / mag;\n const step = (err >= 7.5 ? 10 : err >= 3.5 ? 5 : err >= 1.5 ? 2 : 1) * mag;\n const lo = Math.floor(min / step) * step;\n const hi = Math.ceil(max / step) * step;\n const out = [];\n for (let v = lo; v <= hi + step / 2; v += step) out.push(Number(v.toPrecision(12)));\n return out;\n}\n\nexport function humanize(key) {\n const text = String(key ?? \"\").replace(/[_-]+/g, \" \").trim();\n return text ? text.charAt(0).toUpperCase() + text.slice(1) : \"\";\n}\n\n/** About how wide a label is at text-2xs, for margins and thinning. The\n * axis text is small and the estimate generous, so nothing is clipped. */\nexport function textWidth(s) {\n return String(s ?? \"\").length * 6.4 + 4;\n}\n\n/** A safe id fragment for url(#…) from React's useId. */\nexport function idFrom(uid) {\n return `c${String(uid).replace(/[^a-zA-Z0-9-]/g, \"\")}`;\n}\n\n// ---- the box ---------------------------------------------------------------\n\n/** The width of the element, kept current by a ResizeObserver: a chart is\n * drawn for the width it has, never scaled. A callback ref, so a box that\n * comes back (after the empty state) is measured again. 0 until measured.\n *\n * const [boxRef, width] = useWidth(); <div ref={boxRef}>…</div> */\nexport function useWidth() {\n const [el, setEl] = useState(null);\n const [width, setWidth] = useState(0);\n useEffect(() => {\n if (!el) return undefined;\n setWidth(Math.floor(el.getBoundingClientRect().width));\n if (typeof ResizeObserver === \"undefined\") return undefined;\n const ro = new ResizeObserver(([entry]) => setWidth(Math.floor(entry.contentRect.width)));\n ro.observe(el);\n return () => ro.disconnect();\n }, [el]);\n return [setEl, width];\n}\n\n/** false for the first frame once `ready`, then true: marks draw from\n * nothing to their place. The transitions are motion-safe: classes, so\n * with reduced motion they simply appear. */\nexport function useReveal(ready) {\n const [shown, setShown] = useState(false);\n useEffect(() => {\n if (!ready || shown) return undefined;\n // A frame, so the start is painted; the timer, because a hidden tab\n // runs no frames and the chart must never stay empty.\n const done = () => setShown(true);\n const frame = requestAnimationFrame(() => requestAnimationFrame(done));\n const timer = setTimeout(done, 120);\n return () => {\n cancelAnimationFrame(frame);\n clearTimeout(timer);\n };\n }, [ready, shown]);\n return shown;\n}\n\n// ---- the parts every chart draws ----------------------------------------------\n\n// A square of the series' colour; a dashed series gets a short dashed line.\nfunction Swatch({ t, dim, dashed }) {\n if (dashed) {\n return (\n <svg aria-hidden=\"true\" width=\"12\" height=\"10\" viewBox=\"0 0 12 10\" className={cx(\"shrink-0\", dim && \"opacity-30\")}>\n <line x1=\"0\" y1=\"5\" x2=\"12\" y2=\"5\" strokeWidth=\"2\" strokeDasharray=\"3 2\" className={tone(t, \"stroke\")} />\n </svg>\n );\n }\n return <span aria-hidden=\"true\" className={cx(\"size-2.5 shrink-0 rounded-sm\", tone(t, \"bg\"), dim && \"opacity-30\")} />;\n}\n\n/**\n * The key to a chart's colours. With `onToggle` each entry is a toggle\n * (pressed = shown) that hides or shows its series; without it, a plain\n * list. `items` [{ key, label, tone?, value?, dashed? }] — `value` is a\n * muted note after the label (a slice's share); `dashed` draws the key as\n * a dashed line (a comparison series).\n */\nexport function ChartLegend({ items, hidden, onToggle, className, label = \"Legend\" }) {\n const list = (items ?? []).filter(Boolean);\n if (!list.length) return null;\n const off = hidden instanceof Set ? hidden : new Set(hidden ?? []);\n if (!onToggle) {\n return (\n <ul aria-label={label} className={cx(\"flex flex-wrap items-center justify-center gap-x-4 gap-y-1 text-xs text-muted-foreground\", className)}>\n {list.map((it, i) => (\n <li key={it.key} className=\"flex items-center gap-1.5\">\n <Swatch t={toneOf(it, i)} dashed={it.dashed} />\n <span className=\"text-foreground\">{it.label ?? humanize(it.key)}</span>\n {it.value != null ? <span data-nums>{it.value}</span> : null}\n </li>\n ))}\n </ul>\n );\n }\n return (\n <div role=\"group\" aria-label={label} className={cx(\"flex flex-wrap items-center justify-center gap-1\", className)}>\n {list.map((it, i) => {\n const shown = !off.has(it.key);\n return (\n <Toggle\n key={it.key}\n size=\"sm\"\n variant=\"quiet\"\n pressed={shown}\n onPressedChange={() => onToggle(it.key)}\n className=\"gap-1.5 text-xs\"\n >\n <Swatch t={toneOf(it, i)} dim={!shown} dashed={it.dashed} />\n <span className={shown ? \"text-foreground\" : \"line-through\"}>{it.label ?? humanize(it.key)}</span>\n {it.value != null ? <span data-nums className=\"text-muted-foreground\">{it.value}</span> : null}\n </Toggle>\n );\n })}\n </div>\n );\n}\n\n/** The floating box that says what is under the pointer (or the keyboard).\n * `at` { x, y, flip } in the chart's pixels; flip puts it on the left. */\nexport function TooltipBox({ at, title, rows, footer }) {\n if (!at) return null;\n return (\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute left-0 top-0 z-10 min-w-36 rounded-md border border-border bg-popover px-2.5 py-2 text-xs text-popover-foreground shadow-lg\"\n style={{\n transform: `translate(${Math.round(at.x)}px, ${Math.round(at.y)}px) translate(${at.flip ? \"calc(-100% - 12px)\" : \"12px\"}, ${at.middle ? \"-50%\" : \"0\"})`,\n }}\n >\n {title != null ? <p className=\"mb-1.5 font-medium\">{title}</p> : null}\n <div className=\"grid gap-1\">\n {rows.map((r) => (\n <div key={r.key} className=\"flex items-center gap-2\">\n <Swatch t={r.tone} dashed={r.dashed} />\n <span className=\"flex-1 text-muted-foreground\">{r.label}</span>\n <span data-nums className=\"font-medium\">{r.value}</span>\n </div>\n ))}\n </div>\n {footer ? (\n <div className=\"mt-1.5 flex items-center justify-between gap-2 border-t border-border pt-1.5\">\n <span className=\"text-muted-foreground\">{footer.label}</span>\n <span data-nums className=\"font-medium\">{footer.value}</span>\n </div>\n ) : null}\n </div>\n );\n}\n\n/** The numbers behind the picture, for a screen reader: a real table,\n * visually hidden. `head` the column names, `body` rows of strings. */\nexport function DataTable({ caption, head, body }) {\n return (\n <table className=\"sr-only\">\n <caption>{caption}</caption>\n <thead>\n <tr>\n {head.map((h, i) => (\n <th key={i} scope=\"col\">{h}</th>\n ))}\n </tr>\n </thead>\n <tbody>\n {body.map((cells, r) => (\n <tr key={r}>\n {cells.map((c, i) =>\n i === 0 ? (\n <th key={i} scope=\"row\">{c}</th>\n ) : (\n <td key={i}>{c}</td>\n ),\n )}\n </tr>\n ))}\n </tbody>\n </table>\n );\n}\n\n/** What a chart shows with nothing to draw, at the height it would have. */\nexport function ChartEmpty({ height, title, body }) {\n return (\n <div style={{ minHeight: height }} className=\"grid\">\n <Empty icon=\"chart\" title={title} body={body} className=\"h-full content-center\" />\n </div>\n );\n}\n"
23
+ },
24
+ {
25
+ "path": "registry/blocks/chart/cartesian.jsx",
26
+ "type": "registry:component",
27
+ "target": "@components/makersclaw/blocks/chart/cartesian.jsx",
28
+ "content": "\"use client\";\n\nimport { useId, useMemo, useState } from \"react\";\nimport { cx } from \"../../ui\";\nimport {\n ChartEmpty,\n ChartLegend,\n DataTable,\n TooltipBox,\n defaultFormat,\n humanize,\n idFrom,\n niceTicks,\n num,\n textWidth,\n tone,\n toneOf,\n useReveal,\n useWidth,\n} from \"./core\";\n\n// LineChart, AreaChart and BarChart — one drawing, three marks. Rows in,\n// one x key, the series to draw; the block owns the rest: the scale and its\n// round ticks, gridlines, labels thinned to fit, a width that follows the\n// container, the tooltip and crosshair under a pointer or the arrow keys,\n// the legend that hides a series, the reveal (none with reduced motion),\n// and the words and table a screen reader gets instead of the picture.\n\n// ---- paths -------------------------------------------------------------------\n\nconst f = (n) => Math.round(n * 100) / 100;\n\nfunction linear(pts) {\n return pts.map(([x, y], i) => `${i ? \"L\" : \"M\"}${f(x)},${f(y)}`).join(\"\");\n}\n\nfunction step(pts) {\n let d = `M${f(pts[0][0])},${f(pts[0][1])}`;\n for (let i = 1; i < pts.length; i++) {\n const mid = (pts[i - 1][0] + pts[i][0]) / 2;\n d += `H${f(mid)}V${f(pts[i][1])}H${f(pts[i][0])}`;\n }\n return d;\n}\n\n// Monotone cubic (Fritsch–Carlson): smooth, and never overshoots a point,\n// so a curve does not invent a dip below zero or a peak that was not there.\nfunction monotone(pts) {\n const n = pts.length;\n if (n < 3) return linear(pts);\n const dx = [];\n const m = [];\n for (let i = 0; i < n - 1; i++) {\n dx[i] = pts[i + 1][0] - pts[i][0];\n m[i] = dx[i] === 0 ? 0 : (pts[i + 1][1] - pts[i][1]) / dx[i];\n }\n const t = new Array(n);\n t[0] = m[0];\n t[n - 1] = m[n - 2];\n for (let i = 1; i < n - 1; i++) {\n t[i] = m[i - 1] * m[i] <= 0 ? 0 : (3 * (dx[i - 1] + dx[i])) / ((2 * dx[i] + dx[i - 1]) / m[i - 1] + (dx[i] + 2 * dx[i - 1]) / m[i]);\n }\n let d = `M${f(pts[0][0])},${f(pts[0][1])}`;\n for (let i = 0; i < n - 1; i++) {\n const h = dx[i] / 3;\n d += `C${f(pts[i][0] + h)},${f(pts[i][1] + t[i] * h)} ${f(pts[i + 1][0] - h)},${f(pts[i + 1][1] - t[i + 1] * h)} ${f(pts[i + 1][0])},${f(pts[i + 1][1])}`;\n }\n return d;\n}\n\nfunction curvePath(pts, curve) {\n if (!pts.length) return \"\";\n if (curve === \"step\") return step(pts);\n if (curve === \"linear\") return linear(pts);\n return monotone(pts);\n}\n\n// A bar with its outer end rounded (the end away from zero).\nfunction barPath(x, y, w, h, r, side) {\n const q = Math.max(0, Math.min(r, w / 2, h / 2));\n if (!q || !side) return `M${f(x)},${f(y)}h${f(w)}v${f(h)}h${f(-w)}Z`;\n const X = f(x);\n const Y = f(y);\n if (side === \"top\") return `M${X},${f(y + h)}V${f(y + q)}Q${X},${Y} ${f(x + q)},${Y}H${f(x + w - q)}Q${f(x + w)},${Y} ${f(x + w)},${f(y + q)}V${f(y + h)}Z`;\n if (side === \"bottom\") return `M${X},${Y}V${f(y + h - q)}Q${X},${f(y + h)} ${f(x + q)},${f(y + h)}H${f(x + w - q)}Q${f(x + w)},${f(y + h)} ${f(x + w)},${f(y + h - q)}V${Y}Z`;\n if (side === \"right\") return `M${X},${Y}H${f(x + w - q)}Q${f(x + w)},${Y} ${f(x + w)},${f(y + q)}V${f(y + h - q)}Q${f(x + w)},${f(y + h)} ${f(x + w - q)},${f(y + h)}H${X}Z`;\n return `M${f(x + w)},${Y}H${f(x + q)}Q${X},${Y} ${X},${f(y + q)}V${f(y + h - q)}Q${X},${f(y + h)} ${f(x + q)},${f(y + h)}H${f(x + w)}Z`;\n}\n\n// ---- the chart ----------------------------------------------------------------\n\nfunction Cartesian({\n kind,\n data,\n x,\n series,\n format = defaultFormat,\n formatAxis,\n formatX = (v) => (v == null ? \"\" : String(v)),\n label = \"Chart\",\n description,\n xLabel,\n height = 240,\n stacked = false,\n orientation = \"vertical\",\n valueLabels = false,\n track = false,\n curve = \"monotone\",\n dots = false,\n grid = true,\n xAxis = true,\n yAxis = true,\n legend,\n tooltip = true,\n onSelect,\n emptyLabel = \"No data yet\",\n emptyBody,\n className,\n}) {\n const uid = idFrom(useId());\n const fmt = (v) => (v == null ? \"—\" : format(v));\n const [boxRef, width] = useWidth();\n const [hidden, setHidden] = useState(() => new Set());\n const [active, setActive] = useState(null);\n const [keyed, setKeyed] = useState(false);\n\n const rows = Array.isArray(data) ? data : [];\n // A `muted` series with no tone of its own is the grey: a comparison\n // (last period) that sits behind the series it is compared with.\n const all = useMemo(\n () => (series ?? []).map((s, i) => ({ ...s, label: s.label ?? humanize(s.key), tone: s.muted && s.tone == null ? \"muted\" : toneOf(s, i) })),\n [series],\n );\n const live = all.filter((s) => !hidden.has(s.key));\n // Lines and areas draw muted series first, so they sit underneath.\n const order = live.map((_, j) => j).sort((a, b) => Number(Boolean(live[b].muted)) - Number(Boolean(live[a].muted)));\n const horizontal = kind === \"bar\" && orientation === \"horizontal\";\n const stack = stacked && kind !== \"line\";\n const axisFormat = formatAxis ?? format;\n const showLegend = legend ?? all.length > 1;\n const shown = useReveal(width > 0);\n\n // Each series' [from, to] per row: stacked marks start where the one\n // below ended (positives up, negatives down); the rest start at zero.\n const spans = useMemo(() => {\n const up = rows.map(() => 0);\n const down = rows.map(() => 0);\n return live.map((s) =>\n rows.map((r, i) => {\n const v = num(r?.[s.key]);\n if (v == null) return null;\n if (!stack) return [0, v];\n if (v >= 0) {\n const from = up[i];\n up[i] += v;\n return [from, up[i]];\n }\n const from = down[i];\n down[i] += v;\n return [from, down[i]];\n }),\n );\n }, [rows, live, stack]); // eslint-disable-line react-hooks/exhaustive-deps\n\n let lo = 0;\n let hi = 0;\n for (const s of spans) for (const p of s) if (p) (lo = Math.min(lo, p[0], p[1])), (hi = Math.max(hi, p[0], p[1]));\n const valueLen = horizontal ? Math.max(0, width - 16) : height - 32;\n const ticks = niceTicks(lo, hi, Math.max(2, Math.min(6, Math.floor(valueLen / (horizontal ? 80 : 48)))));\n const vMin = ticks[0];\n const vMax = ticks[ticks.length - 1];\n\n const xNames = rows.map((r) => formatX(r?.[x], r));\n const tickNames = ticks.map((t) => axisFormat(t));\n\n // BarChart's value labels: one at the outer end of every bar, or of every\n // stack (its total). [{ i, j, v, text }]; j is the series (-1: a stack).\n const barLabels = [];\n if (kind === \"bar\" && valueLabels && live.length) {\n const say = (v, row, key) => (typeof valueLabels === \"function\" ? valueLabels(v, row, key) : fmt(v));\n rows.forEach((row, i) => {\n if (stack) {\n const vals = live.map((s) => num(row?.[s.key])).filter((v) => v != null);\n if (!vals.length) return;\n const v = vals.reduce((a, b) => a + b, 0);\n barLabels.push({ i, j: -1, v, text: say(v, row, null) });\n } else {\n live.forEach((s, j) => {\n const v = num(row?.[s.key]);\n if (v != null) barLabels.push({ i, j, v, text: say(v, row, s.key) });\n });\n }\n });\n }\n const labelRoom = barLabels.length ? Math.max(...barLabels.map((l) => textWidth(l.text))) + 6 : 0;\n\n // Margins from what the labels need.\n const catW = horizontal && yAxis ? Math.min(Math.max(...xNames.map(textWidth), 24) + 8, width * 0.4) : 0;\n const valW = !horizontal && yAxis ? Math.max(...tickNames.map(textWidth)) + 8 : 0;\n const m = {\n top: barLabels.length && !horizontal ? 22 : 8,\n right: horizontal ? Math.max(8, textWidth(tickNames[tickNames.length - 1]) / 2, labelRoom) : 8,\n bottom: xAxis ? 24 : 4,\n left: horizontal ? catW : valW,\n };\n // Points sit on the plot's edges (bars sit inside their band), so the\n // first and last x labels, centred on them, need room outside it.\n if (kind !== \"bar\" && xAxis && rows.length) {\n m.left = Math.max(m.left, textWidth(xNames[0]) / 2);\n m.right = Math.max(m.right, textWidth(xNames[xNames.length - 1]) / 2);\n }\n const plotW = Math.max(0, width - m.left - m.right);\n const plotH = Math.max(0, height - m.top - m.bottom);\n const n = rows.length;\n\n // Value → pixel along the value axis; index → pixel along the category axis.\n const vPos = (v) => (horizontal ? m.left + ((v - vMin) / (vMax - vMin || 1)) * plotW : m.top + plotH - ((v - vMin) / (vMax - vMin || 1)) * plotH);\n const band = (horizontal ? plotH : plotW) / Math.max(1, n);\n const cPos =\n kind === \"bar\"\n ? (i) => (horizontal ? m.top : m.left) + band * (i + 0.5)\n : (i) => m.left + (n === 1 ? plotW / 2 : (i * plotW) / (n - 1));\n const zero = vPos(Math.max(vMin, Math.min(0, vMax)));\n\n // Category labels thinned so they never collide.\n const labelEvery = horizontal\n ? Math.max(1, Math.ceil(16 / Math.max(1, band)))\n : Math.max(1, Math.ceil((Math.max(...xNames.map(textWidth), 0) + 12) / Math.max(1, kind === \"bar\" ? band : plotW / Math.max(1, n - 1))));\n\n function indexAt(e) {\n const rect = e.currentTarget.getBoundingClientRect();\n const p = horizontal ? e.clientY - rect.top : e.clientX - rect.left;\n if (kind === \"bar\") return Math.max(0, Math.min(n - 1, Math.floor((p - (horizontal ? m.top : m.left)) / band)));\n if (n === 1) return 0;\n return Math.max(0, Math.min(n - 1, Math.round(((p - m.left) / plotW) * (n - 1))));\n }\n\n function onKeyDown(e) {\n if (!n) return;\n const back = horizontal ? \"ArrowUp\" : \"ArrowLeft\";\n const fwd = horizontal ? \"ArrowDown\" : \"ArrowRight\";\n let next = null;\n if (e.key === fwd || e.key === (horizontal ? \"ArrowRight\" : \"ArrowDown\")) next = active == null ? 0 : Math.min(n - 1, active + 1);\n else if (e.key === back || e.key === (horizontal ? \"ArrowLeft\" : \"ArrowUp\")) next = active == null ? n - 1 : Math.max(0, active - 1);\n else if (e.key === \"Home\") next = 0;\n else if (e.key === \"End\") next = n - 1;\n else if (e.key === \"Escape\") {\n setActive(null);\n return;\n } else if ((e.key === \"Enter\" || e.key === \" \") && onSelect && active != null) {\n e.preventDefault();\n onSelect(rows[active], active);\n return;\n }\n if (next == null) return;\n e.preventDefault();\n setKeyed(true);\n setActive(next);\n }\n\n // ---- words -------------------------------------------------------------------\n\n const summary = useMemo(() => {\n if (!n || !live.length) return \"\";\n const parts = live.map((s) => {\n const vals = rows.map((r) => num(r?.[s.key]));\n const idx = vals.reduce((best, v, i) => (v != null && (best < 0 || v > vals[best]) ? i : best), -1);\n const first = vals.find((v) => v != null);\n const last = [...vals].reverse().find((v) => v != null);\n return `${s.label}: ${kind === \"bar\" ? \"\" : `from ${fmt(first)} to ${fmt(last)}, `}highest ${fmt(vals[idx])} at ${xNames[idx]}`;\n });\n return `${n} ${n === 1 ? \"point\" : \"points\"}, ${xNames[0]} to ${xNames[n - 1]}. ${parts.join(\"; \")}.`;\n }, [rows, live, n]); // eslint-disable-line react-hooks/exhaustive-deps\n\n const tipRows =\n active == null\n ? []\n : live.map((s) => ({ key: s.key, label: s.label, tone: s.tone, dashed: s.dashed, value: fmt(num(rows[active]?.[s.key])) }));\n const total = active == null ? null : live.reduce((sum, s) => sum + (num(rows[active]?.[s.key]) ?? 0), 0);\n const announce = active == null ? \"\" : `${xNames[active]}: ${tipRows.map((r) => `${r.label} ${r.value}`).join(\", \")}`;\n\n const legendNode = showLegend ? (\n <ChartLegend\n items={all}\n hidden={hidden}\n onToggle={(key) =>\n setHidden((h) => {\n const next = new Set(h);\n if (next.has(key)) next.delete(key);\n else next.add(key);\n return next;\n })\n }\n label={`${label}: series`}\n />\n ) : null;\n\n if (!n) {\n return (\n <div className={cx(\"grid gap-3\", className)}>\n <ChartEmpty height={height} title={emptyLabel} body={emptyBody} />\n </div>\n );\n }\n\n // ---- marks -------------------------------------------------------------------\n\n const tipAt =\n active == null || !tooltip\n ? null\n : horizontal\n ? { x: Math.min(width - 12, Math.max(...live.map((_, k) => vPos(spans[k][active]?.[1] ?? 0)), zero)), y: cPos(active), flip: false, middle: true }\n : { x: cPos(active), y: m.top, flip: cPos(active) > width / 2 };\n if (tipAt && horizontal) tipAt.flip = tipAt.x > width * 0.6;\n\n const revealStyle = (axis) => ({\n transform: shown ? \"none\" : axis === \"x\" ? \"scaleX(0)\" : \"scaleY(0)\",\n transformOrigin: axis === \"x\" ? `${zero}px 0` : `0 ${zero}px`,\n });\n\n let marks = null;\n if (kind === \"bar\") {\n const k = live.length;\n const groupW = Math.min(band * (stack ? 0.62 : 0.8), stack ? 56 : 28 * Math.max(1, k) + 4 * (k - 1));\n const gap = k > 1 && !stack ? Math.min(4, groupW * 0.06) : 0;\n const barW = stack ? groupW : Math.max(1, (groupW - gap * (k - 1)) / Math.max(1, k));\n const r = Math.min(4, barW / 3);\n // Which series ends each stack (its outer end is the rounded one).\n const outer = rows.map((_, i) => {\n let top = -1;\n let bottom = -1;\n live.forEach((s, j) => {\n const p = spans[j][i];\n if (!p || p[1] === p[0]) return;\n if (p[1] > p[0]) top = j;\n else bottom = j;\n });\n return { top, bottom };\n });\n // Where the stacked total ends: the far end of the stack on its side.\n const stackEnd = (i, v) => {\n let end = 0;\n live.forEach((_, j) => {\n const p = spans[j][i];\n if (p) end = v >= 0 ? Math.max(end, p[1]) : Math.min(end, p[1]);\n });\n return end;\n };\n const tracks = track ? (\n <g aria-hidden=\"true\" className=\"fill-accent\">\n {rows.map((_, i) => {\n const c = cPos(i) - groupW / 2;\n return horizontal ? (\n <rect key={i} x={m.left} y={c} width={plotW} height={groupW} rx={r} />\n ) : (\n <rect key={i} x={c} y={m.top} width={groupW} height={plotH} rx={r} />\n );\n })}\n </g>\n ) : null;\n const labels = barLabels.length ? (\n <g\n aria-hidden=\"true\"\n className=\"fill-foreground text-2xs font-medium motion-safe:transition-opacity motion-safe:duration-500\"\n style={{ opacity: shown ? 1 : 0 }}\n data-nums\n >\n {barLabels.map(({ i, j, v, text }) => {\n const end = vPos(j < 0 ? stackEnd(i, v) : v);\n const c = cPos(i) + (j < 0 ? 0 : -groupW / 2 + j * (barW + gap) + barW / 2);\n const up = v >= 0;\n return horizontal ? (\n <text key={`${i}-${j}`} x={end + (up ? 6 : -6)} y={c} dy=\"0.32em\" textAnchor={up ? \"start\" : \"end\"}>\n {text}\n </text>\n ) : (\n <text key={`${i}-${j}`} x={c} y={end + (up ? -6 : 12)} textAnchor=\"middle\">\n {text}\n </text>\n );\n })}\n </g>\n ) : null;\n marks = (\n <>\n {tracks}\n <g style={revealStyle(horizontal ? \"x\" : \"y\")} className=\"motion-safe:transition-transform motion-safe:duration-500 motion-safe:ease-out\">\n {live.map((s, j) => (\n <g key={s.key} className={tone(s.tone, \"fill\")}>\n {rows.map((_, i) => {\n const p = spans[j][i];\n if (!p || p[0] === p[1]) return null;\n const a = vPos(p[0]);\n const b = vPos(p[1]);\n const offset = stack ? -groupW / 2 : -groupW / 2 + j * (barW + gap);\n const c = cPos(i) + offset;\n const positive = p[1] > p[0];\n const isOuter = !stack || (positive ? outer[i].top === j : outer[i].bottom === j);\n const side = !isOuter ? null : horizontal ? (positive ? \"right\" : \"left\") : positive ? \"top\" : \"bottom\";\n const d = horizontal\n ? barPath(Math.min(a, b), c, Math.abs(b - a), barW, r, side)\n : barPath(c, Math.min(a, b), barW, Math.abs(b - a), r, side);\n return <path key={i} d={d} className={cx(active != null && active !== i && \"opacity-60\", \"motion-safe:transition-opacity\")} />;\n })}\n </g>\n ))}\n </g>\n {labels}\n </>\n );\n } else {\n const clip = `${uid}-clip`;\n const fade = `${uid}-fade`;\n marks = (\n <>\n <defs>\n <clipPath id={clip}>\n <rect\n x={m.left - 4}\n y={0}\n width={plotW + 8}\n height={height}\n style={{ transform: shown ? \"none\" : \"scaleX(0)\", transformOrigin: `${m.left}px 0` }}\n className=\"motion-safe:transition-transform motion-safe:duration-700 motion-safe:ease-out\"\n />\n </clipPath>\n {/* An alpha mask, not a colour: the area fades toward zero. */}\n <linearGradient id={`${fade}-g`} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0\" stopOpacity=\"1\" />\n <stop offset=\"1\" stopOpacity=\"0.15\" />\n </linearGradient>\n <mask id={fade} style={{ maskType: \"alpha\" }} maskUnits=\"userSpaceOnUse\" x={0} y={0} width={width} height={height}>\n <rect x={0} y={m.top} width={width} height={plotH} fill={`url(#${fade}-g)`} />\n </mask>\n </defs>\n <g clipPath={`url(#${clip})`}>\n {kind === \"area\"\n ? order.map((j) => {\n const s = live[j];\n // A dashed series is a line only: a comparison has no fill.\n if (s.dashed && !stack) return null;\n const top = [];\n const base = [];\n spans[j].forEach((p, i) => {\n top.push([cPos(i), vPos(p ? p[1] : stack ? 0 : 0)]);\n base.push([cPos(i), vPos(p ? p[0] : 0)]);\n });\n const back = curvePath([...base].reverse(), curve).replace(/^M/, \"L\");\n return (\n <path\n key={s.key}\n d={`${curvePath(top, curve)}${back}Z`}\n className={tone(s.tone, \"fill\")}\n fillOpacity={stack ? 0.4 : s.muted ? 0.2 : 0.45}\n mask={stack ? undefined : `url(#${fade})`}\n />\n );\n })\n : null}\n {order.map((j) => {\n const s = live[j];\n // A line breaks where a value is missing.\n const segs = [];\n let cur = [];\n spans[j].forEach((p, i) => {\n if (!p && kind === \"line\") {\n if (cur.length) segs.push(cur);\n cur = [];\n return;\n }\n cur.push([cPos(i), vPos(p ? p[1] : 0)]);\n });\n if (cur.length) segs.push(cur);\n return (\n <g\n key={s.key}\n className={tone(s.tone, \"stroke\")}\n fill=\"none\"\n strokeWidth={s.muted ? 1.5 : 2}\n strokeDasharray={s.dashed ? \"5 4\" : undefined}\n strokeLinejoin=\"round\"\n strokeLinecap={s.dashed ? \"butt\" : \"round\"}\n >\n {segs.map((seg, k) => (\n <path key={k} d={curvePath(seg, curve)} />\n ))}\n {dots\n ? spans[j].map((p, i) =>\n p ? <circle key={i} cx={cPos(i)} cy={vPos(p[1])} r={3} className={cx(tone(s.tone, \"fill\"), \"stroke-card\")} strokeWidth={1.5} strokeDasharray=\"none\" /> : null,\n )\n : null}\n </g>\n );\n })}\n </g>\n </>\n );\n }\n\n const cursor =\n active == null\n ? null\n : kind === \"bar\"\n ? horizontal\n ? <rect x={m.left} y={m.top + band * active} width={plotW} height={band} className=\"fill-muted\" />\n : <rect x={m.left + band * active} y={m.top} width={band} height={plotH} className=\"fill-muted\" />\n : <line x1={cPos(active)} x2={cPos(active)} y1={m.top} y2={m.top + plotH} className=\"stroke-muted-foreground\" strokeDasharray=\"3 3\" strokeWidth={1} />;\n\n const activeDots =\n active == null || kind === \"bar\"\n ? null\n : live.map((s, j) => {\n const p = spans[j][active];\n if (!p) return null;\n return <circle key={s.key} cx={cPos(active)} cy={vPos(p[1])} r={4.5} className={cx(tone(s.tone, \"fill\"), \"stroke-card\")} strokeWidth={2} />;\n });\n\n return (\n <div className={cx(\"grid min-w-0 gap-3\", className)}>\n <div\n ref={boxRef}\n tabIndex={0}\n role=\"group\"\n aria-label={`${label}. Arrow keys read each value.`}\n onKeyDown={onKeyDown}\n onFocus={(e) => {\n if (e.currentTarget.matches(\":focus-visible\") && active == null) {\n setKeyed(true);\n setActive(0);\n }\n }}\n onBlur={() => {\n setKeyed(false);\n setActive(null);\n }}\n className=\"relative min-w-0 rounded-md outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n style={{ height }}\n >\n {width > 0 ? (\n <svg\n role=\"img\"\n aria-label={`${label}. ${description ?? summary}`}\n width={width}\n height={height}\n viewBox={`0 0 ${width} ${height}`}\n className={cx(\"block touch-pan-y select-none overflow-visible\", onSelect && \"cursor-pointer\")}\n onPointerMove={(e) => {\n setKeyed(false);\n setActive(indexAt(e));\n }}\n onPointerDown={(e) => setActive(indexAt(e))}\n onPointerLeave={() => !keyed && setActive(null)}\n onClick={(e) => onSelect?.(rows[indexAt(e)], indexAt(e))}\n >\n {/* raw: an SVG drawing — no kit part draws a chart. */}\n {cursor}\n {grid\n ? ticks.map((t) => {\n const p = f(vPos(t));\n return horizontal ? (\n <line key={t} x1={p} x2={p} y1={m.top} y2={m.top + plotH} className=\"stroke-border\" strokeDasharray={t === 0 ? undefined : \"3 3\"} />\n ) : (\n <line key={t} x1={m.left} x2={m.left + plotW} y1={p} y2={p} className=\"stroke-border\" strokeDasharray={t === 0 ? undefined : \"3 3\"} />\n );\n })\n : null}\n {marks}\n {activeDots}\n <g aria-hidden=\"true\" className=\"fill-muted-foreground text-2xs\" data-nums>\n {/* The value axis. */}\n {(horizontal ? xAxis : yAxis)\n ? ticks.map((t, i) =>\n horizontal ? (\n <text key={t} x={vPos(t)} y={height - 6} textAnchor=\"middle\">\n {tickNames[i]}\n </text>\n ) : (\n <text key={t} x={m.left - 8} y={vPos(t)} dy=\"0.32em\" textAnchor=\"end\">\n {tickNames[i]}\n </text>\n ),\n )\n : null}\n {/* The category axis. */}\n {(horizontal ? yAxis : xAxis)\n ? xNames.map((name, i) => {\n if (i % labelEvery !== 0) return null;\n if (horizontal) {\n return (\n <text key={i} x={m.left - 8} y={cPos(i)} dy=\"0.32em\" textAnchor=\"end\">\n {name}\n </text>\n );\n }\n return (\n <text key={i} x={cPos(i)} y={height - 6} textAnchor=\"middle\">\n {name}\n </text>\n );\n })\n : null}\n </g>\n </svg>\n ) : null}\n {tipAt ? (\n <TooltipBox\n at={tipAt}\n title={xNames[active]}\n rows={tipRows}\n footer={stack && live.length > 1 ? { label: \"Total\", value: fmt(total) } : null}\n />\n ) : null}\n <div className=\"sr-only\" aria-live=\"polite\">\n {keyed ? announce : \"\"}\n </div>\n </div>\n {live.length === 0 ? <p className=\"text-center text-xs text-muted-foreground\">Every series is hidden. Press one below to show it.</p> : null}\n {legendNode}\n <DataTable\n caption={label}\n head={[xLabel ?? humanize(x), ...all.map((s) => s.label)]}\n body={rows.map((r, i) => [xNames[i], ...all.map((s) => fmt(num(r?.[s.key])))])}\n />\n </div>\n );\n}\n\n/** Values over an ordered x (time): lines, optionally curved or stepped. */\nexport function LineChart(props) {\n return <Cartesian {...props} kind=\"line\" />;\n}\n\n/** Lines filled to zero; `stacked` piles the series into a total. */\nexport function AreaChart(props) {\n return <Cartesian {...props} kind=\"area\" />;\n}\n\n/** Bars per category: grouped side by side, or `stacked`; columns\n * (`orientation=\"vertical\"`, the default) or bars (`\"horizontal\"`). */\nexport function BarChart(props) {\n return <Cartesian {...props} kind=\"bar\" />;\n}\n"
29
+ },
30
+ {
31
+ "path": "registry/blocks/chart/pie.jsx",
32
+ "type": "registry:component",
33
+ "target": "@components/makersclaw/blocks/chart/pie.jsx",
34
+ "content": "\"use client\";\n\nimport { useMemo, useState } from \"react\";\nimport { cx } from \"../../ui\";\nimport { ChartEmpty, ChartLegend, DataTable, TooltipBox, defaultFormat, humanize, num, tone, toneOf, useReveal, useWidth } from \"./core\";\n\n// PieChart — parts of one whole, one slice per row; `donut` opens the\n// middle and writes the total (or your words) there. Pointer, touch or the\n// arrow keys pick a slice: it lifts, and the tooltip gives its value and\n// share. The legend hides a slice and the rest re-share the whole.\n\nconst TAU = Math.PI * 2;\n\nfunction point(cx0, cy0, r, a) {\n return [cx0 + r * Math.cos(a), cy0 + r * Math.sin(a)];\n}\n\nconst f = (n) => Math.round(n * 100) / 100;\n\nfunction slicePath(c, r, inner, a0, a1) {\n // A whole circle cannot be one arc: draw it as two halves.\n if (a1 - a0 >= TAU - 1e-6) {\n const mid = a0 + Math.PI;\n return slicePath(c, r, inner, a0, mid) + slicePath(c, r, inner, mid, a1);\n }\n const large = a1 - a0 > Math.PI ? 1 : 0;\n const [x0, y0] = point(c.x, c.y, r, a0);\n const [x1, y1] = point(c.x, c.y, r, a1);\n if (!inner) return `M${f(c.x)},${f(c.y)}L${f(x0)},${f(y0)}A${f(r)},${f(r)} 0 ${large} 1 ${f(x1)},${f(y1)}Z`;\n const [x2, y2] = point(c.x, c.y, inner, a1);\n const [x3, y3] = point(c.x, c.y, inner, a0);\n return `M${f(x0)},${f(y0)}A${f(r)},${f(r)} 0 ${large} 1 ${f(x1)},${f(y1)}L${f(x2)},${f(y2)}A${f(inner)},${f(inner)} 0 ${large} 0 ${f(x3)},${f(y3)}Z`;\n}\n\nexport function PieChart({\n data,\n value = \"value\",\n name = \"name\",\n format = defaultFormat,\n formatName = (v) => (v == null ? \"\" : String(v)),\n label = \"Chart\",\n description,\n height = 240,\n donut = false,\n centerLabel,\n totalLabel = \"Total\",\n legend = true,\n legendValues = \"percent\",\n tooltip = true,\n onSelect,\n emptyLabel = \"No data yet\",\n emptyBody,\n className,\n}) {\n const [boxRef, width] = useWidth();\n const [hidden, setHidden] = useState(() => new Set());\n const [active, setActive] = useState(null);\n const [keyed, setKeyed] = useState(false);\n const shown = useReveal(width > 0);\n\n const rows = Array.isArray(data) ? data : [];\n const slices = useMemo(\n () =>\n rows.map((r, i) => ({\n row: r,\n key: String(r?.[name] ?? i),\n label: formatName(r?.[name], r) || humanize(r?.[name]),\n tone: toneOf(r, i),\n value: Math.max(0, num(r?.[value]) ?? 0),\n })),\n [rows, name, value], // eslint-disable-line react-hooks/exhaustive-deps\n );\n const live = slices.filter((s) => !hidden.has(s.key) && s.value > 0);\n const total = live.reduce((sum, s) => sum + s.value, 0);\n const pct = (v) => (total ? `${Math.round((v / total) * 1000) / 10}%` : \"0%\");\n\n const size = Math.max(0, Math.min(width, height));\n const c = { x: width / 2, y: height / 2 };\n const r = Math.max(0, size / 2 - 6);\n const inner = donut ? r * 0.62 : 0;\n\n let a = -Math.PI / 2;\n const arcs = live.map((s) => {\n const span = total ? (s.value / total) * TAU : 0;\n // A gap between slices by angle, not a stroke: no guess at the colour\n // of whatever the chart sits on.\n const pad = live.length > 1 ? Math.min(0.012, span / 4) : 0;\n const out = { ...s, a0: a, a1: a + span, pad };\n a += span;\n return out;\n });\n\n function onKeyDown(e) {\n if (!arcs.length) return;\n let next = null;\n if (e.key === \"ArrowRight\" || e.key === \"ArrowDown\") next = active == null ? 0 : (active + 1) % arcs.length;\n else if (e.key === \"ArrowLeft\" || e.key === \"ArrowUp\") next = active == null ? arcs.length - 1 : (active - 1 + arcs.length) % arcs.length;\n else if (e.key === \"Home\") next = 0;\n else if (e.key === \"End\") next = arcs.length - 1;\n else if (e.key === \"Escape\") return setActive(null);\n else if ((e.key === \"Enter\" || e.key === \" \") && onSelect && active != null) {\n e.preventDefault();\n onSelect(arcs[active].row, active);\n return;\n }\n if (next == null) return;\n e.preventDefault();\n setKeyed(true);\n setActive(next);\n }\n\n const summary = arcs.length\n ? `${arcs.length} parts of ${format(total)}: ${arcs\n .slice()\n .sort((p, q) => q.value - p.value)\n .map((s) => `${s.label} ${format(s.value)} (${pct(s.value)})`)\n .join(\", \")}.`\n : \"\";\n\n const legendNode = legend ? (\n <ChartLegend\n items={slices.map((s) => ({\n key: s.key,\n label: s.label,\n tone: s.tone,\n value: hidden.has(s.key) ? null : legendValues === \"value\" ? format(s.value) : legendValues === \"percent\" ? pct(s.value) : null,\n }))}\n hidden={hidden}\n onToggle={(key) => {\n setActive(null);\n setHidden((h) => {\n const next = new Set(h);\n if (next.has(key)) next.delete(key);\n else next.add(key);\n return next;\n });\n }}\n label={`${label}: parts`}\n />\n ) : null;\n\n if (!rows.length) {\n return (\n <div className={cx(\"grid gap-3\", className)}>\n <ChartEmpty height={height} title={emptyLabel} body={emptyBody} />\n </div>\n );\n }\n\n const hit = active != null ? arcs[active] : null;\n let tipAt = null;\n // A donut says it in its middle; a pie needs the box.\n if (hit && tooltip && !donut) {\n const mid = (hit.a0 + hit.a1) / 2;\n const [tx, ty] = point(c.x, c.y, (r + inner) / 2 + (donut ? 0 : r * 0.15), mid);\n tipAt = { x: tx, y: ty, flip: tx > c.x, middle: true };\n }\n const centre =\n centerLabel !== undefined\n ? typeof centerLabel === \"object\" && centerLabel !== null && !(\"$$typeof\" in centerLabel)\n ? centerLabel\n : { value: centerLabel }\n : { value: format(total), label: totalLabel };\n\n return (\n <div className={cx(\"grid min-w-0 gap-3\", className)}>\n <div\n ref={boxRef}\n tabIndex={0}\n role=\"group\"\n aria-label={`${label}. Arrow keys read each part.`}\n onKeyDown={onKeyDown}\n onFocus={(e) => {\n if (e.currentTarget.matches(\":focus-visible\") && active == null && arcs.length) {\n setKeyed(true);\n setActive(0);\n }\n }}\n onBlur={() => {\n setKeyed(false);\n setActive(null);\n }}\n className=\"relative min-w-0 rounded-md outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n style={{ height }}\n >\n {width > 0 ? (\n <svg\n role=\"img\"\n aria-label={`${label}. ${description ?? summary}`}\n width={width}\n height={height}\n viewBox={`0 0 ${width} ${height}`}\n className={cx(\"block touch-pan-y select-none\", onSelect && \"cursor-pointer\")}\n onPointerLeave={() => !keyed && setActive(null)}\n >\n {/* raw: an SVG drawing — no kit part draws a chart. */}\n {arcs.length === 0 ? (\n <circle cx={c.x} cy={c.y} r={r} className=\"fill-muted\" />\n ) : null}\n <g\n style={{ transform: shown ? \"none\" : \"rotate(-60deg) scale(0.9)\", transformOrigin: `${c.x}px ${c.y}px`, opacity: shown ? 1 : 0 }}\n className=\"motion-safe:transition-all motion-safe:duration-700 motion-safe:ease-out\"\n >\n {arcs.map((s, i) => (\n <path\n key={s.key}\n d={slicePath(c, active === i ? r + 4 : r, inner, s.a0 + s.pad, s.a1 - s.pad)}\n className={cx(tone(s.tone, \"fill\"), active != null && active !== i && \"opacity-60\", \"motion-safe:transition-opacity\")}\n onPointerEnter={() => {\n setKeyed(false);\n setActive(i);\n }}\n onPointerDown={() => setActive(i)}\n onClick={() => onSelect?.(s.row, i)}\n />\n ))}\n </g>\n {donut && centre ? (\n <g aria-hidden=\"true\" textAnchor=\"middle\">\n <text x={c.x} y={c.y} dy={centre.label ? \"-0.1em\" : \"0.35em\"} className=\"fill-foreground text-2xl font-semibold\" data-nums>\n {hit ? format(hit.value) : centre.value}\n </text>\n {centre.label || hit ? (\n <text x={c.x} y={c.y} dy=\"1.6em\" className=\"fill-muted-foreground text-xs\">\n {hit ? `${hit.label} · ${pct(hit.value)}` : centre.label}\n </text>\n ) : null}\n </g>\n ) : null}\n </svg>\n ) : null}\n {tipAt ? <TooltipBox at={tipAt} title={hit.label} rows={[{ key: hit.key, label: pct(hit.value), tone: hit.tone, value: format(hit.value) }]} /> : null}\n <div className=\"sr-only\" aria-live=\"polite\">\n {keyed && hit ? `${hit.label}: ${format(hit.value)}, ${pct(hit.value)}` : \"\"}\n </div>\n </div>\n {legendNode}\n <DataTable\n caption={label}\n head={[\"\", \"Value\", \"Share\"]}\n body={slices.map((s) => [s.label, format(s.value), hidden.has(s.key) ? \"hidden\" : pct(s.value)])}\n />\n </div>\n );\n}\n"
35
+ },
36
+ {
37
+ "path": "registry/blocks/chart/sparkline.jsx",
38
+ "type": "registry:component",
39
+ "target": "@components/makersclaw/blocks/chart/sparkline.jsx",
40
+ "content": "\"use client\";\n\nimport { useId } from \"react\";\nimport { cx } from \"../../ui\";\nimport { defaultFormat, idFrom, num, tone, useReveal, useWidth } from \"./core\";\n\n// Sparkline — a trend with no axes, the size of a line of text: beside a\n// KPI, in a table cell. Its scale is its own low to high (not zero), so\n// the shape shows the change. A screen reader hears the first, last, low\n// and high values; there is nothing to hover.\n\nconst f = (n) => Math.round(n * 100) / 100;\n\nexport function Sparkline({\n data,\n value = \"value\",\n label = \"Trend\",\n format = defaultFormat,\n tone: t = \"chart-1\",\n area = true,\n dot = true,\n height = 36,\n className,\n}) {\n const uid = idFrom(useId());\n const [boxRef, width] = useWidth();\n const shown = useReveal(width > 0);\n\n const vals = (Array.isArray(data) ? data : []).map((d) => num(typeof d === \"object\" && d !== null ? d[value] : d)).filter((v) => v != null);\n const n = vals.length;\n const lo = n ? Math.min(...vals) : 0;\n const hi = n ? Math.max(...vals) : 0;\n const pad = 3;\n const px = (i) => pad + (n === 1 ? (width - pad * 2) / 2 : (i * (width - pad * 2)) / (n - 1));\n const py = (v) => pad + (hi === lo ? (height - pad * 2) / 2 : (1 - (v - lo) / (hi - lo)) * (height - pad * 2));\n const line = vals.map((v, i) => `${i ? \"L\" : \"M\"}${f(px(i))},${f(py(v))}`).join(\"\");\n const words = n\n ? `${label}: from ${format(vals[0])} to ${format(vals[n - 1])}, low ${format(lo)}, high ${format(hi)}.`\n : `${label}: no data yet.`;\n\n return (\n <div ref={boxRef} className={cx(\"min-w-0\", className)} style={{ height }}>\n {width > 0 ? (\n <svg role=\"img\" aria-label={words} width={width} height={height} viewBox={`0 0 ${width} ${height}`} className=\"block overflow-visible\">\n {/* raw: an SVG drawing — no kit part draws a chart. */}\n {n === 0 ? (\n <line x1={pad} x2={width - pad} y1={height / 2} y2={height / 2} className=\"stroke-border\" strokeDasharray=\"3 3\" />\n ) : (\n <>\n <defs>\n <clipPath id={`${uid}-clip`}>\n <rect\n x={0}\n y={-4}\n width={width + 4}\n height={height + 8}\n style={{ transform: shown ? \"none\" : \"scaleX(0)\", transformOrigin: \"0 0\" }}\n className=\"motion-safe:transition-transform motion-safe:duration-700 motion-safe:ease-out\"\n />\n </clipPath>\n {/* An alpha mask, not a colour: the fill fades downward. */}\n <linearGradient id={`${uid}-g`} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0\" stopOpacity=\"1\" />\n <stop offset=\"1\" stopOpacity=\"0\" />\n </linearGradient>\n <mask id={`${uid}-fade`} style={{ maskType: \"alpha\" }} maskUnits=\"userSpaceOnUse\" x={0} y={0} width={width} height={height}>\n <rect x={0} y={0} width={width} height={height} fill={`url(#${uid}-g)`} />\n </mask>\n </defs>\n <g clipPath={`url(#${uid}-clip)`}>\n {area && n > 1 ? (\n <path\n d={`${line}L${f(px(n - 1))},${height}L${f(px(0))},${height}Z`}\n className={tone(t, \"fill\")}\n fillOpacity={0.3}\n mask={`url(#${uid}-fade)`}\n />\n ) : null}\n <path d={line} fill=\"none\" className={tone(t, \"stroke\")} strokeWidth={1.75} strokeLinejoin=\"round\" strokeLinecap=\"round\" />\n {dot ? <circle cx={px(n - 1)} cy={py(vals[n - 1])} r={2.5} className={tone(t, \"fill\")} /> : null}\n </g>\n </>\n )}\n </svg>\n ) : null}\n </div>\n );\n}\n"
41
+ },
42
+ {
43
+ "path": "registry/blocks/chart/heatmap.jsx",
44
+ "type": "registry:component",
45
+ "target": "@components/makersclaw/blocks/chart/heatmap.jsx",
46
+ "content": "\"use client\";\n\nimport { useMemo, useRef, useState } from \"react\";\nimport { Tooltip, cx } from \"../../ui\";\nimport { ChartEmpty, defaultFormat, humanize, num } from \"./core\";\n\n// HeatmapChart — two dimensions as a grid (hour × day, weekday × week),\n// each cell shaded by its value on a sequential ramp of one chart colour.\n// Unlike the other charts it is HTML, not SVG: a real table, so a screen\n// reader walks it by row and column, and each cell is a control the arrow\n// keys move between and the kit's Tooltip names. A grid wider than its box\n// scrolls sideways inside it; the row labels stay put.\n//\n// ScaleLegend is its key (\"Less ▪▪▪▪▪ More\"); scaleStep and scaleShade are\n// the ramp itself, for a block that paints cells of its own.\n\n// ---- the ramp ----------------------------------------------------------------\n\nfunction toneVar(t) {\n const n = typeof t === \"number\" ? t : Number(String(t ?? \"\").replace(/^chart-/, \"\"));\n return `var(--color-chart-${Number.isInteger(n) && n >= 1 && n <= 5 ? n : 1})`;\n}\n\n/**\n * Which of `steps` shades a value falls in: 0 (the lowest) … steps − 1.\n * null for a value that is missing or not a number. Values outside\n * [min, max] take the end shades.\n */\nexport function scaleStep(value, min, max, steps = 5) {\n const v = num(value);\n if (v == null) return null;\n const n = Math.max(1, Math.round(steps) || 1);\n const lo = num(min) ?? 0;\n const hi = num(max) ?? lo;\n if (!(hi > lo)) return v > lo ? n - 1 : 0;\n const t = (v - lo) / (hi - lo);\n return Math.min(n - 1, Math.max(0, Math.floor(t * n)));\n}\n\n// The lowest shade still reads as \"some\"; the highest is the colour itself.\nconst FLOOR = 10;\n\n/** The fill and the text class for one shade of a ramp. */\nfunction shadeFor(step, steps, t) {\n if (step == null) return { style: undefined, className: \"bg-muted/40 text-muted-foreground\" };\n const n = Math.max(1, Math.round(steps) || 1);\n const share = n === 1 ? 1 : step / (n - 1);\n const mix = Math.round(FLOOR + (100 - FLOOR) * share);\n return {\n // The chart colour thinned toward transparent: over a card or the page\n // it reads as the colour mixed with that background.\n style: { backgroundColor: `color-mix(in oklab, ${toneVar(t)} ${mix}%, transparent)` },\n // Dark shades carry the background's colour as text, light ones the\n // foreground's, so a number on a cell stays readable either way.\n className: share >= 0.7 ? \"text-background\" : \"text-foreground\",\n };\n}\n\n/**\n * The shade a value wears, for a cell drawn outside HeatmapChart:\n * const s = scaleShade(12, { min: 0, max: 40 });\n * <span style={s.style} className={cx(\"size-3 rounded-sm\", s.className)} />\n * → { step, style, className } — `style` is the fill, `className` the text\n * colour that reads on it (and a muted fill when the value is missing).\n */\nexport function scaleShade(value, { min = 0, max = 1, steps = 5, tone = \"chart-1\" } = {}) {\n const step = scaleStep(value, min, max, steps);\n return { step, ...shadeFor(step, steps, tone) };\n}\n\n/**\n * The key to a heatmap: the words (or numbers) at the ends and one swatch\n * per shade between them.\n * labels [\"Less\", \"More\"], or leave out for `format(min)` … `format(max)`\n */\nexport function ScaleLegend({\n min = 0,\n max = 1,\n steps = 5,\n labels,\n tone = \"chart-1\",\n format = defaultFormat,\n shape = \"square\",\n label = \"Scale\",\n className,\n}) {\n const n = Math.max(1, Math.round(steps) || 1);\n const ends = [labels?.[0] ?? min, labels?.[1] ?? max].map((v) => (typeof v === \"number\" ? format(v) : v));\n return (\n <div\n role=\"img\"\n aria-label={`${label}: ${n} shades from ${ends[0]} to ${ends[1]}`}\n className={cx(\"inline-flex items-center gap-1.5 text-xs text-muted-foreground\", className)}\n >\n <span data-nums>{ends[0]}</span>\n <span className=\"flex items-center gap-0.5\">\n {Array.from({ length: n }, (_, i) => (\n <span\n key={i}\n style={shadeFor(i, n, tone).style}\n className={cx(\"h-3 shrink-0\", shape === \"pill\" ? \"w-3 rounded-full\" : \"w-4 rounded-sm\")}\n />\n ))}\n </span>\n <span data-nums>{ends[1]}</span>\n </div>\n );\n}\n\n// ---- the chart ---------------------------------------------------------------\n\nfunction normalizeColumns(columns) {\n return (columns ?? []).filter((c) => c != null).map((c) =>\n typeof c === \"string\" || typeof c === \"number\"\n ? { key: String(c), label: humanize(c) || String(c) }\n : { ...c, key: String(c.key), label: c.label ?? (humanize(c.key) || String(c.key)) },\n );\n}\n\nconst sum = (vals) => {\n const nums = vals.map(num).filter((v) => v != null);\n return nums.length ? nums.reduce((a, b) => a + b, 0) : null;\n};\n\nexport function HeatmapChart({\n data,\n y = \"name\",\n columns,\n formatY = (v) => (v == null ? \"—\" : String(v)),\n tone = \"chart-1\",\n min,\n max,\n steps = 5,\n showValues = false,\n format = defaultFormat,\n cellShape = \"square\",\n rowTotals = false,\n colTotals = false,\n totalLabel = \"Total\",\n tooltip = true,\n formatTooltip,\n legend = true,\n legendLabels,\n onSelect,\n label = \"Heatmap\",\n description,\n yLabel,\n emptyLabel = \"No data yet\",\n emptyBody,\n className,\n}) {\n const bodyRef = useRef(null);\n const [active, setActive] = useState([0, 0]);\n\n const rows = Array.isArray(data) ? data : [];\n const cols = useMemo(() => normalizeColumns(columns), [columns]);\n\n const range = useMemo(() => {\n let lo = Infinity;\n let hi = -Infinity;\n for (const r of rows) for (const c of cols) {\n const v = num(r?.[c.key]);\n if (v == null) continue;\n if (v < lo) lo = v;\n if (v > hi) hi = v;\n }\n if (lo === Infinity) return { lo: 0, hi: 1 };\n return { lo: Math.min(0, lo), hi };\n }, [rows, cols]);\n const lo = num(min) ?? range.lo;\n const hi = num(max) ?? range.hi;\n\n if (!rows.length || !cols.length) {\n return (\n <div className={className}>\n <ChartEmpty height={160} title={emptyLabel} body={emptyBody} />\n </div>\n );\n }\n\n const nr = rows.length;\n const nc = cols.length;\n const pill = cellShape === \"pill\";\n const rowName = (r) => formatY(r?.[y], r);\n const tipOf = (v, r, c) =>\n formatTooltip ? formatTooltip(v, r, c) : `${rowName(r)} · ${c.label}: ${v == null ? \"no data\" : format(v)}`;\n\n const [ar, ac] = [Math.min(active[0], nr - 1), Math.min(active[1], nc - 1)];\n\n function go(r, c) {\n const nextR = Math.max(0, Math.min(nr - 1, r));\n const nextC = Math.max(0, Math.min(nc - 1, c));\n setActive([nextR, nextC]);\n bodyRef.current?.querySelector(`[data-cell=\"${nextR}-${nextC}\"]`)?.focus();\n }\n\n function onKeyDown(e) {\n const at = e.target.closest?.(\"[data-cell]\")?.getAttribute(\"data-cell\");\n if (!at) return;\n const [r, c] = at.split(\"-\").map(Number);\n const moves = {\n ArrowRight: [r, c + 1],\n ArrowLeft: [r, c - 1],\n ArrowDown: [r + 1, c],\n ArrowUp: [r - 1, c],\n Home: e.ctrlKey || e.metaKey ? [0, 0] : [r, 0],\n End: e.ctrlKey || e.metaKey ? [nr - 1, nc - 1] : [r, nc - 1],\n };\n if (moves[e.key]) {\n e.preventDefault();\n go(...moves[e.key]);\n } else if ((e.key === \"Enter\" || e.key === \" \") && onSelect) {\n e.preventDefault();\n onSelect(rows[r], cols[c].key, num(rows[r]?.[cols[c].key]));\n }\n }\n\n // What a screen reader hears after the label: the size, the range, the peak.\n let peak = null;\n for (const r of rows) for (const c of cols) {\n const v = num(r?.[c.key]);\n if (v != null && (!peak || v > peak.v)) peak = { v, r, c };\n }\n const said =\n description ??\n `${nr} rows by ${nc} columns, values from ${format(lo)} to ${format(hi)}${peak ? `; highest ${format(peak.v)} at ${rowName(peak.r)}, ${peak.c.label}` : \"\"}.`;\n\n const cellBox = pill ? \"h-5 min-w-6 rounded-full px-1\" : \"h-8 min-w-9 rounded-md px-1\";\n const totalCell = \"px-1.5 text-right text-xs font-semibold text-foreground whitespace-nowrap\";\n const colSums = colTotals ? cols.map((c) => sum(rows.map((r) => r?.[c.key]))) : null;\n\n return (\n <div className={cx(\"grid gap-3\", className)}>\n <div className=\"overflow-x-auto\">\n <table className=\"w-full border-separate border-spacing-1\">\n <caption className=\"sr-only\">\n {label}. {said}\n </caption>\n <thead>\n <tr>\n <th scope=\"col\" className=\"sticky left-0 z-[1] bg-card\">\n <span className=\"sr-only\">{yLabel ?? (humanize(y) || \"Row\")}</span>\n </th>\n {cols.map((c) => (\n <th key={c.key} scope=\"col\" className=\"px-0.5 text-center text-2xs font-normal whitespace-nowrap text-muted-foreground\">\n {c.label}\n </th>\n ))}\n {rowTotals ? (\n <th scope=\"col\" className=\"px-1.5 text-right text-2xs font-semibold text-foreground\">\n {totalLabel}\n </th>\n ) : null}\n </tr>\n </thead>\n <tbody ref={bodyRef} onKeyDown={onKeyDown}>\n {rows.map((r, ri) => (\n <tr key={`${ri}-${String(r?.[y])}`}>\n <th\n scope=\"row\"\n className=\"sticky left-0 z-[1] bg-card pr-2 text-left text-xs font-normal whitespace-nowrap text-muted-foreground\"\n >\n {rowName(r)}\n </th>\n {cols.map((c, ci) => {\n const v = num(r?.[c.key]);\n const shade = shadeFor(scaleStep(v, lo, hi, steps), steps, tone);\n const tip = tipOf(v, r, c);\n const cell = (\n <div\n data-cell={`${ri}-${ci}`}\n tabIndex={ri === ar && ci === ac ? 0 : -1}\n aria-label={tip}\n onFocus={() => setActive([ri, ci])}\n onClick={onSelect ? () => onSelect(r, c.key, v) : undefined}\n style={shade.style}\n className={cx(\n \"flex w-full items-center justify-center text-2xs font-medium outline-none\",\n \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background\",\n onSelect && \"cursor-pointer\",\n cellBox,\n shade.className,\n )}\n >\n {showValues && v != null ? <span data-nums aria-hidden=\"true\">{format(v)}</span> : null}\n </div>\n );\n return (\n <td key={c.key} className=\"p-0 [&>span]:flex\">\n {tooltip ? (\n <Tooltip label={tip} delay={150}>\n {cell}\n </Tooltip>\n ) : (\n cell\n )}\n </td>\n );\n })}\n {rowTotals ? (\n <td data-nums className={totalCell}>\n {format(sum(cols.map((c) => r?.[c.key])) ?? 0)}\n </td>\n ) : null}\n </tr>\n ))}\n </tbody>\n {colTotals ? (\n <tfoot>\n <tr>\n <th scope=\"row\" className=\"sticky left-0 z-[1] bg-card pr-2 text-left text-xs font-semibold text-foreground\">\n {totalLabel}\n </th>\n {colSums.map((s, i) => (\n <td key={cols[i].key} data-nums className=\"px-0.5 text-center text-2xs font-semibold text-foreground whitespace-nowrap\">\n {format(s ?? 0)}\n </td>\n ))}\n {rowTotals ? (\n <td data-nums className={totalCell}>\n {format(sum(colSums) ?? 0)}\n </td>\n ) : null}\n </tr>\n </tfoot>\n ) : null}\n </table>\n </div>\n {legend ? (\n <div className=\"flex justify-end\">\n <ScaleLegend min={lo} max={hi} steps={steps} tone={tone} labels={legendLabels} format={format} shape={cellShape} />\n </div>\n ) : null}\n </div>\n );\n}\n"
47
+ },
48
+ {
49
+ "path": "registry/blocks/chart/README.md",
50
+ "type": "registry:file",
51
+ "target": "@components/makersclaw/blocks/chart/README.md",
52
+ "content": "# Chart\n\nNumbers as a shape: `LineChart`, `AreaChart` (stacked or not), `BarChart`\n(grouped or stacked, columns or bars), `PieChart` (a pie or a donut with\nthe total in its middle), `Sparkline` (a trend beside a KPI) and\n`HeatmapChart` (a grid of two dimensions, each cell shaded by its value,\nwith `ScaleLegend` as its key). Drawn in SVG (the heatmap as an HTML\ntable), no chart library, in the workspace's chart colours. Each one is as\nwide as its container and redraws when that changes. Hover, touch or the\narrow keys show a tooltip with a crosshair; the legend hides and shows a\nseries; a screen reader gets a summary and the numbers as a table; with\nreduced motion nothing animates.\n\n```jsx\nimport { AreaChart, BarChart, HeatmapChart, PieChart, Sparkline } from \"../components/blocks/chart\"; // a Makersclaw app (\"./blocks/chart\" from components/)\nimport { AreaChart, BarChart, HeatmapChart, PieChart, Sparkline } from \"@/components/makersclaw/blocks/chart\"; // a shadcn project\n```\n\nThey are client components: render them from a `\"use client\"` component,\nand load the rows in the server surface (README §4). Put a chart in a\n`Card` — the card gives the title; the chart is only the picture.\n\n## The data\n\nRows, as `loadRows` returns them. One key holds the x (a month, a stage,\na week) and each series is another key holding a number, in the order the\nx should read — sort in `loadRows`. A value that is missing or not a number\nis a gap in a line and nothing in a bar or an area.\n\n```js\n[{ month: \"Jan\", newBiz: 24600, expansion: 8100 }, { month: \"Feb\", newBiz: 26900, expansion: 8800 }]\n```\n\nA pie takes one row per slice: a key for the name, a key for the value.\n\nA heatmap takes one row per grid row: a key for the row's name and one key\nper column holding its number — a pivot, shaped in the query:\n\n```js\n[{ slot: \"9–12\", mon: 42, tue: 51, wed: 38 }, { slot: \"12–15\", mon: 18, tue: 24, wed: 65 }]\n```\n\n## Colours\n\n`series[].tone` picks a colour: `\"chart-1\"` … `\"chart-5\"` (or `1` … `5`).\nLeft out, the series take chart-1, chart-2, … in order. The chart colours\ncome from the workspace theme (`app/globals.css`, `--color-chart-N`): the\nfirst is the theme's primary, the others turn its hue so neighbours stay\napart in every theme. A series that means a status may take a theme tone\ninstead: `\"success\"`, `\"warning\"`, `\"danger\"`, `\"info\"`, `\"primary\"`,\n`\"muted\"`. In your own markup around a chart use the same classes:\n`bg-chart-1`, `text-chart-2`, `fill-chart-3`, `stroke-chart-4`.\n\n## Props\n\n### LineChart · AreaChart · BarChart\n\n- `data` — the rows. Empty (or missing) shows the empty state.\n- `x` — the key that holds each row's x, e.g. `\"month\"`.\n- `series` — what to draw, in order: `[{ key, label?, tone?, muted?,\n dashed? }]`. `label` defaults to the key, humanised.\n - `dashed` — LineChart and AreaChart: a dashed line with no fill under\n it. The key and the tooltip show a dashed swatch.\n - `muted` — drawn quieter and underneath the rest: the grey (unless it\n has its own `tone`), a thinner line, a fainter fill. On a BarChart it\n is only the grey.\n - Together they are a \"previous period\" to compare against:\n\n ```jsx\n <LineChart\n data={days} // [{ day: \"Mon\", thisWeek: 42, lastWeek: 35 }, …]\n x=\"day\"\n series={[{ key: \"thisWeek\", label: \"This week\" }, { key: \"lastWeek\", label: \"Last week\", muted: true, dashed: true }]}\n label=\"Signups, this week against last\"\n />\n ```\n- `format(value)` — how a value reads in the tooltip, the summary and the\n table. Default: `1,250` · `48.2K`.\n- `formatAxis(value)` — how a tick reads (default `format`); give a\n shorter one when `format` is long (`$48,200` → `$48K`).\n- `formatX(xValue, row)` — how an x reads (default: as written).\n- `label` — what the chart is, for a screen reader: `\"Revenue by month\"`.\n Default `\"Chart\"`; always set it.\n- `description` — the sentence a screen reader hears after `label`.\n Default: made from the data (the range, each series' first, last and\n highest value).\n- `xLabel` — the x column's heading in the hidden table (default: the key,\n humanised).\n- `height` — pixels, default `240`. The width is the container's.\n- `stacked` — AreaChart and BarChart: pile the series (negatives stack\n down). The tooltip adds a total. Default `false`.\n- `orientation` — BarChart: `\"vertical\"` (columns, the default) or\n `\"horizontal\"` (bars, categories down the left — for long names).\n- `valueLabels` — BarChart: each bar's value at its end (\"10%\"), in\n `format`; stacked, the stack's total. `true`, or `(value, row, key)` →\n the words (`key` is null for a total). With it the value axis is often\n noise: `xAxis={false}` (horizontal) or `yAxis={false}`. Default `false`.\n- `track` — BarChart: a quiet full-length track behind every bar, for a\n share or a distribution (a status's share of tickets). Pair it with\n `grid={false}`. Default `false`.\n- `curve` — LineChart and AreaChart: `\"monotone\"` (default, smooth and\n never overshooting), `\"linear\"` or `\"step\"`.\n- `dots` — LineChart and AreaChart: a dot on every point. Default `false`.\n- `grid` · `xAxis` · `yAxis` — gridlines and the two axes' labels. All\n default `true`.\n- `legend` — the legend under the chart; its entries are toggles that hide\n a series. Default: shown when there is more than one series.\n- `tooltip` — default `true`.\n- `onSelect(row, index)` — a click, a tap or Enter on a point or a bar\n calls it: open the month, filter by the stage.\n- `emptyLabel` · `emptyBody` — what the empty state says. Default\n `\"No data yet\"`.\n- `className` — layout around it (width, margin). Never colour.\n\n### PieChart\n\n- `data` — one row per slice.\n- `name` — the key holding a slice's name, default `\"name\"`.\n- `value` — the key holding its number, default `\"value\"`. Zero and\n negative values draw nothing.\n- `donut` — open the middle. It shows the total, and the picked slice's\n value and share while one is picked. Default `false`.\n- `centerLabel` — a donut's middle instead of the total: a string, or\n `{ value, label }`.\n- `totalLabel` — the words under the total, default `\"Total\"`.\n- `format(value)` · `formatName(name, row)` · `label` · `description` ·\n `height` · `tooltip` · `onSelect(row, index)` · `emptyLabel` ·\n `emptyBody` · `className` — as above.\n- `legend` — default `true`; hiding a slice re-shares the rest.\n- `legendValues` — what follows each name in the legend: `\"percent\"`\n (default), `\"value\"` or `\"none\"`.\n- A row's own `tone` colours its slice; otherwise chart-1 … chart-5 in\n order (a sixth slice repeats chart-1 — five is the most a pie reads).\n\n### Sparkline\n\nA trend with no axes, gridlines, legend or tooltip, as wide as its box\nand `height` tall — small enough to sit under a kit `Stat` in a tile:\n\n```jsx\n<Card>\n <Stat label=\"MRR\" value=\"$84.2k\" delta={6.1} />\n <Sparkline data={mrr} label=\"MRR, last 12 months\" height={28} className=\"mt-2\" />\n</Card>\n```\n\n- `data` — numbers (`[12, 15, 14]`) or rows with `value`.\n- `value` — the key when `data` is rows, default `\"value\"`.\n- `label` — what it shows, e.g. `\"MRR, last 12 months\"`: a screen reader\n hears it with the first, last, low and high values. Always set it.\n- `format(value)` — for those words.\n- `tone` — default `\"chart-1\"`.\n- `area` — the faded fill under the line, default `true`.\n- `dot` — a dot on the latest value, default `true`.\n- `height` — default `36`. The width is the container's.\n- `className` — layout. Never colour.\n\n### HeatmapChart\n\n- `data` — one row per grid row. Empty (or no `columns`) shows the empty\n state.\n- `y` — the key holding each row's name, default `\"name\"`.\n- `columns` — the cells of a row, in order: `[\"mon\", \"tue\"]` (labels made\n from the keys) or `[{ key: \"mon\", label: \"Mon\" }, …]`.\n- `formatY(yValue, row)` — how a row's name reads (default: as written).\n- `tone` — the ramp's colour: `\"chart-1\"` (default) … `\"chart-5\"` (or\n `1` … `5`). One colour, light to full: a heatmap is one measure.\n- `min` · `max` — the ends of the ramp. Default: 0 (or the lowest value,\n when negative) and the highest value. Values past them take the end\n shades.\n- `steps` — how many shades, default `5`. Each cell takes the shade its\n value falls in (`scaleStep`), so equal shades mean equal bands.\n- `showValues` — the number on each cell, in `format`; on dark shades it\n is drawn in the background's colour so it stays readable. Default\n `false` (a compact grid of colour).\n- `format(value)` — how a value reads on the cell, in the tooltip, the\n totals and the legend. Default: `1,250` · `48.2K`.\n- `cellShape` — `\"square\"` (default, rounded) or `\"pill\"` (short and\n fully round — the contribution grid).\n- `rowTotals` · `colTotals` — a bold Total column at the right, a bold\n Total row at the foot (and the grand total where they meet). Missing\n values count as nothing. Default `false`.\n- `totalLabel` — default `\"Total\"`.\n- `tooltip` — the kit `Tooltip` on hover or keyboard focus. Default `true`.\n- `formatTooltip(value, row, column)` — its words (a string); `column` is\n `{ key, label }` and `value` is null when the cell has none. Default:\n `\"Tue · 9–12: 51\"`. The same words name the cell to a screen reader.\n- `legend` — a `ScaleLegend` under the grid, at the right. Default `true`.\n Set it `false` to place your own (in the card's header, say).\n- `legendLabels` — its end words, e.g. `[\"Less\", \"More\"]`; default the\n numbers at the two ends.\n- `onSelect(row, columnKey, value)` — a click, or Enter or Space on a\n focused cell.\n- `label` · `description` — as above; the description defaults to the\n size, the range and the highest cell.\n- `yLabel` — the row names' heading for a screen reader (default: the key,\n humanised).\n- `emptyLabel` · `emptyBody` · `className` — as above.\n\nThe grid is a table: a screen reader reads each cell with its row and\ncolumn. Tab lands on one cell; the arrow keys move, Home and End go to the\nrow's ends (with Ctrl or ⌘, the grid's). A grid wider than its box\nscrolls sideways inside it, and the row names stay at the left — on the\ncard's background, so put the heatmap in a `Card`.\n\n### ScaleLegend\n\nThe heatmap's key, for a legend placed yourself: `Less ▪▪▪▪▪ More`.\n\n- `min` · `max` — the ends, default `0` and `1`.\n- `steps` — the shades, default `5` (match the chart's).\n- `labels` — `[\"Less\", \"More\"]`, or leave it out for the numbers at the\n ends, in `format`.\n- `tone` · `format` — as the chart's.\n- `shape` — `\"square\"` (default) or `\"pill\"`, as the chart's cells.\n- `label` — what a screen reader hears it called, default `\"Scale\"`.\n\n### scaleStep · scaleShade\n\nThe ramp itself, for cells a block draws on its own.\n\n- `scaleStep(value, min, max, steps = 5)` → which shade a value falls in:\n `0` … `steps − 1`, or `null` for a missing value.\n- `scaleShade(value, { min, max, steps, tone })` → `{ step, style,\n className }`: put `style` (the fill) and `className` (the text colour\n that reads on it) on your element.\n\n### ChartLegend\n\nThe key the charts draw, for a legend of your own (several sparklines\nsharing one key, say).\n\n- `items` — `[{ key, label, tone?, value? }]`; `value` is a muted note\n after the label.\n- `hidden` — a `Set` (or array) of keys shown as off.\n- `onToggle(key)` — makes each entry a toggle; without it the legend is a\n plain list.\n- `label` — what it is called to a screen reader, default `\"Legend\"`.\n\n## Example\n\n```jsx\n// surfaces/sales.jsx — server: load, hand over\nimport { loadRows } from \"../lib/data\";\nimport SalesCharts from \"../components/SalesCharts\";\n\nexport default async function SalesSurface() {\n const { rows: months, error } = await loadRows({ source: \"public.revenue_by_month\", orderBy: \"month asc\" });\n const { rows: stages } = await loadRows({ source: \"public.deals_by_stage\", orderBy: \"position asc\" });\n const { rows: visits } = await loadRows({ source: \"public.visits_by_slot\", orderBy: \"position asc\" });\n return <SalesCharts months={months} stages={stages} visits={visits} error={error} />;\n}\n```\n\n```jsx\n// components/SalesCharts.jsx — client: the cards and the charts\n\"use client\";\nimport { Card, Notice } from \"./ui\";\nimport { AreaChart, BarChart, HeatmapChart, Sparkline } from \"./blocks/chart\";\n\nconst usd = new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency: \"USD\", maximumFractionDigits: 0 });\nconst usdShort = new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency: \"USD\", notation: \"compact\" });\n\nexport default function SalesCharts({ months, stages, visits, error }) {\n return (\n <div className=\"grid gap-4\">\n {error ? <Notice tone=\"danger\">{error}</Notice> : null}\n <Card title=\"MRR\">\n <Sparkline data={months} value=\"mrr\" label=\"MRR, last 12 months\" format={(v) => usd.format(v)} />\n </Card>\n <Card title=\"Revenue by month\">\n <AreaChart\n data={months}\n x=\"month\"\n series={[{ key: \"new_biz\", label: \"New business\" }, { key: \"expansion\", label: \"Expansion\" }]}\n stacked\n format={(v) => usd.format(v)}\n formatAxis={(v) => usdShort.format(v)}\n label=\"Revenue by month\"\n />\n </Card>\n <Card title=\"Deals by stage\">\n <BarChart data={stages} x=\"stage\" series={[{ key: \"deals\", label: \"Deals\" }]} orientation=\"horizontal\" label=\"Deals by stage\" />\n </Card>\n <Card title=\"Site visits by time of day\">\n <HeatmapChart\n data={visits}\n y=\"slot\"\n columns={[{ key: \"mon\", label: \"Mon\" }, { key: \"tue\", label: \"Tue\" }, { key: \"wed\", label: \"Wed\" }]}\n showValues\n rowTotals\n colTotals\n formatTooltip={(v, row, col) => `${col.label} ${row.slot}: ${v ?? 0} visits`}\n label=\"Site visits by weekday and time slot\"\n />\n </Card>\n </div>\n );\n}\n```\n\n## What it will not do\n\n- Sort, group or sum: the rows come shaped. Totals by month or by stage\n are a query (a view, or `loadRows` over one).\n- A time scale: x is a list of labels, evenly spaced, in row order. Give\n every period a row (a month with no sales is a row with 0).\n- Two value axes, scatter, radar, maps, zoom or brushing.\n- A colour outside the theme: series take the chart tones and the theme's\n status tones only. Colour by value is the heatmap's alone, on one chart\n tone.\n- Draw its own title: put it in a `Card`.\n\nA chart it cannot be is your own component in `components/`, built from\nthe kit — not an edit to this block. Its files are fixed: `npm run check`\nrefuses a change, and `makersclaw add chart` restores them.\n"
53
+ }
54
+ ],
55
+ "categories": [
56
+ "chart",
57
+ "graph",
58
+ "line",
59
+ "area",
60
+ "bar",
61
+ "pie",
62
+ "donut",
63
+ "sparkline",
64
+ "analytics",
65
+ "dashboard",
66
+ "kpi",
67
+ "trend",
68
+ "comparison",
69
+ "previous period",
70
+ "metrics",
71
+ "report",
72
+ "heatmap",
73
+ "grid",
74
+ "scale"
75
+ ],
76
+ "docs": "Installed to components/makersclaw/blocks/chart. Import: import { LineChart, AreaChart, BarChart, PieChart, Sparkline, ChartLegend, HeatmapChart, ScaleLegend, scaleStep, scaleShade } from \"@/components/makersclaw/blocks/chart\". Props and an example: components/makersclaw/blocks/chart/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
77
+ "meta": {
78
+ "makersclaw": {
79
+ "version": "0.3.1",
80
+ "useWhen": "Numbers the founder should see as a shape: revenue by month, deals by stage, pipeline by source, a KPI's trend. Not for a single figure with no history (a stat), exact lookups (a table), or records on a timeline (a calendar)."
81
+ }
82
+ }
83
+ }
package/r/chat.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "chat",
4
+ "type": "registry:block",
5
+ "title": "Chat",
6
+ "description": "The parts of a chat: messages with avatars, times, grouping, sending/failed/streaming states, hover actions, thumbs up/down feedback, in-place editing and a folding reasoning section; a scroller that sticks to the bottom with day separators and older-message loading; a composer (a box or an \"ask anything\" pill) with attachments, a + menu, tools, suggestion chips, Enter to send and Stop; a header; and ChatThread, which composes them from messages + onSend.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.1/r/core.json"
9
+ ],
10
+ "dependencies": [],
11
+ "files": [
12
+ {
13
+ "path": "registry/blocks/chat/index.js",
14
+ "type": "registry:component",
15
+ "target": "@components/makersclaw/blocks/chat/index.js",
16
+ "content": "// The Chat block — `makersclaw add chat`. Fixed: compose it through its\n// props (README.md beside this file); a chat it cannot be is your own\n// component in components/, built from these parts.\nexport { ChatThread } from \"./chat\";\nexport { ChatHeader } from \"./header\";\nexport { Message, Bubble } from \"./message\";\nexport { Attachment } from \"./attachment\";\nexport { MessageScroller } from \"./scroller\";\nexport { Composer } from \"./composer\";\n"
17
+ },
18
+ {
19
+ "path": "registry/blocks/chat/chat.jsx",
20
+ "type": "registry:component",
21
+ "target": "@components/makersclaw/blocks/chat/chat.jsx",
22
+ "content": "\"use client\";\n\nimport { isValidElement, useState } from \"react\";\nimport { Empty, cx } from \"../../ui\";\nimport { Composer } from \"./composer\";\nimport { ChatHeader } from \"./header\";\nimport { Message } from \"./message\";\nimport { MessageScroller } from \"./scroller\";\n\n// ChatThread — a whole conversation from `messages` and `onSend`: the\n// scrolling list (MessageScroller) of Message rows, and the Composer under\n// it; a header over it when given one. The common case in one element;\n// when a thread needs a shape this cannot take, compose the same parts\n// yourself (README.md).\n//\n// <ChatThread messages={messages} onSend={send} streaming={busy} onStop={stop}\n// onRetry={resend} assistantName=\"Support\" height=\"36rem\" />\n\nexport function ChatThread({\n messages,\n onSend,\n onStop,\n streaming,\n onRetry,\n onRegenerate,\n actionsOf,\n onFeedback,\n onEdit,\n onLoadMore,\n hasMore = false,\n loadingMore = false,\n attachments,\n onAttach,\n onRemoveAttachment,\n accept,\n disabled = false,\n disabledPlaceholder,\n placeholder,\n variant = \"plain\",\n assistantName = \"Assistant\",\n assistantAvatar,\n userName = \"You\",\n userAvatar,\n showUserAvatar = false,\n emptyTitle = \"No messages yet\",\n emptyBody = \"Write the first one below.\",\n emptyState,\n header,\n shape = \"box\",\n tools,\n menu,\n suggestions,\n suggestionsWhen = \"empty\",\n suggestionsLook = \"chips\",\n suggestionMode = \"send\",\n label = \"Conversation\",\n height = \"min(40rem, 80dvh)\",\n className,\n}) {\n const list = messages ?? [];\n // Streaming when told so, or when the last message says it still is.\n const busy = streaming ?? list.some((m) => m.status === \"streaming\");\n // The message being edited: one at a time, and ArrowUp in an empty\n // composer opens the last one you sent.\n const [editingId, setEditingId] = useState(null);\n const lastMine = onEdit ? [...list].reverse().find((m) => m.role === \"user\" && m.status !== \"sending\") : null;\n const showSuggestions = suggestions && suggestions.length && (suggestionsWhen === \"always\" || list.length === 0);\n\n const top = !header ? null : isValidElement(header) || typeof header !== \"object\" ? header : <ChatHeader {...header} />;\n\n return (\n <section\n aria-label={label}\n style={height ? { height } : undefined}\n className={cx(\"flex min-h-0 flex-col overflow-hidden rounded-lg border border-border bg-card text-card-foreground\", className)}\n >\n {top}\n <MessageScroller\n messages={list}\n onLoadMore={onLoadMore}\n hasMore={hasMore}\n loadingMore={loadingMore}\n label={`${label} messages`}\n className=\"flex-1\"\n emptyState={emptyState ?? <Empty icon=\"mail\" title={emptyTitle} body={emptyBody} className=\"my-auto\" />}\n renderMessage={(m, ctx) => (\n <Message\n message={m}\n first={ctx.first}\n last={ctx.last}\n latest={ctx.latest}\n variant={variant}\n assistantName={assistantName}\n assistantAvatar={assistantAvatar}\n userName={userName}\n userAvatar={userAvatar}\n showUserAvatar={showUserAvatar}\n onRetry={onRetry}\n onRegenerate={onRegenerate}\n actionsOf={actionsOf}\n onFeedback={onFeedback}\n onEdit={onEdit}\n editing={onEdit ? editingId === m.id : undefined}\n onEditingChange={(next) => setEditingId(next ? m.id : null)}\n />\n )}\n />\n <div className=\"border-t border-border p-2 sm:p-3\">\n <Composer\n onSend={onSend}\n onStop={onStop}\n streaming={busy}\n disabled={disabled}\n disabledPlaceholder={disabledPlaceholder}\n placeholder={placeholder}\n attachments={attachments}\n onAttach={onAttach}\n onRemoveAttachment={onRemoveAttachment}\n accept={accept}\n shape={shape}\n tools={tools}\n menu={menu}\n suggestions={showSuggestions ? suggestions : undefined}\n suggestionsLook={suggestionsLook}\n suggestionsPlacement=\"above\"\n suggestionMode={suggestionMode}\n onEditLast={lastMine && !disabled && !busy ? () => setEditingId(lastMine.id) : undefined}\n />\n </div>\n </section>\n );\n}\n"
23
+ },
24
+ {
25
+ "path": "registry/blocks/chat/header.jsx",
26
+ "type": "registry:component",
27
+ "target": "@components/makersclaw/blocks/chat/header.jsx",
28
+ "content": "\"use client\";\n\nimport { Avatar, Menu, StatusDot, cx } from \"../../ui\";\n\n// ChatHeader — the strip over a conversation: who you are talking to\n// (an avatar, a name, a line under it or a live status), then the\n// surface's own actions (\"New chat\") and a menu of the rest. ChatThread\n// draws it when given `header={{ title, … }}`; it is exported for a\n// thread you compose yourself.\n//\n// <ChatHeader title=\"Ada\" status={{ tone: \"success\", label: \"Online\" }}\n// actions={<Button size=\"sm\" variant=\"quiet\" icon=\"plus\" onClick={reset}>New chat</Button>} />\n\nexport function ChatHeader({ title, subtitle, status, avatar, actions, menu, menuLabel = \"Conversation options\", className }) {\n return (\n <header className={cx(\"flex min-h-14 items-center gap-3 border-b border-border px-3 py-2 sm:px-4\", className)}>\n {avatar ? <Avatar name={avatar.name ?? title} src={avatar.src} size=\"sm\" shape=\"square\" status={avatar.status} /> : null}\n <div className=\"min-w-0 flex-1\">\n {title ? <h2 className=\"truncate text-sm font-semibold leading-tight\">{title}</h2> : null}\n {status ? (\n <StatusDot tone={status.tone} size=\"sm\" pulse={status.pulse} label={status.label} className=\"max-w-full text-xs\" />\n ) : subtitle ? (\n <p className=\"truncate text-xs text-muted-foreground\">{subtitle}</p>\n ) : null}\n </div>\n {actions || (menu && menu.length) ? (\n <div className=\"flex shrink-0 items-center gap-1\">\n {actions}\n <Menu items={menu} label={menuLabel} />\n </div>\n ) : null}\n </header>\n );\n}\n"
29
+ },
30
+ {
31
+ "path": "registry/blocks/chat/message.jsx",
32
+ "type": "registry:component",
33
+ "target": "@components/makersclaw/blocks/chat/message.jsx",
34
+ "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { Avatar, Button, Collapsible, IconButton, Icon, Menu, Spinner, Textarea, Toggle, Tooltip, cx } from \"../../ui\";\nimport { Attachment } from \"./attachment\";\nimport { ChatText } from \"./text\";\n\n// Message and Bubble — one turn of a conversation. A message is its\n// author (avatar, name, time — shown once for a run of messages from the\n// same author), its words in a Bubble, its files, its state (sending,\n// streaming, failed with Retry) and the actions that appear on hover or\n// focus (copy, retry, a Menu of the surface's own). An assistant reply can\n// carry its reasoning (folded under \"Thought for 4s\") and take a thumbs up\n// or down; a user message can be edited in place.\n\n/** The text of a message: `content` when it is a string, else the text\n * parts of `parts` / `content` ([{ type: \"text\", text }]), joined. */\nexport function textOf(message) {\n const c = message?.content;\n if (typeof c === \"string\") return c;\n const parts = Array.isArray(c) ? c : Array.isArray(message?.parts) ? message.parts : [];\n return parts\n .filter((p) => p && (p.type === \"text\" || typeof p === \"string\"))\n .map((p) => (typeof p === \"string\" ? p : p.text ?? \"\"))\n .join(\"\");\n}\n\nexport function toDate(value) {\n if (value == null || value === \"\") return null;\n const d = value instanceof Date ? value : new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n}\n\nexport function formatTime(date) {\n return date.toLocaleTimeString(undefined, { hour: \"numeric\", minute: \"2-digit\" });\n}\n\n/**\n * The container for a message's words.\n * from user · assistant — user bubbles are filled and sit at the end\n * variant bubble · plain (assistant text on the page, no box)\n * first / last its place in a run from one author: the corner on the\n * author's side tightens between messages of one run\n * text a string, drawn by the safe renderer; or pass children\n */\nexport function Bubble({ from = \"assistant\", variant = \"bubble\", first = true, last = true, text, streaming, className, children }) {\n const user = from === \"user\";\n const plain = !user && variant === \"plain\";\n const caret = streaming ? (\n <span aria-hidden=\"true\" className=\"ml-0.5 inline-block h-[1em] w-1.5 translate-y-0.5 animate-pulse rounded-sm bg-current opacity-60\" />\n ) : null;\n const body = text !== undefined ? <ChatText text={text} tone={user ? \"primary\" : undefined} trailing={caret} /> : children;\n return (\n <div\n className={cx(\n \"min-w-0 max-w-full text-sm leading-relaxed\",\n plain\n ? \"py-0.5\"\n : cx(\n \"rounded-2xl px-3.5 py-2.5\",\n user ? \"bg-primary text-primary-foreground\" : \"bg-muted text-foreground\",\n user ? [!first && \"rounded-tr-md\", !last && \"rounded-br-md\"] : [!first && \"rounded-tl-md\", !last && \"rounded-bl-md\"],\n ),\n className,\n )}\n >\n {body}\n {text === undefined ? caret : null}\n </div>\n );\n}\n\nexport function formatSeconds(s) {\n const n = Math.max(0, Math.round(Number(s) || 0));\n return n < 60 ? `${n}s` : `${Math.floor(n / 60)}m ${n % 60}s`;\n}\n\n/** The folding reasoning over an assistant reply: \"Thinking…\" while it\n * streams with no answer yet, then \"Thought for 4s\". Closed by default. */\nfunction Reasoning({ text, seconds, thinking }) {\n const [open, setOpen] = useState(false);\n const title = thinking ? \"Thinking…\" : seconds ? `Thought for ${formatSeconds(seconds)}` : \"Reasoning\";\n return (\n <Collapsible\n open={open}\n onOpenChange={setOpen}\n chevron=\"right\"\n title={<span className={cx(\"font-normal text-muted-foreground\", thinking && \"animate-pulse motion-reduce:animate-none\")}>{title}</span>}\n className=\"w-full max-w-prose\"\n >\n <div className=\"border-l-2 border-border pl-3 text-xs leading-relaxed text-muted-foreground\">\n {text ? <ChatText text={text} /> : <span>Nothing to show yet.</span>}\n </div>\n </Collapsible>\n );\n}\n\n/** A sent message's words, back in a field: Enter (or Save) saves through\n * `onSave(text)`, Esc (or Cancel) leaves it as it was. A failed save keeps\n * the field open with the reason under it. */\nfunction EditBox({ initial, onSave, onCancel }) {\n const id = useId();\n const [text, setText] = useState(initial);\n const [saving, setSaving] = useState(false);\n const [error, setError] = useState(null);\n\n async function save() {\n const next = text.trim();\n if (!next || saving) return;\n if (next === initial.trim()) return onCancel();\n setSaving(true);\n setError(null);\n let r;\n try {\n r = await onSave(next);\n } catch (e) {\n r = { ok: false, error: e?.message ?? String(e) };\n }\n setSaving(false);\n if (r && r.ok === false) setError(r.error ?? \"The change was not saved\");\n }\n\n return (\n <div className=\"grid w-[32rem] max-w-full gap-2\">\n <label htmlFor={id} className=\"sr-only\">\n Edit message\n </label>\n <Textarea\n id={id}\n autoFocus\n autoGrow\n minRows={2}\n maxRows={10}\n value={text}\n invalid={Boolean(error)}\n aria-describedby={error ? `${id}-error` : undefined}\n onChange={(e) => setText(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Escape\") {\n e.preventDefault();\n onCancel();\n } else if (e.key === \"Enter\" && !e.shiftKey && !e.nativeEvent.isComposing) {\n e.preventDefault();\n save();\n }\n }}\n />\n {error ? (\n <p id={`${id}-error`} role=\"alert\" className=\"text-xs text-destructive\">\n {error}\n </p>\n ) : null}\n <div className=\"flex justify-end gap-2\">\n <Button size=\"sm\" variant=\"quiet\" onClick={onCancel} disabled={saving}>\n Cancel\n </Button>\n <Button size=\"sm\" variant=\"primary\" onClick={save} loading={saving} disabled={!text.trim()}>\n Save\n </Button>\n </div>\n </div>\n );\n}\n\nfunction Thinking({ label = \"Thinking\" }) {\n return (\n <span role=\"status\" className=\"inline-flex h-5 items-center gap-1\" aria-label={label}>\n {[0, 150, 300].map((d) => (\n <span key={d} aria-hidden=\"true\" className=\"size-1.5 animate-pulse rounded-full bg-muted-foreground\" style={{ animationDelay: `${d}ms` }} />\n ))}\n </span>\n );\n}\n\n/**\n * One message.\n * message { id, role, content | parts, createdAt?, status?, error?,\n * author?: { name, src }, attachments?: [{ id?, name, size?, type?, url? }] }\n * status: sending · streaming · failed · (anything else: sent)\n * first / last its place in a run from one author (MessageScroller\n * works these out); `first` shows the avatar, name and time\n * latest the newest reply: its actions stay in view, not only on hover\n * message.reasoning (string) · message.reasoningDuration (seconds) — the\n * folding reasoning over an assistant reply\n * message.feedback \"up\" · \"down\" — the thumbs already chosen\n * onFeedback(message, \"up\" | \"down\" | null) thumbs on assistant replies;\n * may return { ok: false, error }: the choice goes back\n * onEdit(message, text) an edit button on user messages; may return\n * { ok: false, error }: the field stays open with the reason\n * editing / onEditingChange(next) control the edit (ChatThread does,\n * for ArrowUp in an empty composer)\n */\nexport function Message({\n message,\n first = true,\n last = true,\n latest = false,\n variant = \"plain\",\n assistantName = \"Assistant\",\n assistantAvatar,\n userName = \"You\",\n userAvatar,\n showUserAvatar = false,\n onRetry,\n onRegenerate,\n onCopy,\n actionsOf,\n onFeedback,\n onEdit,\n editing: editingProp,\n onEditingChange,\n formatTime: timeOf = formatTime,\n className,\n}) {\n const [copied, setCopied] = useState(false);\n const copiedTimer = useRef(0);\n useEffect(() => () => clearTimeout(copiedTimer.current), []);\n const [ownEditing, setOwnEditing] = useState(false);\n const editing = editingProp ?? ownEditing;\n const setEditing = (next) => {\n if (editingProp === undefined) setOwnEditing(next);\n onEditingChange?.(next);\n };\n // The thumbs: shown at once, taken back if the save fails.\n const [choice, setChoice] = useState(undefined);\n const [feedbackError, setFeedbackError] = useState(null);\n const [feedbackBusy, setFeedbackBusy] = useState(false);\n const given = message?.feedback ?? null;\n useEffect(() => setChoice(undefined), [given]);\n\n if (!message) return null;\n const role = message.role === \"user\" ? \"user\" : message.role === \"system\" ? \"system\" : \"assistant\";\n const text = textOf(message);\n const status = message.status;\n const streaming = status === \"streaming\";\n const failed = status === \"failed\";\n const sending = status === \"sending\";\n const date = toDate(message.createdAt);\n\n if (role === \"system\") {\n return (\n <div className={cx(\"flex justify-center px-2 py-1\", className)}>\n <ChatText text={text} className=\"max-w-prose text-balance text-center text-xs text-muted-foreground\" />\n </div>\n );\n }\n\n const user = role === \"user\";\n const name = message.author?.name ?? (user ? userName : assistantName);\n const src = message.author?.src ?? (user ? userAvatar : assistantAvatar);\n const showAvatar = !user || showUserAvatar;\n const files = (message.attachments ?? []).filter(Boolean);\n\n async function copy() {\n try {\n if (onCopy) await onCopy(message, text);\n else await navigator.clipboard.writeText(text);\n setCopied(true);\n clearTimeout(copiedTimer.current);\n copiedTimer.current = setTimeout(() => setCopied(false), 1500);\n } catch {\n // No clipboard (an insecure origin): nothing to say beyond not ticking.\n }\n }\n\n const feedback = choice !== undefined ? choice : given;\n async function giveFeedback(v) {\n const before = feedback;\n const next = before === v ? null : v;\n setChoice(next);\n setFeedbackError(null);\n setFeedbackBusy(true);\n let r;\n try {\n r = await onFeedback(message, next);\n } catch (e) {\n r = { ok: false, error: e?.message ?? String(e) };\n }\n setFeedbackBusy(false);\n if (r && r.ok === false) {\n setChoice(before);\n setFeedbackError(r.error ?? \"Your feedback was not saved\");\n }\n }\n\n const reasoning = typeof message.reasoning === \"string\" ? message.reasoning : null;\n const thinking = streaming && !text;\n\n const extra = actionsOf?.(message) ?? [];\n const canRegenerate = !user && onRegenerate && !streaming;\n const hasActions = !sending && !streaming && !failed;\n\n // The actions sit under the words on touch, where they always show.\n const toolbar =\n hasActions && (text || extra.length) ? (\n <div\n className={cx(\n \"-mx-1 flex items-center gap-0.5 transition-opacity\",\n latest && !user\n ? \"opacity-100\"\n : \"pointer-events-none opacity-0 group-hover/message:pointer-events-auto group-hover/message:opacity-100 group-focus-within/message:pointer-events-auto group-focus-within/message:opacity-100 [@media(hover:none)]:pointer-events-auto [@media(hover:none)]:opacity-100\",\n // On a pointer that hovers, the actions float instead of holding\n // a row open under every message: beside a user's bubble, over\n // the top corner of an assistant's (the newest reply keeps its row).\n user\n ? \"flex-row-reverse [@media(hover:hover)]:absolute [@media(hover:hover)]:right-full [@media(hover:hover)]:top-0 [@media(hover:hover)]:mx-0 [@media(hover:hover)]:mr-1\"\n : !latest &&\n \"[@media(hover:hover)]:absolute [@media(hover:hover)]:-top-3 [@media(hover:hover)]:right-0 [@media(hover:hover)]:z-10 [@media(hover:hover)]:mx-0 [@media(hover:hover)]:rounded-md [@media(hover:hover)]:border [@media(hover:hover)]:border-border [@media(hover:hover)]:bg-card [@media(hover:hover)]:p-0.5 [@media(hover:hover)]:shadow-sm\",\n )}\n >\n {text ? (\n <Tooltip label={copied ? \"Copied\" : \"Copy\"}>\n <IconButton icon={copied ? \"check\" : \"copy\"} label={copied ? \"Copied\" : \"Copy message\"} size=\"sm\" onClick={copy} />\n </Tooltip>\n ) : null}\n {canRegenerate ? (\n <Tooltip label=\"Regenerate\">\n <IconButton icon=\"refresh\" label=\"Regenerate reply\" size=\"sm\" onClick={() => onRegenerate(message)} />\n </Tooltip>\n ) : null}\n {!user && onFeedback && text ? (\n <>\n <Toggle\n variant=\"quiet\"\n size=\"sm\"\n icon=\"thumbs-up\"\n label=\"Good reply\"\n pressed={feedback === \"up\"}\n disabled={feedbackBusy}\n onPressedChange={() => giveFeedback(\"up\")}\n />\n <Toggle\n variant=\"quiet\"\n size=\"sm\"\n icon=\"thumbs-down\"\n label=\"Bad reply\"\n pressed={feedback === \"down\"}\n disabled={feedbackBusy}\n onPressedChange={() => giveFeedback(\"down\")}\n />\n </>\n ) : null}\n {user && onEdit && text ? (\n <Tooltip label=\"Edit\">\n <IconButton icon=\"pencil\" label=\"Edit message\" size=\"sm\" onClick={() => setEditing(true)} />\n </Tooltip>\n ) : null}\n <Menu items={extra} label=\"More actions for this message\" align={user ? \"end\" : \"start\"} />\n </div>\n ) : null;\n\n return (\n <article\n aria-label={`Message from ${name}`}\n data-role={role}\n data-status={status || undefined}\n className={cx(\"group/message relative flex gap-2.5\", user && \"flex-row-reverse\", first ? \"mt-4\" : \"mt-1\", className)}\n >\n {showAvatar ? (\n <div className=\"w-8 shrink-0\">\n {first ? <Avatar name={name} src={src} size=\"sm\" shape={user ? \"round\" : \"square\"} /> : null}\n </div>\n ) : null}\n <div\n className={cx(\n \"flex min-w-0 flex-col gap-1\",\n user ? \"max-w-[85%] items-end\" : variant === \"plain\" ? \"flex-1 items-start\" : \"max-w-[85%] items-start\",\n )}\n >\n {first ? (\n <p className={cx(\"flex items-baseline gap-2 text-xs\", user && \"flex-row-reverse\")}>\n <span className=\"font-medium text-foreground\">{name}</span>\n {date ? (\n <time dateTime={date.toISOString()} suppressHydrationWarning className=\"text-muted-foreground\">\n {timeOf(date)}\n </time>\n ) : null}\n </p>\n ) : null}\n\n {!user && (reasoning || (thinking && reasoning !== null)) ? (\n <Reasoning text={reasoning} seconds={message.reasoningDuration} thinking={thinking} />\n ) : null}\n\n {editing && user && onEdit ? (\n <EditBox\n initial={text}\n onCancel={() => setEditing(false)}\n onSave={async (next) => {\n const r = await onEdit(message, next);\n if (!(r && r.ok === false)) setEditing(false);\n return r;\n }}\n />\n ) : text || (streaming && reasoning === null) ? (\n <div className={cx(\"flex max-w-full flex-col gap-1\", user ? \"relative items-end\" : \"items-start\")}>\n <Bubble\n from={role}\n variant={variant}\n first={first}\n last={last}\n text={text || undefined}\n streaming={streaming && Boolean(text)}\n className={cx(failed && \"opacity-70\", sending && \"opacity-80\")}\n >\n {text ? null : <Thinking />}\n </Bubble>\n {toolbar}\n </div>\n ) : null}\n\n {feedbackError ? (\n <p role=\"alert\" className=\"text-xs text-destructive\">\n {feedbackError}\n </p>\n ) : null}\n\n {files.length ? (\n <ul className={cx(\"flex max-w-full flex-wrap gap-2\", user && \"justify-end\")}>\n {files.map((f, i) => (\n <li key={f.id ?? `${f.name}-${i}`} className=\"min-w-0 max-w-full\">\n <Attachment name={f.name} size={f.size} type={f.type} url={f.url} href={f.href} error={f.error} />\n </li>\n ))}\n </ul>\n ) : null}\n\n {sending ? (\n <p className=\"flex items-center gap-1.5 text-xs text-muted-foreground\">\n <Spinner size=\"sm\" label={null} className=\"size-3\" />\n Sending…\n </p>\n ) : null}\n\n {failed ? (\n <div role=\"alert\" className=\"flex flex-wrap items-center gap-x-2 text-xs text-destructive\">\n <Icon name=\"warning\" size={14} />\n <span>{message.error ? `Not sent: ${message.error}` : \"Not sent\"}</span>\n {onRetry ? (\n <Button variant=\"link\" size=\"sm\" className=\"min-h-9 text-xs\" onClick={() => onRetry(message)}>\n Retry\n </Button>\n ) : null}\n </div>\n ) : null}\n\n </div>\n </article>\n );\n}\n"
35
+ },
36
+ {
37
+ "path": "registry/blocks/chat/text.jsx",
38
+ "type": "registry:component",
39
+ "target": "@components/makersclaw/blocks/chat/text.jsx",
40
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { IconButton, cx } from \"../../ui\";\n\n// ChatText — a message's words, drawn safely. No HTML is ever parsed and\n// no markdown library is loaded: the text is split by hand and every piece\n// is a React text node, so \"<script>\" in a message is shown, not run.\n//\n// What it understands, and nothing more:\n// ```lang … ``` a code block (an unclosed fence runs to the end, so a\n// block that is still streaming already looks like one)\n// `code` a code span\n// **bold** strong text\n// line breaks kept as they were typed\n// Links, headings, lists and tables stay plain text.\n\nconst FENCE = /```([^\\n`]*)\\n?([\\s\\S]*?)(?:```|$)/g;\nconst INLINE = /(`[^`\\n]+`|\\*\\*[^*\\n]+\\*\\*)/g;\n\nfunction blocks(text) {\n const out = [];\n let last = 0;\n for (const m of text.matchAll(FENCE)) {\n // A block stands on its own line: the newline on either side of it is\n // the block's, not the text's.\n if (m.index > last) out.push({ type: \"text\", body: text.slice(last, m.index).replace(/\\n$/, \"\") });\n out.push({ type: \"code\", lang: m[1].trim(), body: m[2].replace(/\\n$/, \"\") });\n last = m.index + m[0].length;\n if (text[last] === \"\\n\") last += 1;\n }\n if (last < text.length) out.push({ type: \"text\", body: text.slice(last) });\n return out;\n}\n\nfunction Inline({ text, tone }) {\n return text.split(INLINE).map((piece, i) => {\n if (piece.length > 2 && piece.startsWith(\"`\") && piece.endsWith(\"`\")) {\n return (\n <code\n key={i}\n className={cx(\"rounded-sm px-1 py-0.5 font-mono text-[0.9em]\", tone === \"primary\" ? \"bg-primary-foreground/15\" : \"bg-muted\")}\n >\n {piece.slice(1, -1)}\n </code>\n );\n }\n if (piece.length > 4 && piece.startsWith(\"**\") && piece.endsWith(\"**\")) {\n return (\n <strong key={i} className=\"font-semibold\">\n {piece.slice(2, -2)}\n </strong>\n );\n }\n return piece;\n });\n}\n\nfunction CodeBlock({ lang, body }) {\n const [copied, setCopied] = useState(false);\n async function copy() {\n try {\n await navigator.clipboard.writeText(body);\n setCopied(true);\n setTimeout(() => setCopied(false), 1500);\n } catch {\n // No clipboard (an insecure origin): the code is still selectable.\n }\n }\n return (\n <div className=\"my-2 min-w-0 overflow-hidden rounded-md border border-border bg-muted text-foreground first:mt-0 last:mb-0\">\n <div className=\"flex min-h-9 items-center justify-between gap-2 border-b border-border pl-3\">\n <span className=\"truncate font-mono text-xs text-muted-foreground\">{lang || \"code\"}</span>\n <IconButton icon={copied ? \"check\" : \"copy\"} label={copied ? \"Copied\" : \"Copy code\"} size=\"sm\" onClick={copy} />\n </div>\n <pre className=\"overflow-x-auto p-3 font-mono text-xs leading-relaxed\">\n <code>{body}</code>\n </pre>\n </div>\n );\n}\n\n/** The words of one message. `tone` is \"primary\" inside a user bubble;\n * `trailing` sits inline after the last word (a streaming caret). */\nexport function ChatText({ text, tone, trailing, className }) {\n const value = String(text ?? \"\");\n const parts = blocks(value);\n const end = parts.length - 1;\n return (\n <div className={cx(\"min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere]\", className)}>\n {parts.map((b, i) =>\n b.type === \"code\" ? (\n <CodeBlock key={i} lang={b.lang} body={b.body} />\n ) : (\n <span key={i}>\n <Inline text={b.body} tone={tone} />\n {i === end ? trailing : null}\n </span>\n ),\n )}\n {parts[end]?.type === \"text\" ? null : trailing}\n </div>\n );\n}\n"
41
+ },
42
+ {
43
+ "path": "registry/blocks/chat/attachment.jsx",
44
+ "type": "registry:component",
45
+ "target": "@components/makersclaw/blocks/chat/attachment.jsx",
46
+ "content": "\"use client\";\n\nimport { Icon, IconButton, Image, Progress, cx } from \"../../ui\";\n\n// Attachment — one file on a message or waiting in the composer: an icon\n// for its kind (or the picture itself when it is an image with a `url`),\n// its name and size, a bar while it uploads, the reason when it failed,\n// and a remove button when `onRemove` is given.\n\nconst KINDS = [\n [/^image\\//, \"image\", \"Image\"],\n [/^(audio|video)\\//, \"play\", \"Media\"],\n [/(zip|tar|gzip|compressed|x-7z|rar)/, \"archive\", \"Archive\"],\n [/(spreadsheet|excel|csv)/, \"grid\", \"Spreadsheet\"],\n [/(pdf|word|document|text|rtf|presentation|powerpoint)/, \"file\", \"Document\"],\n];\nconst BY_EXT = {\n png: \"image/png\", jpg: \"image/jpeg\", jpeg: \"image/jpeg\", gif: \"image/gif\", webp: \"image/webp\", svg: \"image/svg+xml\",\n pdf: \"application/pdf\", doc: \"application/msword\", docx: \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n txt: \"text/plain\", md: \"text/markdown\", csv: \"text/csv\", xls: \"application/vnd.ms-excel\", xlsx: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n zip: \"application/zip\", mp3: \"audio/mpeg\", mp4: \"video/mp4\", mov: \"video/quicktime\",\n};\n\nexport function kindOf(type, name) {\n const mime = type || BY_EXT[String(name ?? \"\").split(\".\").pop()?.toLowerCase()] || \"\";\n for (const [re, icon, label] of KINDS) if (re.test(mime)) return { icon, label, image: icon === \"image\" };\n return { icon: \"file\", label: \"File\", image: false };\n}\n\n/** 2048 → \"2 KB\", 5_300_000 → \"5.1 MB\". */\nexport function formatBytes(bytes) {\n const n = Number(bytes);\n if (!Number.isFinite(n) || n < 0) return \"\";\n if (n < 1024) return `${n} B`;\n const units = [\"KB\", \"MB\", \"GB\", \"TB\"];\n let v = n / 1024;\n let u = 0;\n while (v >= 1024 && u < units.length - 1) {\n v /= 1024;\n u += 1;\n }\n return `${v >= 10 ? Math.round(v) : Math.round(v * 10) / 10} ${units[u]}`;\n}\n\nexport function Attachment({ name, size, type, url, href, progress, error, onRemove, removeLabel, className }) {\n const kind = kindOf(type, name);\n const uploading = typeof progress === \"number\" && progress < 100 && !error;\n const thumb = kind.image && url;\n const open = href ?? (!uploading && !error ? url : undefined);\n const facts = error ? error : uploading ? `Uploading… ${Math.round(progress)}%` : [kind.label, formatBytes(size)].filter(Boolean).join(\" · \");\n\n const title = open ? (\n // raw: an attachment opens a file URL (often a blob: or a storage\n // link) in a new tab; Button's href is next/link, which is for pages.\n <a\n href={open}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"block truncate rounded-sm text-sm font-medium outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring\"\n >\n {name}\n </a>\n ) : (\n <p className=\"truncate text-sm font-medium\">{name}</p>\n );\n\n return (\n <div\n className={cx(\n \"flex w-64 max-w-full min-w-0 items-center gap-2.5 rounded-lg border bg-card p-2 text-card-foreground\",\n error ? \"border-destructive/50\" : \"border-border\",\n className,\n )}\n >\n {thumb ? (\n <Image src={url} alt=\"\" ratio=\"square\" className=\"size-10 shrink-0\" />\n ) : (\n <span className=\"flex size-10 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground\">\n <Icon name={error ? \"warning\" : kind.icon} size={18} className={error ? \"text-destructive\" : undefined} />\n </span>\n )}\n <div className=\"grid min-w-0 flex-1 gap-1\">\n {title}\n {uploading ? (\n <Progress value={progress} aria-label={`Uploading ${name}`} valueText={`${Math.round(progress)}%`} />\n ) : (\n <p className={cx(\"truncate text-xs\", error ? \"text-destructive\" : \"text-muted-foreground\")}>{facts}</p>\n )}\n </div>\n {onRemove ? (\n <IconButton icon=\"x\" size=\"sm\" label={removeLabel ?? `Remove ${name}`} onClick={onRemove} className=\"-my-1\" />\n ) : null}\n </div>\n );\n}\n"
47
+ },
48
+ {
49
+ "path": "registry/blocks/chat/scroller.jsx",
50
+ "type": "registry:component",
51
+ "target": "@components/makersclaw/blocks/chat/scroller.jsx",
52
+ "content": "\"use client\";\n\nimport { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport { Button, ScrollArea, Separator, Spinner, cx } from \"../../ui\";\nimport { Message, textOf, toDate } from \"./message\";\n\n// MessageScroller — the conversation's scrolling list. It keeps to the\n// bottom while messages arrive and stream, unless the reader has scrolled\n// up: then it stays where they are and offers \"Jump to latest\". It puts a\n// day between messages from different days, works out which messages run\n// together from one author, loads older messages when the reader reaches\n// the top (keeping their place), and says each finished assistant reply\n// once to a screen reader.\n\nconst NEAR_BOTTOM_PX = 64;\nconst NEAR_TOP_PX = 120;\nconst RUN_GAP_MS = 5 * 60 * 1000;\n\n// useLayoutEffect warns on the server; the scroll work only matters in a browser.\nconst useIsoLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\n// Days are the reader's own, but the server does not know their time\n// zone: the first render (server and hydration alike) groups by UTC, and\n// the browser regroups by local days once it is running. Same markup on\n// both sides, so no hydration mismatch.\nfunction dayKey(d, local) {\n return local ? `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}` : d.toISOString().slice(0, 10);\n}\n\nexport function formatDay(date, now = new Date()) {\n const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n const that = new Date(date.getFullYear(), date.getMonth(), date.getDate());\n const days = Math.round((today - that) / 86400000);\n if (days === 0) return \"Today\";\n if (days === 1) return \"Yesterday\";\n return date.toLocaleDateString(undefined, {\n weekday: \"short\",\n day: \"numeric\",\n month: \"short\",\n year: date.getFullYear() === now.getFullYear() ? undefined : \"numeric\",\n });\n}\n\nfunction authorKey(m) {\n return `${m.role}|${m.author?.name ?? \"\"}`;\n}\n\n/** Messages with their day separators and run positions worked out. */\nfunction layout(messages, idOf, local) {\n const out = [];\n let prev = null;\n let prevDay = null;\n messages.forEach((m, i) => {\n const date = toDate(m.createdAt);\n const day = date ? dayKey(date, local) : prevDay;\n if (date && day !== prevDay) out.push({ type: \"day\", key: `day-${day}`, date });\n const prevDate = prev ? toDate(prev.createdAt) : null;\n const joins =\n prev &&\n m.role !== \"system\" &&\n authorKey(prev) === authorKey(m) &&\n day === prevDay &&\n !(date && prevDate && date - prevDate > RUN_GAP_MS);\n if (joins) out[out.length - 1].last = false;\n out.push({ type: \"message\", key: idOf(m, i), message: m, first: !joins, last: true });\n prev = m;\n prevDay = day;\n });\n // The newest reply keeps its actions in view: it is the one people copy\n // or regenerate.\n for (let i = out.length - 1; i >= 0; i--) {\n if (out[i].type === \"message\" && out[i].message.role === \"assistant\") {\n out[i].latest = true;\n break;\n }\n }\n return out;\n}\n\nexport function MessageScroller({\n messages,\n renderMessage,\n idOf = (m, i) => String(m.id ?? i),\n onLoadMore,\n hasMore = false,\n loadingMore = false,\n loadMoreLabel = \"Load earlier messages\",\n jumpLabel = \"Jump to latest\",\n emptyState = null,\n formatDay: dayOf = formatDay,\n announce = (m) => `${m.author?.name ?? \"Assistant\"} replied: ${textOf(m).slice(0, 280)}`,\n label = \"Conversation\",\n className,\n children,\n}) {\n const scroller = useRef(null);\n const atBottom = useRef(true);\n const anchor = useRef(null);\n const settled = useRef(new Map());\n const primed = useRef(false);\n const [away, setAway] = useState(false);\n const [unseen, setUnseen] = useState(false);\n const [said, setSaid] = useState(\"\");\n const [local, setLocal] = useState(false);\n useEffect(() => setLocal(true), []);\n\n const list = messages ?? [];\n const rows = layout(list, idOf, local);\n const lastMsg = list[list.length - 1];\n const firstId = list.length ? idOf(list[0], 0) : null;\n // What changes when the conversation grows at the bottom: the last\n // message, its status, and how much of it has streamed in.\n const tail = lastMsg ? `${idOf(lastMsg, list.length - 1)}|${lastMsg.status ?? \"\"}|${textOf(lastMsg).length}` : \"\";\n\n function toBottom(smooth) {\n const el = scroller.current;\n if (!el) return;\n el.scrollTo({ top: el.scrollHeight, behavior: smooth ? \"smooth\" : \"auto\" });\n }\n\n function onScroll() {\n const el = scroller.current;\n if (!el) return;\n const bottom = el.scrollHeight - el.scrollTop - el.clientHeight <= NEAR_BOTTOM_PX;\n atBottom.current = bottom;\n setAway(!bottom);\n if (bottom) setUnseen(false);\n if (el.scrollTop <= NEAR_TOP_PX && hasMore && !loadingMore && onLoadMore) loadMore();\n }\n\n function loadMore() {\n const el = scroller.current;\n if (el) anchor.current = { id: firstId, height: el.scrollHeight, top: el.scrollTop };\n onLoadMore?.();\n }\n\n // Older messages landed above: keep the reader on the message they were reading.\n useIsoLayoutEffect(() => {\n const el = scroller.current;\n const a = anchor.current;\n if (!el || !a || a.id === firstId) return;\n el.scrollTop = a.top + (el.scrollHeight - a.height);\n anchor.current = null;\n }, [firstId]);\n\n // Newer messages: follow them if the reader was at the bottom, or if the\n // newest is their own; otherwise flag that there is something below.\n useIsoLayoutEffect(() => {\n if (!lastMsg) {\n atBottom.current = true;\n setAway(false);\n setUnseen(false);\n return;\n }\n if (!primed.current) {\n primed.current = true;\n toBottom(false);\n return;\n }\n if (atBottom.current || lastMsg.role === \"user\") {\n toBottom(false);\n atBottom.current = true;\n setAway(false);\n setUnseen(false);\n } else {\n setUnseen(true);\n }\n }, [tail]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Say a reply once, when it is whole — never token by token.\n useEffect(() => {\n const seen = settled.current;\n const first = seen.size === 0;\n list.forEach((m, i) => {\n const id = idOf(m, i);\n const done = m.status !== \"streaming\" && m.status !== \"sending\";\n const was = seen.get(id);\n seen.set(id, done);\n if (!first && m.role === \"assistant\" && done && was !== true && textOf(m)) setSaid(announce(m));\n });\n }, [tail]); // eslint-disable-line react-hooks/exhaustive-deps\n\n const draw = renderMessage ?? ((m, ctx) => <Message message={m} first={ctx.first} last={ctx.last} latest={ctx.latest} />);\n\n return (\n <div className={cx(\"relative flex min-h-0 flex-col\", className)}>\n <ScrollArea\n ref={scroller}\n onScroll={onScroll}\n label={label}\n aria-busy={loadingMore || undefined}\n className=\"flex-1\"\n >\n <div className=\"flex min-h-full flex-col justify-end px-3 py-4 sm:px-4\">\n {hasMore || loadingMore ? (\n <div className=\"flex justify-center pb-3\">\n {loadingMore ? (\n <span className=\"flex min-h-9 items-center gap-2 text-xs text-muted-foreground\">\n <Spinner size=\"sm\" label={null} />\n Loading earlier messages…\n </span>\n ) : (\n <Button variant=\"quiet\" size=\"sm\" onClick={loadMore}>\n {loadMoreLabel}\n </Button>\n )}\n </div>\n ) : null}\n {list.length === 0 && !loadingMore ? emptyState : null}\n <ol className=\"flex flex-col [&>li:first-child>*]:mt-0\">\n {rows.map((row) =>\n row.type === \"day\" ? (\n <li key={row.key} className=\"mt-6 first:mt-0\">\n <Separator label={<span suppressHydrationWarning>{dayOf(row.date)}</span>} />\n </li>\n ) : (\n <li key={row.key} className=\"flex flex-col\">\n {draw(row.message, { first: row.first, last: row.last, latest: Boolean(row.latest) })}\n </li>\n ),\n )}\n </ol>\n {children}\n </div>\n </ScrollArea>\n {away ? (\n <div className=\"pointer-events-none absolute inset-x-0 bottom-3 flex justify-center\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n icon=\"chevron-down\"\n className=\"pointer-events-auto rounded-full shadow-md\"\n onClick={() => {\n toBottom(true);\n setUnseen(false);\n }}\n >\n {unseen ? \"New messages\" : jumpLabel}\n </Button>\n </div>\n ) : null}\n <p aria-live=\"polite\" className=\"sr-only\">\n {said}\n </p>\n </div>\n );\n}\n"
53
+ },
54
+ {
55
+ "path": "registry/blocks/chat/composer.jsx",
56
+ "type": "registry:component",
57
+ "target": "@components/makersclaw/blocks/chat/composer.jsx",
58
+ "content": "\"use client\";\n\nimport { useEffect, useId, useLayoutEffect, useRef, useState } from \"react\";\nimport { Button, FileInput, Icon, IconButton, InputGroup, Kbd, Menu, cx } from \"../../ui\";\nimport { Attachment } from \"./attachment\";\n\n// Composer — where a message is written. One row that grows with the text\n// (to `maxRows`, then it scrolls), Enter sends and Shift+Enter starts a\n// new line (never while an input method is composing a word), files come\n// in through the paperclip, a paste or a drop and wait in a row above the\n// text, and while a reply streams the send button is Stop. The field is\n// the kit's multi-line InputGroup: the files above the text, the\n// paperclip and the hint in its bar, send at the end.\n//\n// shape=\"pill\" is the \"ask anything\" box: one round row with the \"+\" menu\n// and the paperclip before the text and the tools and Send after it,\n// which opens into a rounded box (text on top, the bar under it) once the\n// text runs past one line. `suggestions` are prompt chips (or cards) that\n// fill the field, or send at once.\n\n// useLayoutEffect warns on the server; the sizing only matters in a browser.\nconst useIsoLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\nconst LINE_PX = 24; // the pill's text is text-base leading-6\n\n/** Prompt chips or starter cards. Each is a kit Button. */\nfunction Suggestions({ items, look, onPick, disabled, label }) {\n const list = (items ?? []).filter(Boolean);\n if (!list.length) return null;\n const cards = look === \"cards\";\n return (\n <ul aria-label={label} className={cards ? \"grid grid-cols-1 gap-2 sm:grid-cols-2\" : \"flex flex-wrap justify-center gap-2\"}>\n {list.map((s, i) => (\n <li key={s.id ?? `${s.label}-${i}`} className={cards ? \"min-w-0\" : \"min-w-0 max-w-full\"}>\n <Button\n variant=\"secondary\"\n size={cards ? \"md\" : \"sm\"}\n align={cards ? \"start\" : \"center\"}\n icon={s.icon}\n disabled={disabled || s.disabled}\n onClick={() => onPick(s)}\n className={cards ? \"h-auto w-full py-2\" : \"max-w-full rounded-full font-normal\"}\n >\n {cards ? (\n <span className=\"grid min-w-0 text-left\">\n <span className=\"truncate\">{s.label}</span>\n {s.description ? <span className=\"truncate text-xs font-normal text-muted-foreground\">{s.description}</span> : null}\n </span>\n ) : (\n <span className=\"min-w-0 truncate\">{s.label}</span>\n )}\n </Button>\n </li>\n ))}\n </ul>\n );\n}\n\nexport function Composer({\n onSend,\n onStop,\n streaming = false,\n disabled = false,\n value: controlled,\n onValueChange,\n placeholder = \"Write a message…\",\n disabledPlaceholder,\n attachments,\n onAttach,\n onRemoveAttachment,\n accept,\n maxRows = 8,\n canSendEmpty = false,\n label = \"Message\",\n hint = true,\n autoFocus = false,\n shape = \"box\",\n tools: extraTools,\n menu,\n menuLabel = \"Add to the message\",\n suggestions,\n suggestionsLook = \"chips\",\n suggestionsPlacement = \"below\",\n suggestionMode = \"fill\",\n suggestionsLabel = \"Suggested prompts\",\n onEditLast,\n className,\n}) {\n const id = useId();\n const area = useRef(null);\n const [own, setOwn] = useState(\"\");\n const [error, setError] = useState(null);\n const [dragging, setDragging] = useState(false);\n const [wide, setWide] = useState(false);\n const value = controlled ?? own;\n const setValue = (v) => {\n if (controlled === undefined) setOwn(v);\n onValueChange?.(v);\n };\n\n const pill = shape === \"pill\";\n const files = (attachments ?? []).filter(Boolean);\n const uploading = files.some((f) => typeof f.progress === \"number\" && f.progress < 100 && !f.error);\n const ready = files.filter((f) => !f.error);\n const hasContent = value.trim().length > 0 || ready.length > 0 || canSendEmpty;\n const canSend = !disabled && !streaming && !uploading && hasContent && Boolean(onSend);\n // The pill opens into a box past one line, on a new line, or with files.\n const open = pill && (wide || value.includes(\"\\n\") || files.length > 0);\n\n // The pill's own textarea grows with its text (the box's is the kit's).\n useIsoLayoutEffect(() => {\n if (!pill) return;\n const el = area.current;\n if (!el) return;\n el.style.height = \"auto\";\n const h = el.scrollHeight;\n const max = maxRows * LINE_PX;\n el.style.height = `${Math.min(h, max)}px`;\n el.style.overflowY = h > max ? \"auto\" : \"hidden\";\n if (value === \"\") setWide(false);\n else if (!open && h > LINE_PX * 1.5) setWide(true);\n }, [pill, value, open, maxRows]);\n\n async function send(override) {\n const text = override ?? value;\n const content = text.trim().length > 0 || ready.length > 0 || canSendEmpty;\n if (disabled || streaming || uploading || !onSend || !content) return;\n setError(null);\n setValue(\"\");\n let result;\n try {\n result = await onSend(text.trim(), ready);\n } catch (e) {\n result = { ok: false, error: e?.message ?? String(e) };\n }\n if (result && result.ok === false) {\n // Nothing typed is lost: the words come back to the field.\n setValue(text);\n setError(result.error ?? \"The message was not sent\");\n }\n area.current?.focus();\n }\n\n function pick(s) {\n if (s.onSelect) return s.onSelect(s);\n const text = s.prompt ?? s.label ?? \"\";\n if (suggestionMode === \"send\") return send(text);\n setValue(text);\n setError(null);\n requestAnimationFrame(() => {\n const el = area.current;\n if (!el) return;\n el.focus();\n el.setSelectionRange?.(text.length, text.length);\n });\n }\n\n function take(list) {\n const picked = Array.from(list ?? []).filter(Boolean);\n if (picked.length && onAttach && !disabled) onAttach(picked);\n }\n\n function onKeyDown(e) {\n if (e.key === \"Enter\" && !e.shiftKey && !e.nativeEvent.isComposing) {\n e.preventDefault();\n send();\n } else if (e.key === \"Escape\" && streaming && onStop) {\n e.preventDefault();\n onStop();\n } else if (e.key === \"ArrowUp\" && onEditLast && value === \"\" && !e.nativeEvent.isComposing) {\n // Up in an empty field edits the last thing you sent.\n e.preventDefault();\n onEditLast();\n }\n }\n\n function onPaste(e) {\n if (!onAttach || !e.clipboardData?.files?.length) return;\n e.preventDefault();\n take(e.clipboardData.files);\n }\n\n const placeholderText = disabled ? disabledPlaceholder ?? placeholder : placeholder;\n\n const above = files.length ? (\n <ul aria-label=\"Attached files\" className={cx(\"flex gap-2 overflow-x-auto\", pill ? \"px-1 pb-1\" : \"p-2 pb-0\")}>\n {files.map((f, i) => (\n <li key={f.id ?? `${f.name}-${i}`} className=\"shrink-0\">\n <Attachment\n name={f.name}\n size={f.size}\n type={f.type}\n url={f.url}\n progress={f.progress}\n error={f.error}\n onRemove={onRemoveAttachment ? () => onRemoveAttachment(f) : undefined}\n className=\"w-56\"\n />\n </li>\n ))}\n </ul>\n ) : null;\n\n const plus =\n menu && menu.length ? (\n <Menu\n items={menu}\n label={menuLabel}\n trigger={<Icon name=\"plus\" size={18} />}\n align=\"start\"\n side=\"top\"\n size=\"md\"\n className=\"rounded-full text-muted-foreground\"\n />\n ) : null;\n\n const clip = onAttach ? <FileInput icon=\"paperclip\" label=\"Attach files\" multiple accept={accept} disabled={disabled} onFiles={take} /> : null;\n\n const tools = (\n <>\n {plus}\n {clip}\n {extraTools}\n {hint && !disabled ? (\n <p aria-hidden=\"true\" className=\"hidden min-w-0 items-center gap-1 truncate text-xs text-muted-foreground md:flex\">\n {streaming ? (\n <>\n <Kbd>Esc</Kbd> to stop\n </>\n ) : (\n <>\n <Kbd>Enter</Kbd> to send · <Kbd>Shift</Kbd>+<Kbd>Enter</Kbd> for a new line\n </>\n )}\n </p>\n ) : null}\n </>\n );\n\n const action =\n streaming && onStop ? (\n <IconButton icon=\"stop\" label=\"Stop the reply\" variant=\"secondary\" onClick={onStop} className=\"rounded-full\" />\n ) : (\n <IconButton\n icon=\"send\"\n label={uploading ? \"Waiting for files to upload\" : \"Send\"}\n variant=\"primary\"\n disabled={!canSend}\n onClick={() => send()}\n className=\"rounded-full\"\n />\n );\n\n const chips =\n suggestions && suggestions.length ? (\n <Suggestions items={suggestions} look={suggestionsLook} onPick={pick} disabled={disabled || streaming} label={suggestionsLabel} />\n ) : null;\n\n const field = pill ? (\n // A press anywhere in the pill that is not a control puts the caret in the text.\n <div\n onMouseDown={(e) => {\n if (e.target.closest(\"button, a, input, select, textarea, [role=button]\")) return;\n e.preventDefault();\n area.current?.focus();\n }}\n className={cx(\n \"grid w-full cursor-text border border-input bg-background text-foreground shadow-sm\",\n \"has-[textarea:focus-visible]:ring-2 has-[textarea:focus-visible]:ring-ring\",\n \"has-[textarea[aria-invalid=true]]:border-destructive\",\n \"has-[textarea:disabled]:cursor-not-allowed has-[textarea:disabled]:opacity-50\",\n open ? \"grid-cols-[minmax(0,1fr)_auto] gap-y-1 rounded-3xl p-2\" : \"grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-1 rounded-full p-1.5\",\n )}\n >\n {above ? <div className=\"col-span-2 row-start-1 min-w-0 cursor-default\">{above}</div> : null}\n <div className={cx(\"flex min-w-0 items-center gap-1\", open && cx(\"col-start-1\", above ? \"row-start-3\" : \"row-start-2\"))}>\n {plus}\n {clip}\n </div>\n <div className={cx(\"min-w-0\", open ? cx(\"col-span-2 px-2 pt-1\", above ? \"row-start-2\" : \"row-start-1\") : \"px-1.5\")}>\n {/* InputGroup's multi-line field always sets its bar under the text;\n the pill needs the addons on the text's own row, moving under it\n as the text grows. raw: kit gap, an inline InputGroup layout. */}\n <textarea\n ref={area}\n id={id}\n rows={1}\n value={value}\n disabled={disabled}\n aria-invalid={error ? true : undefined}\n aria-describedby={error ? `${id}-error` : undefined}\n autoFocus={autoFocus}\n placeholder={placeholderText}\n onChange={(e) => setValue(e.target.value)}\n onPaste={onPaste}\n onKeyDown={onKeyDown}\n className=\"block w-full resize-none border-0 bg-transparent p-0 text-base leading-6 text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed\"\n />\n </div>\n <div className={cx(\"flex items-center gap-1 justify-self-end\", open && cx(\"col-start-2\", above ? \"row-start-3\" : \"row-start-2\"))}>\n {extraTools}\n {action}\n </div>\n </div>\n ) : (\n <InputGroup\n multiline\n ref={area}\n id={id}\n minRows={1}\n maxRows={maxRows}\n value={value}\n disabled={disabled}\n invalid={Boolean(error)}\n autoFocus={autoFocus}\n placeholder={placeholderText}\n aria-describedby={error ? `${id}-error` : undefined}\n above={above}\n tools={tools}\n action={action}\n onChange={(e) => setValue(e.target.value)}\n onPaste={onPaste}\n onKeyDown={onKeyDown}\n className=\"shadow-sm\"\n />\n );\n\n return (\n <div className={cx(\"grid gap-1.5\", chips && \"gap-3\", className)}>\n {chips && suggestionsPlacement === \"above\" ? chips : null}\n <div className=\"grid gap-1.5\">\n <label htmlFor={id} className=\"sr-only\">\n {label}\n </label>\n {/* The drop target is the whole field, files and bar included. */}\n <div\n onDragOver={(e) => {\n if (!onAttach || disabled || !e.dataTransfer?.types?.includes(\"Files\")) return;\n e.preventDefault();\n setDragging(true);\n }}\n onDragLeave={(e) => {\n if (!e.currentTarget.contains(e.relatedTarget)) setDragging(false);\n }}\n onDrop={(e) => {\n if (!onAttach || disabled) return;\n e.preventDefault();\n setDragging(false);\n take(e.dataTransfer?.files);\n }}\n className={cx(pill ? (open ? \"rounded-3xl\" : \"rounded-full\") : \"rounded-md\", dragging && \"ring-2 ring-ring\")}\n >\n {field}\n </div>\n {error ? (\n <p id={`${id}-error`} role=\"alert\" className=\"px-1 text-xs text-destructive\">\n {error}\n </p>\n ) : null}\n </div>\n {chips && suggestionsPlacement !== \"above\" ? chips : null}\n </div>\n );\n}\n"
59
+ },
60
+ {
61
+ "path": "registry/blocks/chat/README.md",
62
+ "type": "registry:file",
63
+ "target": "@components/makersclaw/blocks/chat/README.md",
64
+ "content": "# Chat\n\nThe parts of a chat, and a thread made of them. `Message` is one turn:\navatar, name, time, the words in a `Bubble`, its files, its state\n(sending, streaming, failed with Retry) and the actions that appear on\nhover or focus (copy, regenerate, a menu of yours). `MessageScroller` is\nthe list: it keeps to the bottom while a reply streams unless the reader\nscrolled up, then offers \"Jump to latest\"; it puts a day between days,\nruns consecutive messages from one author together, loads older messages\nfrom the top and says each finished reply to a screen reader.\n`Composer` is the input: it grows with the text, Enter sends,\nShift+Enter is a new line, files come by paperclip, paste or drop, and\nSend becomes Stop while a reply streams; it comes as a box or as the\nround \"ask anything\" pill, with a \"+\" menu, your tools in its bar and\nsuggestion chips. An assistant reply can fold its reasoning away\n(\"Thought for 4s\") and take a thumbs up or down; a user message can be\nedited in place. `ChatHeader` is the strip over a thread (name, status,\nactions). `Attachment` is one file. `ChatThread` puts them together from\n`messages` and `onSend`.\n\n```jsx\nimport { ChatThread } from \"../components/blocks/chat\"; // a Makersclaw app (\"./blocks/chat\" from components/)\nimport { ChatThread } from \"@/components/makersclaw/blocks/chat\"; // a shadcn project\n```\n\nEvery part is a client component: render them from a `\"use client\"`\ncomponent. The block imports nothing but React and the kit — it holds no\nconversation and calls no model. The messages come in as a prop and the\nsend goes out as `onSend`; where the reply comes from (your server action,\nthe AI SDK's `useChat`, a support inbox table) is the surface's.\n\nThis is not the app window's Agent tab — the OS draws that. It is for a\nconversation that lives on your own screen: a support reply pane, a\ncustomer-facing assistant, comments on a record.\n\n## The data\n\nA message:\n\n```js\n{\n id: \"m5\", // stable; the list is keyed on it\n role: \"user\", // user · assistant · system\n content: \"It's still showing…\", // a string — or `parts: [{ type: \"text\", text }]`\n createdAt: \"2026-09-23T10:04:00Z\",\n author: { name: \"Maya Chen\", src: \"https://…\" }, // optional\n status: \"failed\", // sending · streaming · failed · nothing (sent)\n error: \"you are offline\", // shown with a failed message\n attachments: [{ id, name, size, type, url }], // size in bytes; url shows an image's thumbnail\n reasoning: \"They asked about the double charge…\", // assistant only: folded over the reply\n reasoningDuration: 4, // seconds: \"Thought for 4s\"\n feedback: \"up\", // up · down — the thumbs already given (assistant only)\n}\n```\n\n`reasoning` is drawn in a kit `Collapsible`, closed, over the reply.\nWhile the message streams with no answer yet it reads \"Thinking…\"; give\n`reasoning: \"\"` from the start to show that line instead of the three\ndots.\n\nA message with `parts` (the AI SDK's shape) is drawn from its text parts;\nother parts are not drawn. A `system` message is a centred muted line.\nAn assistant message with `status: \"streaming\"` and no text yet shows\nthree dots; with text it shows a caret after it.\n\nThe words are drawn by a small safe renderer — never as HTML, and with no\nmarkdown library. It understands fenced code blocks (with a Copy button;\nan unclosed fence runs to the end, so a block still streaming already\nlooks like one), `code spans`, `**bold**` and line breaks. Everything else\n(links, lists, headings) is plain text. `<script>` in a message is shown,\nnot run.\n\n## Props\n\n### ChatThread\n\n- `messages` — the conversation, oldest first.\n- `onSend(text, attachments)` — send it. The composer clears at once;\n return `{ ok: false, error }` (or throw) and the text comes back to the\n field with the reason under it. The usual pattern is to add the message\n with `status: \"sending\"` here, then mark it sent or `failed`.\n- `streaming` — a reply is streaming: Send becomes Stop, Esc stops.\n Default: true while any message has `status: \"streaming\"`.\n- `onStop()` — stop the reply.\n- `onRetry(message)` — the Retry on a failed message. Without it, a\n failed message says so and offers nothing.\n- `onRegenerate(message)` — a regenerate button on assistant messages.\n- `actionsOf(message)` — more items for a message's menu, kit `Menu`\n shape: `[{ label, onSelect, icon?, tone?: \"danger\" }]`.\n- `onFeedback(message, \"up\" | \"down\" | null)` — thumbs up and down on\n assistant replies (pressing the chosen one again clears it: `null`).\n The choice shows at once; return `{ ok: false, error }` (or throw) and\n it goes back, with the reason under the message. Store it and pass it\n back as `message.feedback`.\n- `onEdit(message, text)` — an edit button on your own messages. The\n words come back in a field: Enter or Save saves, Esc or Cancel leaves\n them. Return `{ ok: false, error }` and the field stays open with the\n reason. Up arrow in an empty composer edits the last message you sent.\n What an edit does next (drop the replies after it, ask again) is yours.\n- `header` — a node over the list, or the props of a `ChatHeader`\n (`{ title, status, actions, menu }`).\n- `shape` — the composer: `\"box\"` (default) or `\"pill\"` (see Composer).\n- `tools` · `menu` — the composer's bar: nodes of yours, and kit `Menu`\n items behind a \"+\" (see Composer).\n- `suggestions` — prompt chips over the composer:\n `[{ label, prompt?, icon?, description? }]`. Shown while the thread is\n empty; `suggestionsWhen=\"always\"` keeps them. `suggestionMode`\n `\"send\"` (the default here: a press sends `prompt ?? label`) or\n `\"fill\"` (puts it in the field). `suggestionsLook` `\"chips\"` (default)\n or `\"cards\"`.\n- `onLoadMore()` · `hasMore` (false) · `loadingMore` (false) — older\n messages. Reaching the top (or pressing \"Load earlier messages\") calls\n `onLoadMore`; prepend them to `messages` and the reader keeps their place.\n- `attachments` · `onAttach(files)` · `onRemoveAttachment(att)` · `accept`\n — files waiting in the composer. `onAttach` gets the browser's `File`s:\n upload them and pass them back in `attachments` as\n `{ id, name, size, type, url?, progress?, error? }` (`progress` 0–100\n draws a bar; Send waits until every upload is at 100). Without\n `onAttach` there is no paperclip. They reach `onSend` as its second\n argument.\n- `disabled` (false) · `disabledPlaceholder` · `placeholder` — a closed\n or read-only thread.\n- `variant` — how assistant words sit: `\"plain\"` (default, on the page)\n or `\"bubble\"`. User messages are always filled bubbles at the end.\n- `assistantName` (\"Assistant\") · `assistantAvatar` · `userName` (\"You\")\n · `userAvatar` · `showUserAvatar` (false) — who is who, when a message\n has no `author`.\n- `emptyTitle` · `emptyBody` · `emptyState` — what an empty thread says;\n `emptyState` replaces the whole thing.\n- `height` — default `\"min(40rem, 80dvh)\"`; the list scrolls inside it.\n- `label` — what the thread is called to a screen reader.\n- `className` — layout around it (width, margin). Never colour.\n\n### Message\n\n`message` (the shape above), `first` / `last` (its place in a run from\none author: `first` shows the avatar, name and time), `latest` (the\nnewest reply: its actions stay in view instead of waiting for hover), and `variant`,\n`assistantName`, `assistantAvatar`, `userName`, `userAvatar`,\n`showUserAvatar`, `onRetry`, `onRegenerate`, `actionsOf`, `onFeedback`,\n`onEdit` as above. `editing` / `onEditingChange(next)` control the edit\nfield (leave them out and the message keeps its own).\n`onCopy(message, text)` replaces the clipboard write; `formatTime(date)`\nreplaces the time's format.\n\n### Bubble\n\n`from` (`\"user\"` · `\"assistant\"`), `variant` (`\"bubble\"` · `\"plain\"`),\n`first` / `last`, `streaming` (draws the caret), and `text` (drawn by the\nsafe renderer) or `children`.\n\n### ChatHeader\n\n`title`, then `status` (`{ tone: \"success\", label: \"Online\", pulse? }`,\na kit `StatusDot`) or `subtitle` (a muted line) under it, `avatar`\n(`{ name?, src?, status? }`), `actions` (nodes at the end: a \"New chat\"\nButton), `menu` (kit `Menu` items behind a \"⋯\") and `menuLabel`.\n\n### Attachment\n\n`name`, `size` (bytes), `type` (MIME; the extension is used when it is\nmissing), `url` (an image shows itself; anything else opens it),\n`href` (open this instead), `progress` (0–100, a bar until 100), `error`,\n`onRemove`, `removeLabel`. An image's thumbnail is a kit `Image`: a\nskeleton while it loads, the image glyph if it fails.\n\n### MessageScroller\n\n`messages`, `renderMessage(message, { first, last, latest })` (default: a plain\n`Message`), `idOf(message, i)`, `onLoadMore` · `hasMore` · `loadingMore`,\n`emptyState`, `formatDay(date)` (\"Today\", \"Yesterday\", \"Mon 21 Sep\"),\n`announce(message)` (what a screen reader hears when a reply is whole),\n`jumpLabel`, `loadMoreLabel`, `label`, `className` (give it a bounded\nheight, or put it in a flex column), `children` (drawn after the last\nmessage — a typing line, say).\n\nThe list is a kit `ScrollArea`. Once it overflows it joins the tab order\nas a region named `label`, so a keyboard can scroll it.\n\n### Composer\n\n`onSend`, `onStop`, `streaming`, `disabled`, `disabledPlaceholder`,\n`placeholder`, `attachments` · `onAttach` · `onRemoveAttachment` ·\n`accept` as in ChatThread, and `value` / `onValueChange` (control the\ntext — to quote a message into it, say), `maxRows` (8), `canSendEmpty`\n(false), `label` (\"Message\"), `hint` (true: the Enter / Shift+Enter keys\nunder the field, from md up), `autoFocus`, and:\n\n- `shape` — `\"box\"` (default: the text on top, the bar under it) or\n `\"pill\"`: the \"ask anything\" box, one round row with the \"+\" menu and\n the paperclip before the text and `tools` and Send after it. Past one\n line (or with files waiting) it opens into a rounded box, text on top;\n emptied, it is a pill again. The pill shows no key hint.\n- `tools` — nodes in the bar: a model `Select`, a kit `Menu` of\n settings with checkbox items, a `Toggle` for \"Auto\", a mic. The\n composer draws them; what they choose is yours (read it in `onSend`).\n- `menu` · `menuLabel` (\"Add to the message\") — kit `Menu` items behind\n a \"+\" at the start of the bar: \"Add files\", \"Search the web\".\n- `suggestions` — `[{ label, prompt?, icon?, description?, onSelect? }]`,\n drawn `suggestionsPlacement` `\"below\"` (default) or `\"above\"` the\n field. `suggestionMode` `\"fill\"` (default: the prompt goes in the\n field, caret at its end) or `\"send\"`; an item's own `onSelect(item)`\n replaces both. `suggestionsLook` `\"chips\"` (round, centred) or\n `\"cards\"` (a two-column grid with `description` under each label).\n `suggestionsLabel` names the list to a reader.\n- `onEditLast()` — Up arrow in the empty field (ChatThread opens the last\n message you sent for editing).\n\nThe field is the kit's multi-line `InputGroup`: the waiting files sit\nabove the text inside its one border, the paperclip (a kit `FileInput`)\nand the hint in its bar, Send at the end. The picker leaves out files\n`accept` rules out; a drop or a paste is passed on as it comes.\n\n## Example\n\n```jsx\n// components/SupportChat.jsx — client: one ticket's conversation\n\"use client\";\nimport { useState } from \"react\";\nimport { insertRow } from \"../lib/actions\";\nimport { ChatThread } from \"./blocks/chat\";\n\nexport default function SupportChat({ ticket, initial }) {\n const [messages, setMessages] = useState(initial);\n const patch = (id, change) => setMessages((all) => all.map((m) => (m.id === id ? { ...m, ...change } : m)));\n\n async function send(text) {\n const id = crypto.randomUUID();\n const draft = { id, role: \"user\", content: text, createdAt: new Date().toISOString(), status: \"sending\" };\n setMessages((all) => [...all, draft]);\n const r = await insertRow({ source: \"public.ticket_messages\", values: { id, ticket_id: ticket.id, body: text }, detail: text });\n patch(id, r.ok ? { status: undefined } : { status: \"failed\", error: r.error });\n }\n\n return (\n <ChatThread\n messages={messages}\n onSend={send}\n onRetry={(m) => { setMessages((all) => all.filter((x) => x.id !== m.id)); send(m.content); }}\n assistantName={ticket.agent_name}\n userName=\"You\"\n label={`Ticket ${ticket.number}`}\n />\n );\n}\n```\n\nWith the AI SDK in a shadcn project, `useChat()` already has the shape:\n`<ChatThread messages={messages} onSend={(text) => sendMessage({ text })}\nstreaming={status === \"streaming\" || status === \"submitted\"} onStop={stop}\nonRegenerate={() => regenerate()} />`.\n\n## What it will not do\n\n- Call a model, store a message or upload a file: those are the\n surface's, through `onSend`, `onAttach` and the rows it passes back.\n- Render HTML or full markdown: links, lists, tables and images inside\n the text stay plain text.\n- Keep the feedback or the edit: `onFeedback` and `onEdit` hand them to\n you, and the rows you pass back are what shows. Emoji reactions are\n yours, in `actionsOf`.\n- Record audio or pick a model: a mic or a model `Select` is a node you\n put in `tools`.\n- Draw the app window's Agent tab, or a job the founder commissioned —\n that is `job-panel`.\n\nA chat it cannot be is your own component in `components/`, built from\nthese parts and the kit — not an edit to this block. Its files are fixed:\n`npm run check` refuses a change, and `makersclaw add chat` restores them.\n\n## Credits\n\nThe pill composer, tools, suggestions, feedback, edit, reasoning and header are ported from blocks.so (https://blocks.so, ai-01–05 and chat-01–03, MIT) and Creative Tim UI (https://github.com/creativetimofficial/ui, ai-chat-streaming-01, MIT), rewritten on the Makersclaw kit.\n"
65
+ }
66
+ ],
67
+ "categories": [
68
+ "chat",
69
+ "messages",
70
+ "conversation",
71
+ "thread",
72
+ "assistant",
73
+ "ai",
74
+ "support",
75
+ "inbox",
76
+ "composer",
77
+ "attachments",
78
+ "streaming",
79
+ "bubble",
80
+ "reply",
81
+ "feedback",
82
+ "reasoning",
83
+ "prompt",
84
+ "suggestions",
85
+ "ask anything",
86
+ "header"
87
+ ],
88
+ "docs": "Installed to components/makersclaw/blocks/chat. Import: import { ChatThread, ChatHeader, Message, Bubble, Attachment, MessageScroller, Composer } from \"@/components/makersclaw/blocks/chat\". Props and an example: components/makersclaw/blocks/chat/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
89
+ "meta": {
90
+ "makersclaw": {
91
+ "version": "0.3.1",
92
+ "useWhen": "A conversation lives inside the app's own screen: a support inbox reply pane, a customer-facing assistant, comments between teammates on a record, a thread per object. Not the app window's Agent tab (the OS draws that) and not a one-off job the founder commissions (job-panel)."
93
+ }
94
+ }
95
+ }