makersclaw 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (473) hide show
  1. package/THIRD-PARTY-NOTICES.md +180 -0
  2. package/package.json +3 -2
  3. package/r/annotate.json +62 -0
  4. package/r/approval-request.json +52 -0
  5. package/r/audio-player.json +74 -0
  6. package/r/auth-form.json +55 -0
  7. package/r/board.json +10 -6
  8. package/r/calendar-view.json +105 -0
  9. package/r/cart.json +62 -0
  10. package/r/chart.json +83 -0
  11. package/r/chat.json +95 -0
  12. package/r/code-editor.json +94 -0
  13. package/r/comment-box.json +64 -0
  14. package/r/contact-section.json +53 -0
  15. package/r/core.json +205 -8
  16. package/r/csv-import.json +63 -0
  17. package/r/data-table.json +69 -0
  18. package/r/diagram.json +74 -0
  19. package/r/doc-editor.json +82 -0
  20. package/r/doc-page.json +61 -0
  21. package/r/email-render.json +73 -0
  22. package/r/faq.json +50 -0
  23. package/r/file-upload.json +54 -0
  24. package/r/flow-canvas.json +93 -0
  25. package/r/footer.json +52 -0
  26. package/r/gantt.json +70 -0
  27. package/r/job-panel.json +12 -7
  28. package/r/json-editor.json +66 -0
  29. package/r/map.json +89 -0
  30. package/r/menubar.json +52 -0
  31. package/r/mind-map.json +67 -0
  32. package/r/navigation-menu.json +52 -0
  33. package/r/order-detail.json +54 -0
  34. package/r/org-chart.json +66 -0
  35. package/r/page-builder.json +105 -0
  36. package/r/payment-methods.json +51 -0
  37. package/r/pdf-document.json +79 -0
  38. package/r/pdf-viewer.json +88 -0
  39. package/r/product-detail.json +76 -0
  40. package/r/product-grid.json +65 -0
  41. package/r/record-form.json +67 -0
  42. package/r/record-notes.json +55 -0
  43. package/r/registry.json +3411 -29
  44. package/r/search-dialog.json +51 -0
  45. package/r/settings-list.json +66 -0
  46. package/r/setup-checklist.json +51 -0
  47. package/r/share-dialog.json +52 -0
  48. package/r/sidebar.json +69 -0
  49. package/r/signature-pad.json +72 -0
  50. package/r/sketch-canvas.json +99 -0
  51. package/r/spreadsheet.json +90 -0
  52. package/r/stat-cards.json +66 -0
  53. package/r/task-list.json +63 -0
  54. package/r/task-model.json +98 -0
  55. package/r/testimonials.json +52 -0
  56. package/r/transaction-list.json +53 -0
  57. package/r/whiteboard.json +66 -0
  58. package/registry/blocks/annotate/README.md +179 -0
  59. package/registry/blocks/annotate/annotate-data.js +250 -0
  60. package/registry/blocks/annotate/annotate.jsx +646 -0
  61. package/registry/blocks/annotate/block.json +10 -0
  62. package/registry/blocks/annotate/demo.jsx +135 -0
  63. package/registry/blocks/annotate/index.js +15 -0
  64. package/registry/blocks/approval-request/README.md +116 -0
  65. package/registry/blocks/approval-request/approval-request.jsx +386 -0
  66. package/registry/blocks/approval-request/block.json +10 -0
  67. package/registry/blocks/approval-request/demo.jsx +127 -0
  68. package/registry/blocks/approval-request/index.js +4 -0
  69. package/registry/blocks/audio-player/README.md +167 -0
  70. package/registry/blocks/audio-player/audio-player.jsx +623 -0
  71. package/registry/blocks/audio-player/block.json +14 -0
  72. package/registry/blocks/audio-player/colors.js +109 -0
  73. package/registry/blocks/audio-player/demo.jsx +209 -0
  74. package/registry/blocks/audio-player/index.js +6 -0
  75. package/registry/blocks/audio-player/time.js +67 -0
  76. package/registry/blocks/audio-player/wavesurfer.js +9 -0
  77. package/registry/blocks/auth-form/README.md +126 -0
  78. package/registry/blocks/auth-form/auth-form.jsx +568 -0
  79. package/registry/blocks/auth-form/block.json +10 -0
  80. package/registry/blocks/auth-form/demo.jsx +131 -0
  81. package/registry/blocks/auth-form/index.js +3 -0
  82. package/registry/blocks/board/README.md +55 -5
  83. package/registry/blocks/board/block.json +2 -2
  84. package/registry/blocks/board/board.jsx +230 -86
  85. package/registry/blocks/board/demo.jsx +103 -24
  86. package/registry/blocks/calendar-view/README.md +180 -0
  87. package/registry/blocks/calendar-view/agenda.jsx +147 -0
  88. package/registry/blocks/calendar-view/block.json +76 -0
  89. package/registry/blocks/calendar-view/calendar-model.js +442 -0
  90. package/registry/blocks/calendar-view/calendar.jsx +627 -0
  91. package/registry/blocks/calendar-view/demo.jsx +195 -0
  92. package/registry/blocks/calendar-view/draft.jsx +49 -0
  93. package/registry/blocks/calendar-view/index.js +39 -0
  94. package/registry/blocks/calendar-view/month.jsx +346 -0
  95. package/registry/blocks/calendar-view/paint.js +69 -0
  96. package/registry/blocks/calendar-view/time-grid.jsx +527 -0
  97. package/registry/blocks/calendar-view/tray.jsx +87 -0
  98. package/registry/blocks/cart/README.md +146 -0
  99. package/registry/blocks/cart/block.json +10 -0
  100. package/registry/blocks/cart/cart.jsx +597 -0
  101. package/registry/blocks/cart/demo.jsx +98 -0
  102. package/registry/blocks/cart/index.js +4 -0
  103. package/registry/blocks/cart/money.js +28 -0
  104. package/registry/blocks/chart/README.md +316 -0
  105. package/registry/blocks/chart/block.json +10 -0
  106. package/registry/blocks/chart/cartesian.jsx +642 -0
  107. package/registry/blocks/chart/core.jsx +262 -0
  108. package/registry/blocks/chart/demo.jsx +357 -0
  109. package/registry/blocks/chart/heatmap.jsx +336 -0
  110. package/registry/blocks/chart/index.js +8 -0
  111. package/registry/blocks/chart/pie.jsx +245 -0
  112. package/registry/blocks/chart/sparkline.jsx +88 -0
  113. package/registry/blocks/chat/README.md +273 -0
  114. package/registry/blocks/chat/attachment.jsx +95 -0
  115. package/registry/blocks/chat/block.json +10 -0
  116. package/registry/blocks/chat/chat.jsx +132 -0
  117. package/registry/blocks/chat/composer.jsx +364 -0
  118. package/registry/blocks/chat/demo.jsx +578 -0
  119. package/registry/blocks/chat/header.jsx +34 -0
  120. package/registry/blocks/chat/index.js +9 -0
  121. package/registry/blocks/chat/message.jsx +450 -0
  122. package/registry/blocks/chat/scroller.jsx +247 -0
  123. package/registry/blocks/chat/text.jsx +104 -0
  124. package/registry/blocks/code-editor/README.md +210 -0
  125. package/registry/blocks/code-editor/block.json +22 -0
  126. package/registry/blocks/code-editor/code-editor.jsx +534 -0
  127. package/registry/blocks/code-editor/codemirror.js +38 -0
  128. package/registry/blocks/code-editor/demo.jsx +205 -0
  129. package/registry/blocks/code-editor/index.js +8 -0
  130. package/registry/blocks/code-editor/json.js +317 -0
  131. package/registry/blocks/code-editor/sql.js +136 -0
  132. package/registry/blocks/code-editor/text.js +101 -0
  133. package/registry/blocks/code-editor/theme.js +186 -0
  134. package/registry/blocks/comment-box/README.md +123 -0
  135. package/registry/blocks/comment-box/block.json +10 -0
  136. package/registry/blocks/comment-box/comment-body.jsx +217 -0
  137. package/registry/blocks/comment-box/comment-box.jsx +198 -0
  138. package/registry/blocks/comment-box/comment-time.jsx +77 -0
  139. package/registry/blocks/comment-box/demo.jsx +141 -0
  140. package/registry/blocks/comment-box/index.js +8 -0
  141. package/registry/blocks/contact-section/README.md +152 -0
  142. package/registry/blocks/contact-section/block.json +11 -0
  143. package/registry/blocks/contact-section/contact-section.jsx +509 -0
  144. package/registry/blocks/contact-section/demo.jsx +93 -0
  145. package/registry/blocks/contact-section/index.js +4 -0
  146. package/registry/blocks/csv-import/README.md +133 -0
  147. package/registry/blocks/csv-import/block.json +13 -0
  148. package/registry/blocks/csv-import/csv-import.jsx +479 -0
  149. package/registry/blocks/csv-import/demo.jsx +106 -0
  150. package/registry/blocks/csv-import/import.js +200 -0
  151. package/registry/blocks/csv-import/index.js +7 -0
  152. package/registry/blocks/data-table/README.md +386 -0
  153. package/registry/blocks/data-table/block.json +10 -0
  154. package/registry/blocks/data-table/data-table.jsx +1164 -0
  155. package/registry/blocks/data-table/demo.jsx +588 -0
  156. package/registry/blocks/data-table/index.js +5 -0
  157. package/registry/blocks/data-table/model.js +169 -0
  158. package/registry/blocks/diagram/README.md +108 -0
  159. package/registry/blocks/diagram/block.json +13 -0
  160. package/registry/blocks/diagram/demo.jsx +113 -0
  161. package/registry/blocks/diagram/diagram.jsx +481 -0
  162. package/registry/blocks/diagram/index.js +7 -0
  163. package/registry/blocks/diagram/mermaid.js +52 -0
  164. package/registry/blocks/diagram/palette.js +313 -0
  165. package/registry/blocks/diagram/source.js +98 -0
  166. package/registry/blocks/doc-editor/README.md +215 -0
  167. package/registry/blocks/doc-editor/block.json +15 -0
  168. package/registry/blocks/doc-editor/demo.jsx +225 -0
  169. package/registry/blocks/doc-editor/doc-editor.jsx +291 -0
  170. package/registry/blocks/doc-editor/doc.js +309 -0
  171. package/registry/blocks/doc-editor/index.js +7 -0
  172. package/registry/blocks/doc-editor/schema.jsx +56 -0
  173. package/registry/blocks/doc-editor/shadcn-classes.js +110 -0
  174. package/registry/blocks/doc-editor/theme.js +82 -0
  175. package/registry/blocks/doc-page/README.md +119 -0
  176. package/registry/blocks/doc-page/autosave.js +104 -0
  177. package/registry/blocks/doc-page/block.json +10 -0
  178. package/registry/blocks/doc-page/demo.jsx +137 -0
  179. package/registry/blocks/doc-page/doc-page.jsx +276 -0
  180. package/registry/blocks/doc-page/index.js +4 -0
  181. package/registry/blocks/email-render/README.md +151 -0
  182. package/registry/blocks/email-render/block.json +14 -0
  183. package/registry/blocks/email-render/demo.jsx +153 -0
  184. package/registry/blocks/email-render/doc-email.jsx +210 -0
  185. package/registry/blocks/email-render/email.jsx +221 -0
  186. package/registry/blocks/email-render/index.js +10 -0
  187. package/registry/blocks/email-render/render.js +50 -0
  188. package/registry/blocks/email-render/theme.js +215 -0
  189. package/registry/blocks/faq/README.md +101 -0
  190. package/registry/blocks/faq/block.json +11 -0
  191. package/registry/blocks/faq/demo.jsx +89 -0
  192. package/registry/blocks/faq/faq.jsx +220 -0
  193. package/registry/blocks/faq/index.js +4 -0
  194. package/registry/blocks/file-upload/README.md +138 -0
  195. package/registry/blocks/file-upload/block.json +10 -0
  196. package/registry/blocks/file-upload/demo.jsx +204 -0
  197. package/registry/blocks/file-upload/file-upload.jsx +635 -0
  198. package/registry/blocks/file-upload/index.js +4 -0
  199. package/registry/blocks/flow-canvas/README.md +420 -0
  200. package/registry/blocks/flow-canvas/block.json +13 -0
  201. package/registry/blocks/flow-canvas/board.js +819 -0
  202. package/registry/blocks/flow-canvas/chrome.jsx +649 -0
  203. package/registry/blocks/flow-canvas/context.jsx +35 -0
  204. package/registry/blocks/flow-canvas/demo.jsx +249 -0
  205. package/registry/blocks/flow-canvas/edges.jsx +107 -0
  206. package/registry/blocks/flow-canvas/flow-canvas.jsx +1329 -0
  207. package/registry/blocks/flow-canvas/index.js +41 -0
  208. package/registry/blocks/flow-canvas/nodes.jsx +392 -0
  209. package/registry/blocks/flow-canvas/theme.js +174 -0
  210. package/registry/blocks/footer/README.md +133 -0
  211. package/registry/blocks/footer/block.json +11 -0
  212. package/registry/blocks/footer/demo.jsx +119 -0
  213. package/registry/blocks/footer/footer.jsx +406 -0
  214. package/registry/blocks/footer/index.js +4 -0
  215. package/registry/blocks/gantt/README.md +220 -0
  216. package/registry/blocks/gantt/block.json +10 -0
  217. package/registry/blocks/gantt/demo.jsx +148 -0
  218. package/registry/blocks/gantt/gantt.jsx +923 -0
  219. package/registry/blocks/gantt/index.js +7 -0
  220. package/registry/blocks/gantt/rows.jsx +468 -0
  221. package/registry/blocks/gantt/schedule.js +278 -0
  222. package/registry/blocks/job-panel/README.md +80 -0
  223. package/registry/blocks/job-panel/block.json +3 -3
  224. package/registry/blocks/job-panel/demo.jsx +230 -3
  225. package/registry/blocks/job-panel/job-panel.jsx +169 -3
  226. package/registry/blocks/json-editor/README.md +133 -0
  227. package/registry/blocks/json-editor/block.json +10 -0
  228. package/registry/blocks/json-editor/demo.jsx +92 -0
  229. package/registry/blocks/json-editor/index.js +7 -0
  230. package/registry/blocks/json-editor/json-editor.jsx +305 -0
  231. package/registry/blocks/json-editor/json-tree.jsx +393 -0
  232. package/registry/blocks/json-editor/tree.js +225 -0
  233. package/registry/blocks/map/README.md +221 -0
  234. package/registry/blocks/map/block.json +14 -0
  235. package/registry/blocks/map/demo.jsx +201 -0
  236. package/registry/blocks/map/geo.js +198 -0
  237. package/registry/blocks/map/index.js +8 -0
  238. package/registry/blocks/map/list.jsx +128 -0
  239. package/registry/blocks/map/map-view.jsx +715 -0
  240. package/registry/blocks/map/maplibre.js +30 -0
  241. package/registry/blocks/map/marks.jsx +146 -0
  242. package/registry/blocks/map/theme.js +142 -0
  243. package/registry/blocks/menubar/README.md +109 -0
  244. package/registry/blocks/menubar/block.json +10 -0
  245. package/registry/blocks/menubar/demo.jsx +128 -0
  246. package/registry/blocks/menubar/index.js +4 -0
  247. package/registry/blocks/menubar/menubar.jsx +199 -0
  248. package/registry/blocks/mind-map/README.md +206 -0
  249. package/registry/blocks/mind-map/block.json +10 -0
  250. package/registry/blocks/mind-map/demo.jsx +103 -0
  251. package/registry/blocks/mind-map/index.js +31 -0
  252. package/registry/blocks/mind-map/mind-map.jsx +1125 -0
  253. package/registry/blocks/mind-map/topic.jsx +143 -0
  254. package/registry/blocks/mind-map/tree.js +645 -0
  255. package/registry/blocks/navigation-menu/README.md +124 -0
  256. package/registry/blocks/navigation-menu/block.json +10 -0
  257. package/registry/blocks/navigation-menu/demo.jsx +84 -0
  258. package/registry/blocks/navigation-menu/index.js +4 -0
  259. package/registry/blocks/navigation-menu/navigation-menu.jsx +448 -0
  260. package/registry/blocks/order-detail/README.md +109 -0
  261. package/registry/blocks/order-detail/block.json +10 -0
  262. package/registry/blocks/order-detail/demo.jsx +88 -0
  263. package/registry/blocks/order-detail/index.js +4 -0
  264. package/registry/blocks/order-detail/order-detail.jsx +354 -0
  265. package/registry/blocks/org-chart/README.md +145 -0
  266. package/registry/blocks/org-chart/block.json +10 -0
  267. package/registry/blocks/org-chart/demo.jsx +94 -0
  268. package/registry/blocks/org-chart/index.js +7 -0
  269. package/registry/blocks/org-chart/org-chart.jsx +631 -0
  270. package/registry/blocks/org-chart/org.js +245 -0
  271. package/registry/blocks/org-chart/person.jsx +96 -0
  272. package/registry/blocks/page-builder/README.md +257 -0
  273. package/registry/blocks/page-builder/block.json +14 -0
  274. package/registry/blocks/page-builder/demo.jsx +145 -0
  275. package/registry/blocks/page-builder/grapes.js +18 -0
  276. package/registry/blocks/page-builder/index.js +11 -0
  277. package/registry/blocks/page-builder/page-builder.jsx +704 -0
  278. package/registry/blocks/page-builder/page-view.jsx +33 -0
  279. package/registry/blocks/page-builder/page.js +118 -0
  280. package/registry/blocks/page-builder/panels.jsx +588 -0
  281. package/registry/blocks/page-builder/sections.js +209 -0
  282. package/registry/blocks/page-builder/theme.js +87 -0
  283. package/registry/blocks/page-builder/thumbs.jsx +189 -0
  284. package/registry/blocks/page-builder/tokens.js +329 -0
  285. package/registry/blocks/payment-methods/README.md +104 -0
  286. package/registry/blocks/payment-methods/block.json +10 -0
  287. package/registry/blocks/payment-methods/demo.jsx +71 -0
  288. package/registry/blocks/payment-methods/index.js +4 -0
  289. package/registry/blocks/payment-methods/payment-methods.jsx +302 -0
  290. package/registry/blocks/pdf-document/README.md +213 -0
  291. package/registry/blocks/pdf-document/block.json +13 -0
  292. package/registry/blocks/pdf-document/demo.jsx +136 -0
  293. package/registry/blocks/pdf-document/index.js +33 -0
  294. package/registry/blocks/pdf-document/money.js +83 -0
  295. package/registry/blocks/pdf-document/pdf-document.jsx +144 -0
  296. package/registry/blocks/pdf-document/primitives.jsx +271 -0
  297. package/registry/blocks/pdf-document/quote.jsx +179 -0
  298. package/registry/blocks/pdf-document/theme.js +193 -0
  299. package/registry/blocks/pdf-viewer/README.md +191 -0
  300. package/registry/blocks/pdf-viewer/block.json +14 -0
  301. package/registry/blocks/pdf-viewer/demo.jsx +252 -0
  302. package/registry/blocks/pdf-viewer/helpers.js +270 -0
  303. package/registry/blocks/pdf-viewer/index.js +8 -0
  304. package/registry/blocks/pdf-viewer/notes.jsx +195 -0
  305. package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1368 -0
  306. package/registry/blocks/pdf-viewer/pdfjs.js +37 -0
  307. package/registry/blocks/pdf-viewer/ssr-shim-end.js +6 -0
  308. package/registry/blocks/pdf-viewer/ssr-shim.js +44 -0
  309. package/registry/blocks/product-detail/README.md +217 -0
  310. package/registry/blocks/product-detail/block.json +10 -0
  311. package/registry/blocks/product-detail/demo.jsx +233 -0
  312. package/registry/blocks/product-detail/index.js +7 -0
  313. package/registry/blocks/product-detail/price.jsx +115 -0
  314. package/registry/blocks/product-detail/product-detail.jsx +348 -0
  315. package/registry/blocks/product-detail/product-media.jsx +102 -0
  316. package/registry/blocks/product-detail/product-options.jsx +144 -0
  317. package/registry/blocks/product-grid/README.md +162 -0
  318. package/registry/blocks/product-grid/block.json +10 -0
  319. package/registry/blocks/product-grid/demo.jsx +255 -0
  320. package/registry/blocks/product-grid/index.js +5 -0
  321. package/registry/blocks/product-grid/product-card.jsx +232 -0
  322. package/registry/blocks/product-grid/product-grid.jsx +235 -0
  323. package/registry/blocks/record-form/README.md +196 -0
  324. package/registry/blocks/record-form/block.json +10 -0
  325. package/registry/blocks/record-form/demo.jsx +525 -0
  326. package/registry/blocks/record-form/index.js +4 -0
  327. package/registry/blocks/record-form/record-fields.jsx +457 -0
  328. package/registry/blocks/record-form/record-form.jsx +415 -0
  329. package/registry/blocks/record-form/rules.js +81 -0
  330. package/registry/blocks/record-notes/README.md +139 -0
  331. package/registry/blocks/record-notes/block.json +10 -0
  332. package/registry/blocks/record-notes/demo.jsx +179 -0
  333. package/registry/blocks/record-notes/index.js +4 -0
  334. package/registry/blocks/record-notes/record-notes.jsx +266 -0
  335. package/registry/blocks/search-dialog/README.md +108 -0
  336. package/registry/blocks/search-dialog/block.json +10 -0
  337. package/registry/blocks/search-dialog/demo.jsx +116 -0
  338. package/registry/blocks/search-dialog/index.js +3 -0
  339. package/registry/blocks/search-dialog/search-dialog.jsx +235 -0
  340. package/registry/blocks/settings-list/README.md +171 -0
  341. package/registry/blocks/settings-list/block.json +10 -0
  342. package/registry/blocks/settings-list/demo.jsx +333 -0
  343. package/registry/blocks/settings-list/index.js +4 -0
  344. package/registry/blocks/settings-list/row-state.jsx +93 -0
  345. package/registry/blocks/settings-list/settings-list.jsx +349 -0
  346. package/registry/blocks/settings-list/settings-matrix.jsx +118 -0
  347. package/registry/blocks/setup-checklist/README.md +142 -0
  348. package/registry/blocks/setup-checklist/block.json +10 -0
  349. package/registry/blocks/setup-checklist/demo.jsx +159 -0
  350. package/registry/blocks/setup-checklist/index.js +4 -0
  351. package/registry/blocks/setup-checklist/setup-checklist.jsx +446 -0
  352. package/registry/blocks/share-dialog/README.md +113 -0
  353. package/registry/blocks/share-dialog/block.json +10 -0
  354. package/registry/blocks/share-dialog/demo.jsx +140 -0
  355. package/registry/blocks/share-dialog/index.js +3 -0
  356. package/registry/blocks/share-dialog/share-dialog.jsx +384 -0
  357. package/registry/blocks/sidebar/README.md +190 -0
  358. package/registry/blocks/sidebar/block.json +43 -0
  359. package/registry/blocks/sidebar/demo.jsx +577 -0
  360. package/registry/blocks/sidebar/index.js +4 -0
  361. package/registry/blocks/sidebar/sidebar-panel.jsx +149 -0
  362. package/registry/blocks/sidebar/sidebar.jsx +864 -0
  363. package/registry/blocks/signature-pad/README.md +137 -0
  364. package/registry/blocks/signature-pad/block.json +13 -0
  365. package/registry/blocks/signature-pad/colors.js +115 -0
  366. package/registry/blocks/signature-pad/demo.jsx +94 -0
  367. package/registry/blocks/signature-pad/index.js +6 -0
  368. package/registry/blocks/signature-pad/pad.js +7 -0
  369. package/registry/blocks/signature-pad/signature-pad.jsx +434 -0
  370. package/registry/blocks/signature-pad/signature.js +68 -0
  371. package/registry/blocks/sketch-canvas/README.md +280 -0
  372. package/registry/blocks/sketch-canvas/block.json +13 -0
  373. package/registry/blocks/sketch-canvas/demo.jsx +159 -0
  374. package/registry/blocks/sketch-canvas/excalidraw.js +40 -0
  375. package/registry/blocks/sketch-canvas/index.js +9 -0
  376. package/registry/blocks/sketch-canvas/palette.js +226 -0
  377. package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1188 -0
  378. package/registry/blocks/sketch-canvas/sketch.js +179 -0
  379. package/registry/blocks/sketch-canvas/ssr-shim-end.js +6 -0
  380. package/registry/blocks/sketch-canvas/ssr-shim.js +45 -0
  381. package/registry/blocks/sketch-canvas/stamps.js +104 -0
  382. package/registry/blocks/sketch-canvas/theme.js +131 -0
  383. package/registry/blocks/spreadsheet/README.md +307 -0
  384. package/registry/blocks/spreadsheet/block.json +13 -0
  385. package/registry/blocks/spreadsheet/cells.jsx +455 -0
  386. package/registry/blocks/spreadsheet/chrome.jsx +325 -0
  387. package/registry/blocks/spreadsheet/demo.jsx +177 -0
  388. package/registry/blocks/spreadsheet/grid.js +387 -0
  389. package/registry/blocks/spreadsheet/index.js +34 -0
  390. package/registry/blocks/spreadsheet/spreadsheet.jsx +1732 -0
  391. package/registry/blocks/spreadsheet/types.js +473 -0
  392. package/registry/blocks/stat-cards/README.md +173 -0
  393. package/registry/blocks/stat-cards/block.json +10 -0
  394. package/registry/blocks/stat-cards/demo.jsx +257 -0
  395. package/registry/blocks/stat-cards/index.js +4 -0
  396. package/registry/blocks/stat-cards/stat-cards.jsx +307 -0
  397. package/registry/blocks/stat-cards/usage.jsx +243 -0
  398. package/registry/blocks/task-list/README.md +176 -0
  399. package/registry/blocks/task-list/block.json +10 -0
  400. package/registry/blocks/task-list/demo.jsx +134 -0
  401. package/registry/blocks/task-list/index.js +5 -0
  402. package/registry/blocks/task-list/task-list.jsx +907 -0
  403. package/registry/blocks/task-model/README.md +455 -0
  404. package/registry/blocks/task-model/block.json +167 -0
  405. package/registry/blocks/task-model/changes.js +89 -0
  406. package/registry/blocks/task-model/dates.js +371 -0
  407. package/registry/blocks/task-model/demo.jsx +215 -0
  408. package/registry/blocks/task-model/dnd.jsx +240 -0
  409. package/registry/blocks/task-model/index.js +97 -0
  410. package/registry/blocks/task-model/model.js +919 -0
  411. package/registry/blocks/task-model/parts.jsx +449 -0
  412. package/registry/blocks/task-model/toolbar.jsx +271 -0
  413. package/registry/blocks/testimonials/README.md +139 -0
  414. package/registry/blocks/testimonials/block.json +11 -0
  415. package/registry/blocks/testimonials/demo.jsx +137 -0
  416. package/registry/blocks/testimonials/index.js +4 -0
  417. package/registry/blocks/testimonials/testimonials.jsx +430 -0
  418. package/registry/blocks/transaction-list/README.md +126 -0
  419. package/registry/blocks/transaction-list/block.json +10 -0
  420. package/registry/blocks/transaction-list/demo.jsx +79 -0
  421. package/registry/blocks/transaction-list/index.js +4 -0
  422. package/registry/blocks/transaction-list/transaction-list.jsx +383 -0
  423. package/registry/blocks/whiteboard/README.md +179 -0
  424. package/registry/blocks/whiteboard/block.json +10 -0
  425. package/registry/blocks/whiteboard/demo.jsx +169 -0
  426. package/registry/blocks/whiteboard/index.js +6 -0
  427. package/registry/blocks/whiteboard/templates.js +150 -0
  428. package/registry/blocks/whiteboard/whiteboard.jsx +721 -0
  429. package/src/install.mjs +18 -0
  430. package/src/shadcn.mjs +8 -0
  431. package/templates/next/files/README.md +145 -11
  432. package/templates/next/files/app/globals.css +55 -4
  433. package/templates/next/files/check/run.mjs +3 -3
  434. package/templates/next/files/components/ui/avatar.jsx +141 -0
  435. package/templates/next/files/components/ui/breadcrumb.jsx +149 -0
  436. package/templates/next/files/components/ui/button-group.jsx +70 -0
  437. package/templates/next/files/components/ui/button.jsx +132 -12
  438. package/templates/next/files/components/ui/calendar.jsx +609 -0
  439. package/templates/next/files/components/ui/carousel.jsx +281 -0
  440. package/templates/next/files/components/ui/collapsible.jsx +208 -0
  441. package/templates/next/files/components/ui/combobox.jsx +608 -0
  442. package/templates/next/files/components/ui/command.jsx +577 -0
  443. package/templates/next/files/components/ui/context-menu.jsx +191 -0
  444. package/templates/next/files/components/ui/date-picker.jsx +360 -0
  445. package/templates/next/files/components/ui/distribution.jsx +117 -0
  446. package/templates/next/files/components/ui/feedback.jsx +143 -0
  447. package/templates/next/files/components/ui/field.jsx +115 -3
  448. package/templates/next/files/components/ui/file-input.jsx +234 -0
  449. package/templates/next/files/components/ui/icon.jsx +130 -8
  450. package/templates/next/files/components/ui/image.jsx +143 -0
  451. package/templates/next/files/components/ui/index.js +47 -2
  452. package/templates/next/files/components/ui/input-group.jsx +213 -0
  453. package/templates/next/files/components/ui/input-otp.jsx +170 -0
  454. package/templates/next/files/components/ui/item.jsx +206 -0
  455. package/templates/next/files/components/ui/menu.jsx +454 -82
  456. package/templates/next/files/components/ui/notice.jsx +126 -14
  457. package/templates/next/files/components/ui/pagination.jsx +156 -0
  458. package/templates/next/files/components/ui/popover.jsx +284 -0
  459. package/templates/next/files/components/ui/radio.jsx +193 -0
  460. package/templates/next/files/components/ui/rating.jsx +131 -0
  461. package/templates/next/files/components/ui/resizable.jsx +338 -0
  462. package/templates/next/files/components/ui/scroll-area.jsx +123 -0
  463. package/templates/next/files/components/ui/separator.jsx +119 -0
  464. package/templates/next/files/components/ui/sheet.jsx +96 -11
  465. package/templates/next/files/components/ui/slider.jsx +174 -0
  466. package/templates/next/files/components/ui/stat.jsx +574 -0
  467. package/templates/next/files/components/ui/steps.jsx +338 -0
  468. package/templates/next/files/components/ui/swatch.jsx +33 -0
  469. package/templates/next/files/components/ui/table.jsx +130 -0
  470. package/templates/next/files/components/ui/toast.jsx +440 -0
  471. package/templates/next/files/components/ui/toggle.jsx +195 -0
  472. package/templates/next/files/components/ui/tooltip.jsx +270 -0
  473. package/templates/next/template.json +264 -8
@@ -0,0 +1,63 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "csv-import",
4
+ "type": "registry:block",
5
+ "title": "CsvImport",
6
+ "description": "Import a CSV in three calm steps: drop a file (or paste text, or download a template), match each file column to a field with automatic matches and sample values (or, with no fields, name and type a new table's fields from guessed types), then review every row in an editable spreadsheet where cells that do not fit their type are marked red and fixed in place, delete rows or leave the bad ones out, and import through one async onImport that answers { ok }. Quoted commas and newlines, TSV and semicolon files, byte-order marks, 50,000 rows.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/core.json",
9
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/spreadsheet.json"
10
+ ],
11
+ "dependencies": [
12
+ "papaparse@5.7.0"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "registry/blocks/csv-import/index.js",
17
+ "type": "registry:component",
18
+ "target": "@components/makersclaw/blocks/csv-import/index.js",
19
+ "content": "// The CsvImport block — `makersclaw add csv-import`. Fixed: compose it\n// through its props (README.md beside this file). The helpers (read CSV\n// text, match columns, guess types, build typed rows) are plain functions\n// and work anywhere. Built on the spreadsheet block (its column types and\n// its grid for the review); papaparse reads the CSV.\nexport { CsvImport } from \"./csv-import\";\nexport { parseCsvText, readCsvFile, headersOf, autoMatch, guessColumns, buildRows, cleanRow, templateCsv } from \"./import\";\n"
20
+ },
21
+ {
22
+ "path": "registry/blocks/csv-import/csv-import.jsx",
23
+ "type": "registry:component",
24
+ "target": "@components/makersclaw/blocks/csv-import/csv-import.jsx",
25
+ "content": "\"use client\";\n\n// CsvImport: a CSV in, typed rows out, in three steps — Upload, Match\n// columns, Review — then one call to onImport. Nothing is saved before\n// that call. The review is a Spreadsheet: a cell that did not fit its\n// field keeps its text and a red corner, and is fixed in place.\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { Button, Card, FileDrop, Icon, Input, Notice, Select, Skeleton, Steps, Switch, Textarea, ToggleGroup, cx } from \"../../ui\";\nimport { Spreadsheet, TYPE_ORDER, typeOf } from \"../spreadsheet\";\nimport { autoMatch, buildRows, cleanRow, guessColumns, headersOf, parseCsvText, readCsvFile, templateCsv } from \"./import\";\n\nconst STEPS = [\"Upload\", \"Match columns\", \"Review\"];\nconst ACCEPT = \".csv,.tsv,.txt,text/csv,text/tab-separated-values,text/plain\";\n\nconst plural = (n, noun) => `${n.toLocaleString()} ${n === 1 ? noun : `${noun}s`}`;\n\n// What the review grid marks: the text that did not fit (kept on the row).\nconst problemOf = (row, col) => row.__problems?.[col.key] ?? null;\n\nfunction hasProblem(row, cols) {\n if (row.__problems && Object.keys(row.__problems).length) return true;\n return cols.some((c) => c.required && typeOf(c).empty(row[c.key]));\n}\n\nexport function CsvImport({\n fields,\n onImport,\n people,\n noun = \"row\",\n title,\n description,\n maxRows = 50000,\n example,\n templateName,\n onCancel,\n onDone,\n doneLabel,\n locale,\n today,\n initialText,\n initialName = \"Pasted text\",\n className,\n}) {\n const intoTable = Array.isArray(fields) && fields.length > 0;\n const ctx = useMemo(() => ({ people: people ?? [], locale }), [people, locale]);\n\n const [step, setStep] = useState(\"upload\");\n const [error, setError] = useState(null);\n const [reading, setReading] = useState(false);\n const [pasting, setPasting] = useState(false);\n const [pasted, setPasted] = useState(\"\");\n const [source, setSource] = useState(null); // { name, data }\n const [hasHeader, setHasHeader] = useState(true);\n const [mapping, setMapping] = useState([]); // file column → field key | null\n const [newCols, setNewCols] = useState([]); // a new table's fields, one per file column (type \"\" = skip)\n const [rows, setRows] = useState([]);\n const [cols, setCols] = useState([]);\n const [only, setOnly] = useState(\"all\");\n const [skipBad, setSkipBad] = useState(false);\n const [busy, setBusy] = useState(false);\n const [result, setResult] = useState(null);\n\n const headers = useMemo(() => (source ? headersOf(source.data, hasHeader) : []), [source, hasHeader]);\n const body = useMemo(() => (source ? source.data.slice(hasHeader ? 1 : 0) : []), [source, hasHeader]);\n\n // ── step 1: a file (or pasted text) ──────────────────────────────────\n function take(name, parsed) {\n if (parsed.errors.length && !parsed.data.length) {\n setError(\"We couldn’t read that file. Save it as CSV (comma separated) and try again.\");\n return;\n }\n if (parsed.data.length === 0 || (parsed.data.length === 1 && parsed.data[0].every((c) => !c.trim()))) {\n setError(\"That file is empty. Check it has a row of column names and at least one row under it.\");\n return;\n }\n if (parsed.data.length - 1 > maxRows) {\n setError(`That file has ${parsed.data.length.toLocaleString()} rows; this import takes up to ${maxRows.toLocaleString()}. Split it and import each part.`);\n return;\n }\n const hs = headersOf(parsed.data, true);\n setError(null);\n setSource({ name, data: parsed.data });\n setHasHeader(true);\n setMapping(intoTable ? autoMatch(hs, fields) : hs.map(() => null));\n setNewCols(intoTable ? [] : guessColumns(hs, parsed.data.slice(1)));\n setStep(\"match\");\n }\n\n // Text handed in (a paste the host already has) skips the upload step.\n const started = useRef(false);\n useEffect(() => {\n if (!initialText || started.current) return;\n started.current = true;\n take(initialName, parseCsvText(initialText));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [initialText]);\n\n async function onFiles([file]) {\n if (!file) return;\n setReading(true);\n setError(null);\n try {\n take(file.name, await readCsvFile(file));\n } catch {\n setError(\"We couldn’t open that file. Try saving it again as CSV.\");\n } finally {\n setReading(false);\n }\n }\n\n function onHeaderToggle(on) {\n setHasHeader(on);\n if (!source) return;\n const hs = headersOf(source.data, on);\n const rest = source.data.slice(on ? 1 : 0);\n if (intoTable) setMapping(on ? autoMatch(hs, fields) : hs.map((_, i) => mapping[i] ?? null));\n else setNewCols(guessColumns(hs, rest));\n }\n\n function downloadTemplate() {\n const blob = new Blob([templateCsv(fields, example)], { type: \"text/csv\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = templateName ?? `${noun}s-template.csv`;\n a.click();\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n }\n\n // ── step 2: which column fills which field ───────────────────────────\n const targetFields = intoTable ? fields.filter((f) => !f.readOnly && !f.computed) : [];\n const used = new Set(mapping.filter(Boolean));\n const missing = intoTable ? targetFields.filter((f) => f.required && !used.has(f.key)) : [];\n const importedCount = intoTable ? mapping.filter(Boolean).length : newCols.filter((c) => c.type).length;\n const namesClash = !intoTable && new Set(newCols.filter((c) => c.type).map((c) => c.name.trim().toLowerCase())).size !== newCols.filter((c) => c.type).length;\n const blankName = !intoTable && newCols.some((c) => c.type && !c.name.trim());\n\n function setTarget(i, key) {\n setMapping((m) => m.map((k, j) => (j === i ? key || null : key && k === key ? null : k)));\n }\n\n function toReview() {\n const targetCols = intoTable ? fields : newCols.filter((c) => c.type).map((c) => ({ ...c, name: c.name.trim() }));\n const map = intoTable ? mapping : newCols.map((c) => (c.type ? c.key : null));\n const built = buildRows(body, map, targetCols, { ctx });\n const shown = intoTable ? built.fields.filter((f) => map.includes(f.key) || f.required) : built.fields;\n setRows(built.rows);\n setCols(shown);\n setOnly(built.problems ? \"problems\" : \"all\");\n setSkipBad(false);\n setResult(null);\n setError(null);\n setStep(\"review\");\n }\n\n // ── step 3: fix, then import ─────────────────────────────────────────\n const bad = useMemo(() => rows.filter((r) => hasProblem(r, cols)), [rows, cols]);\n const toImport = useMemo(() => (skipBad ? rows.filter((r) => !hasProblem(r, cols)) : rows), [rows, cols, skipBad]);\n const blocked = !skipBad && bad.length > 0;\n const showBad = only === \"problems\" && bad.length > 0;\n const shownRows = showBad ? bad : rows;\n\n async function runImport() {\n setBusy(true);\n setError(null);\n let res;\n try {\n res = (await onImport?.({ rows: toImport.map(cleanRow), columns: cols, skipped: rows.length - toImport.length })) ?? { ok: true };\n } catch (e) {\n res = { ok: false, error: e?.message };\n }\n setBusy(false);\n if (!res.ok) {\n setError(res.error ?? \"The import did not go through. Nothing was saved — try again.\");\n return;\n }\n setResult({ imported: res.imported ?? toImport.length, skipped: rows.length - toImport.length });\n setStep(\"done\");\n }\n\n function restart() {\n setStep(\"upload\");\n setSource(null);\n setRows([]);\n setCols([]);\n setPasted(\"\");\n setPasting(false);\n setError(null);\n setResult(null);\n }\n\n const stepIndex = STEPS.indexOf(step === \"upload\" ? \"Upload\" : step === \"match\" ? \"Match columns\" : \"Review\");\n const heading = title ?? `Import ${noun}s from a CSV`;\n\n // ── drawing ──────────────────────────────────────────────────────────\n if (step === \"done\" && result) {\n return (\n <Card className={cx(\"mx-auto w-full max-w-xl\", className)}>\n <div className=\"grid justify-items-center gap-3 py-8 text-center\">\n <span className=\"grid size-12 place-items-center rounded-full bg-success/15 text-success\">\n <Icon name=\"check\" size={24} />\n </span>\n <h3 className=\"text-lg font-semibold\">{plural(result.imported, noun)} imported</h3>\n <p className=\"max-w-sm text-sm text-muted-foreground\">\n {result.skipped ? `${plural(result.skipped, \"row\")} with problems ${result.skipped === 1 ? \"was\" : \"were\"} left out.` : `Every row in ${source?.name ?? \"the file\"} came in.`}\n </p>\n <div className=\"mt-2 flex flex-wrap justify-center gap-2\">\n <Button onClick={restart}>Import another file</Button>\n {onDone ? (\n <Button variant=\"primary\" onClick={onDone}>\n {doneLabel ?? `View ${noun}s`}\n </Button>\n ) : null}\n </div>\n </div>\n </Card>\n );\n }\n\n return (\n <div className={cx(\"grid min-w-0 gap-5\", className)}>\n <div className=\"flex flex-wrap items-start justify-between gap-4\">\n <div className=\"grid gap-1\">\n <h2 className=\"text-lg font-semibold\">{heading}</h2>\n <p className=\"max-w-prose text-sm text-muted-foreground\">\n {description ?? \"Bring rows in from a spreadsheet or another tool. Nothing is saved until you press Import.\"}\n </p>\n </div>\n <Steps steps={STEPS} current={stepIndex} size=\"sm\" label=\"Import progress\" hideLabel className=\"w-full max-w-xs\" />\n </div>\n\n {error ? (\n <Notice tone=\"danger\" title=\"That didn’t work\" onDismiss={() => setError(null)}>\n {error}\n </Notice>\n ) : null}\n\n {step === \"upload\" ? (\n <div className=\"grid gap-3\">\n {reading ? (\n <div className=\"grid gap-3 rounded-lg border border-dashed border-border p-8\" aria-busy=\"true\">\n <p className=\"text-sm text-muted-foreground\">Reading the file…</p>\n <Skeleton className=\"h-4 w-3/4\" />\n <Skeleton className=\"h-4 w-1/2\" />\n <Skeleton className=\"h-4 w-2/3\" />\n </div>\n ) : pasting ? (\n <div className=\"grid gap-3\">\n <Textarea\n rows={8}\n aria-label=\"CSV text\"\n placeholder={intoTable ? `${fields.map((f) => f.name).join(\",\")}\\n…` : \"Name,Email,Amount\\nAda Lovelace,ada@example.com,1200\"}\n value={pasted}\n onChange={(e) => setPasted(e.target.value)}\n className=\"font-mono text-xs\"\n />\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <Button variant=\"quiet\" onClick={() => setPasting(false)}>\n Upload a file instead\n </Button>\n <Button variant=\"primary\" disabled={!pasted.trim()} onClick={() => take(\"Pasted text\", parseCsvText(pasted))}>\n Continue\n </Button>\n </div>\n </div>\n ) : (\n <FileDrop\n accept={ACCEPT}\n onFiles={onFiles}\n onReject={() => setError(\"That is not a CSV file. Export the sheet as CSV (File → Download → CSV) and drop that.\")}\n label=\"Drop a CSV file here, or browse\"\n hint={`CSV or TSV, up to ${maxRows.toLocaleString()} rows. The first row should name the columns.`}\n />\n )}\n {!reading && !pasting ? (\n <div className=\"flex flex-wrap items-center justify-between gap-2 text-sm\">\n <Button variant=\"link\" size=\"sm\" tone=\"muted\" onClick={() => setPasting(true)}>\n Paste the text instead\n </Button>\n <div className=\"flex items-center gap-3\">\n {intoTable ? (\n <Button variant=\"link\" size=\"sm\" tone=\"muted\" icon=\"download\" onClick={downloadTemplate}>\n Download a template\n </Button>\n ) : null}\n {onCancel ? (\n <Button variant=\"quiet\" onClick={onCancel}>\n Cancel\n </Button>\n ) : null}\n </div>\n </div>\n ) : null}\n </div>\n ) : null}\n\n {step === \"match\" && source ? (\n <div className=\"grid gap-4\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <p className=\"flex min-w-0 items-center gap-2 text-sm\">\n <Icon name=\"file\" className=\"shrink-0 text-muted-foreground\" />\n <span className=\"truncate font-medium\">{source.name}</span>\n <span className=\"shrink-0 text-muted-foreground\">· {plural(body.length, \"row\")}</span>\n </p>\n <Switch id=\"csv-has-header\" label=\"First row names the columns\" checked={hasHeader} onChange={(e) => onHeaderToggle(e.target.checked)} />\n </div>\n {missing.length ? (\n <Notice tone=\"warning\" title={missing.length === 1 ? `${missing[0].name} is required` : \"Some fields are required\"}>\n Pick the column that holds {missing.map((f) => f.name).join(\", \")}.\n </Notice>\n ) : null}\n <div className=\"overflow-hidden rounded-lg border border-border\">\n <div className=\"hidden grid-cols-[minmax(0,1fr)_1.5rem_minmax(0,18rem)] gap-3 border-b border-border bg-muted px-4 py-2 text-xs font-medium text-muted-foreground sm:grid\">\n <span>In your file</span>\n <span />\n <span>{intoTable ? \"Goes to\" : \"Becomes the field\"}</span>\n </div>\n <ul className=\"divide-y divide-border\">\n {headers.map((h, i) => {\n const samples = body\n .slice(0, 40)\n .map((r) => (r[i] ?? \"\").trim())\n .filter(Boolean)\n .slice(0, 3);\n const target = intoTable ? mapping[i] : newCols[i]?.type ? newCols[i].key : null;\n return (\n <li key={i} className=\"grid gap-2 px-4 py-3 sm:grid-cols-[minmax(0,1fr)_1.5rem_minmax(0,18rem)] sm:items-center sm:gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium\">{h}</p>\n <p className=\"truncate text-xs text-muted-foreground\">{samples.length ? samples.join(\" · \") : \"Empty in the first rows\"}</p>\n </div>\n <Icon name=\"arrow-right\" className={cx(\"hidden sm:block\", target ? \"text-primary\" : \"text-muted-foreground\")} />\n {intoTable ? (\n <Select\n aria-label={`Field for ${h}`}\n value={mapping[i] ?? \"\"}\n onChange={(e) => setTarget(i, e.target.value)}\n options={[\n { value: \"\", label: \"Don’t import\" },\n ...targetFields.map((f) => ({\n value: f.key,\n label: `${f.name}${f.required ? \" (required)\" : \"\"}${used.has(f.key) && mapping[i] !== f.key ? \" — in use\" : \"\"}`,\n })),\n ]}\n />\n ) : (\n <div className=\"grid grid-cols-[minmax(0,1fr)_minmax(0,9rem)] gap-2\">\n <Input\n aria-label={`Name for ${h}`}\n value={newCols[i]?.name ?? \"\"}\n disabled={!newCols[i]?.type}\n invalid={Boolean(newCols[i]?.type) && !newCols[i]?.name.trim()}\n onChange={(e) => setNewCols((cs) => cs.map((c, j) => (j === i ? { ...c, name: e.target.value } : c)))}\n />\n <Select\n aria-label={`Type for ${h}`}\n value={newCols[i]?.type ?? \"\"}\n onChange={(e) =>\n setNewCols((cs) =>\n cs.map((c, j) => {\n if (j !== i) return c;\n const type = e.target.value;\n if (!type) return { ...c, type: \"\" };\n const guessed = guessColumns([c.name], body.map((r) => [r[i] ?? \"\"]))[0];\n return { ...c, type, options: type === \"select\" || type === \"multi-select\" ? guessed.options ?? c.options ?? [] : c.options };\n }),\n )\n }\n options={[{ value: \"\", label: \"Don’t import\" }, ...TYPE_ORDER.filter((t) => t !== \"person\").map((t) => ({ value: t, label: typeOf({ type: t }).label }))]}\n />\n </div>\n )}\n </li>\n );\n })}\n </ul>\n </div>\n {namesClash ? <p className=\"text-sm text-destructive\">Two fields have the same name. Rename one.</p> : null}\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <Button variant=\"quiet\" icon=\"arrow-left\" onClick={restart}>\n Choose another file\n </Button>\n <div className=\"flex items-center gap-3\">\n <span className=\"text-sm text-muted-foreground max-sm:hidden\">\n {importedCount} of {headers.length} columns\n </span>\n <Button variant=\"primary\" disabled={!importedCount || missing.length > 0 || namesClash || blankName || !body.length} onClick={toReview}>\n {body.length ? `Review ${plural(body.length, \"row\")}` : \"No rows to review\"}\n </Button>\n </div>\n </div>\n </div>\n ) : null}\n\n {step === \"review\" ? (\n <div className=\"grid gap-4\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <p className=\"text-sm\">\n <span className=\"font-medium tabular-nums\">{plural(rows.length - bad.length, noun)} ready</span>\n {bad.length ? <span className=\"text-muted-foreground\"> · {plural(bad.length, \"row\")} to fix</span> : null}\n </p>\n {bad.length ? (\n <ToggleGroup\n size=\"sm\"\n aria-label=\"Show\"\n value={showBad ? \"problems\" : \"all\"}\n onChange={setOnly}\n items={[\n { value: \"problems\", label: `Needs fixing (${bad.length.toLocaleString()})` },\n { value: \"all\", label: \"All rows\" },\n ]}\n />\n ) : null}\n </div>\n {bad.length ? (\n <Notice tone=\"warning\">\n Some cells don’t fit their field — a red corner marks each one. Click it and type the right value, delete the row, or leave those rows out.\n </Notice>\n ) : null}\n <Spreadsheet\n label=\"Rows to import\"\n noun={noun}\n toolbar={false}\n height={Math.min(440, 44 + Math.max(4, shownRows.length + 1) * 40)}\n rows={shownRows}\n columns={cols}\n people={people}\n locale={locale}\n today={today}\n validate={problemOf}\n defaultView={{ frozen: 1 }}\n onChange={(changes) => {\n setRows((rs) => {\n const by = new Map();\n for (const c of changes) by.set(c.rowId, [...(by.get(c.rowId) ?? []), c]);\n return rs.map((r) => {\n const list = by.get(r.id);\n if (!list) return r;\n const next = { ...r, __problems: { ...(r.__problems ?? {}) } };\n for (const c of list) {\n next[c.key] = c.value;\n delete next.__problems[c.key];\n }\n if (!Object.keys(next.__problems).length) delete next.__problems;\n return next;\n });\n });\n return { ok: true };\n }}\n onDeleteRows={(ids) => {\n const gone = new Set(ids);\n setRows((rs) => rs.filter((r) => !gone.has(r.id)));\n return { ok: true };\n }}\n lockFields={intoTable}\n onColumnsChange={(next) => {\n setCols(next);\n return { ok: true };\n }}\n empty={<p className=\"text-sm text-muted-foreground\">No rows left to import. Go back and choose another file.</p>}\n />\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <Button variant=\"quiet\" icon=\"arrow-left\" onClick={() => setStep(\"match\")}>\n Back to matching\n </Button>\n <div className=\"flex flex-wrap items-center gap-3\">\n {bad.length ? <Switch id=\"csv-skip-bad\" label={`Leave out ${plural(bad.length, \"row\")} to fix`} checked={skipBad} onChange={(e) => setSkipBad(e.target.checked)} /> : null}\n <Button variant=\"primary\" loading={busy} disabled={blocked || toImport.length === 0} onClick={runImport}>\n {toImport.length ? `Import ${plural(toImport.length, noun)}` : \"Nothing to import\"}\n </Button>\n </div>\n </div>\n {blocked ? <p className=\"text-right text-xs text-muted-foreground\">Fix the rows marked in red, or leave them out, to import.</p> : null}\n </div>\n ) : null}\n </div>\n );\n}\n\n"
26
+ },
27
+ {
28
+ "path": "registry/blocks/csv-import/import.js",
29
+ "type": "registry:component",
30
+ "target": "@components/makersclaw/blocks/csv-import/import.js",
31
+ "content": "// csv-import's pure helpers: read CSV text (papaparse), name the file's\n// columns, match them to fields, guess types for a new table, and turn\n// the text rows into typed rows with their problems. No React: they run\n// on the server and in a test. The column types — how \"$1,250\" becomes\n// 1250, \"9/23/2026\" a date — are the spreadsheet's registry.\nimport Papa from \"papaparse\";\nimport { guessType, optionValue, parseCell, typeOf } from \"../spreadsheet\";\n\n/**\n * CSV (or TSV, or ; separated) text → { data: string[][], delimiter,\n * errors }. The separator is guessed; blank lines are dropped; a byte-\n * order mark is removed. Quoted cells may hold commas and newlines.\n */\nexport function parseCsvText(text) {\n const clean = String(text ?? \"\").replace(/^/, \"\");\n const res = Papa.parse(clean, { skipEmptyLines: \"greedy\", delimitersToGuess: [\",\", \"\\t\", \";\", \"|\"] });\n const data = res.data.map((row) => row.map((c) => (typeof c === \"string\" ? c : String(c ?? \"\"))));\n // Papa reports a short or long row as an error; those rows are still\n // read (short ones padded later), so only a real failure is kept.\n const errors = res.errors.filter((e) => e.type !== \"FieldMismatch\" && e.code !== \"UndetectableDelimiter\");\n return { data, delimiter: res.meta.delimiter, errors };\n}\n\n/** Read a File as text and parse it. */\nexport async function readCsvFile(file) {\n const text = await file.text();\n return parseCsvText(text);\n}\n\n/** The file's column names: the first row's cells, or \"Column 1\"…;\n * blanks get a name and repeats a number, so every name is unique. */\nexport function headersOf(data, hasHeader = true) {\n const width = Math.max(0, ...data.slice(0, 200).map((r) => r.length));\n const first = hasHeader ? data[0] ?? [] : [];\n const seen = new Map();\n return Array.from({ length: width }, (_, i) => {\n let name = String(first[i] ?? \"\").trim() || `Column ${i + 1}`;\n const n = seen.get(name.toLowerCase()) ?? 0;\n seen.set(name.toLowerCase(), n + 1);\n if (n) name = `${name} ${n + 1}`;\n return name;\n });\n}\n\nconst norm = (s) =>\n String(s ?? \"\")\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[̀-ͯ]/g, \"\")\n .replace(/[^a-z0-9]/g, \"\");\n\n// Names that mean the same field, compared normalised.\nconst SAME = [\n [\"email\", \"emailaddress\", \"mail\", \"workemail\"],\n [\"phone\", \"phonenumber\", \"telephone\", \"tel\", \"mobile\", \"cell\"],\n [\"company\", \"companyname\", \"organization\", \"organisation\", \"account\", \"accountname\", \"business\"],\n [\"name\", \"fullname\", \"contact\", \"contactname\", \"person\"],\n [\"firstname\", \"first\", \"givenname\"],\n [\"lastname\", \"last\", \"surname\", \"familyname\"],\n [\"website\", \"url\", \"site\", \"domain\", \"web\", \"homepage\"],\n [\"amount\", \"value\", \"dealvalue\", \"price\", \"total\"],\n [\"owner\", \"assignee\", \"assignedto\", \"rep\", \"salesrep\"],\n [\"status\", \"stage\", \"state\"],\n [\"date\", \"closedate\", \"duedate\"],\n [\"notes\", \"note\", \"comments\", \"description\"],\n [\"tags\", \"labels\", \"segments\", \"categories\"],\n];\n\nfunction score(header, field) {\n const h = norm(header);\n const n = norm(field.name);\n const k = norm(field.key);\n if (!h) return 0;\n if (h === n || h === k) return 100;\n const group = SAME.find((g) => g.includes(h));\n if (group && (group.includes(n) || group.includes(k))) return 80;\n if (n.length >= 3 && (h.includes(n) || n.includes(h))) return 60;\n if (k.length >= 3 && (h.includes(k) || k.includes(h))) return 50;\n return 0;\n}\n\n/** Each file column → the field it most likely fills (a key, or null to\n * skip). Best matches first; a field is used once. */\nexport function autoMatch(headers, fields) {\n const pairs = [];\n headers.forEach((h, i) =>\n fields.forEach((f) => {\n if (f.readOnly || f.computed) return;\n const s = score(h, f);\n if (s) pairs.push({ i, key: f.key, s });\n }),\n );\n pairs.sort((a, b) => b.s - a.s || a.i - b.i);\n const out = headers.map(() => null);\n const used = new Set();\n for (const p of pairs) {\n if (out[p.i] !== null || used.has(p.key)) continue;\n out[p.i] = p.key;\n used.add(p.key);\n }\n return out;\n}\n\nconst slug = (s) => norm(s).slice(0, 40) || \"field\";\n\n/** A new table from the file: one field per column, its type guessed\n * from up to 300 values (select options from the distinct values). */\nexport function guessColumns(headers, body) {\n const taken = new Set();\n return headers.map((name, i) => {\n let key = slug(name);\n for (let n = 2; taken.has(key); n++) key = `${slug(name)}${n}`;\n taken.add(key);\n const sample = body.slice(0, 300).map((r) => r[i] ?? \"\");\n const type = guessType(sample);\n const col = { key, name, type };\n if (type === \"select\" || type === \"multi-select\") {\n const labels = [];\n const seen = new Set();\n for (const v of body.map((r) => r[i] ?? \"\")) {\n const parts = type === \"multi-select\" ? String(v).split(/\\s*[,;]\\s*/) : [String(v).trim()];\n for (const p of parts) {\n const t = p.trim();\n if (t && !seen.has(t.toLowerCase())) {\n seen.add(t.toLowerCase());\n labels.push(t);\n }\n }\n if (labels.length > 40) break;\n }\n const values = [];\n col.options = labels.map((label, j) => {\n const value = optionValue(label, values);\n values.push(value);\n return { value, label, hue: (j % 5) + 1 };\n });\n }\n return col;\n });\n}\n\n/**\n * The text rows → typed rows. `mapping[i]` is the field key file column\n * i fills (null: skipped). Each row gets an `id` and `__problems`\n * ({ key: message } for text that did not fit its field — the text is\n * kept so it can be fixed) — strip it before saving (`cleanRow`). A select\n * value that is no option yet becomes a new option (`creates`: key →\n * options) unless the field says `allowNew: false`.\n */\nexport function buildRows(body, mapping, fields, { ctx, makeId } = {}) {\n const byKey = new Map(fields.map((f) => [f.key, { ...f, options: [...(f.options ?? [])] }]));\n const creates = new Map();\n let problems = 0;\n const rows = body.map((line, n) => {\n const row = { id: makeId ? makeId(n) : `import-${n + 1}` };\n const bad = {};\n mapping.forEach((key, i) => {\n if (!key) return;\n const field = byKey.get(key);\n if (!field) return;\n const text = line[i] ?? \"\";\n const res = parseCell(text, field, ctx);\n if (\"error\" in res) {\n bad[key] = res.error;\n row[key] = text;\n return;\n }\n if (res.create) {\n field.options.push(...res.create);\n creates.set(key, [...(creates.get(key) ?? []), ...res.create]);\n }\n row[key] = res.value;\n });\n for (const f of fields) {\n if (f.required && !(f.key in bad) && typeOf(f).empty(row[f.key])) bad[f.key] = `${f.name} is required`;\n }\n if (Object.keys(bad).length) {\n row.__problems = bad;\n problems++;\n }\n return row;\n });\n return { rows, problems, creates, fields: [...byKey.values()] };\n}\n\n/** A row without the importer's own keys. */\nexport function cleanRow(row) {\n const { __problems, ...rest } = row;\n return rest;\n}\n\n/** A CSV of the fields' names and one example line: the \"template\" a\n * person fills in before importing. */\nexport function templateCsv(fields, example) {\n const quote = (v) => (/[\",\\n]/.test(v) ? `\"${String(v).replace(/\"/g, '\"\"')}\"` : String(v));\n const head = fields.filter((f) => !f.readOnly && !f.computed);\n const lines = [head.map((f) => quote(f.name)).join(\",\")];\n if (example) lines.push(head.map((f) => quote(example[f.key] ?? \"\")).join(\",\"));\n return `${lines.join(\"\\n\")}\\n`;\n}\n"
32
+ },
33
+ {
34
+ "path": "registry/blocks/csv-import/README.md",
35
+ "type": "registry:file",
36
+ "target": "@components/makersclaw/blocks/csv-import/README.md",
37
+ "content": "# CsvImport\n\nBring rows in from a CSV in three steps. **Upload**: drop a file or browse\n(or paste the text, or download a template of your fields to fill in).\n**Match columns**: each column in the file, with a few of its values,\nbeside the field it goes to — matched for you by name and common synonyms\n(\"E-mail address\" → Email, \"Organization\" → Company), changeable, or\n\"Don't import\"; required fields that are still unmatched are named.\n**Review**: every row in an editable spreadsheet. A cell that does not fit\nits field (\"last spring\" in a date, \"abc\" in an amount, a missing required\nvalue) keeps its text and gets a red corner; click it and type the right\nvalue, delete the row, or leave the rows with problems out. Then one\n**Import** — nothing is saved before it.\n\nWith no `fields`, it makes a new table instead: each column becomes a\nfield whose name you can change and whose type is guessed from its values\n(number, currency, date, checkbox, link, single or multiple select, text).\n\nThe CSV is read by [papaparse](https://www.papaparse.com) (MIT): commas,\ntabs, semicolons or pipes (guessed), quoted cells with commas and\nnewlines, a byte-order mark. Typed values come from the spreadsheet\nblock's column types, so \"$1,250\" is 1250 and \"11/02/2024\" is a day.\n\n```jsx\nimport { CsvImport } from \"../components/blocks/csv-import\"; // a Makersclaw app (\"./blocks/csv-import\" from components/)\nimport { CsvImport } from \"@/components/makersclaw/blocks/csv-import\"; // a shadcn project\n```\n\nIt is a client component. `makersclaw add csv-import` adds the\nspreadsheet block (it requires it) and `papaparse` (pinned, exact): run\n`npm install` after.\n\n## The data\n\n`fields` are the spreadsheet's columns — the same shape, the same types\n(its README, \"The data\"):\n\n```js\nconst FIELDS = [\n { key: \"name\", name: \"Name\", type: \"text\", required: true },\n { key: \"email\", name: \"Email\", type: \"text\", required: true },\n { key: \"stage\", name: \"Stage\", type: \"select\", options: [{ value: \"lead\", label: \"Lead\", hue: 2 }] },\n { key: \"mrr\", name: \"MRR\", type: \"currency\", precision: 0 },\n { key: \"owner\", name: \"Owner\", type: \"person\" }, // matched by name, first name, email or id in `people`\n { key: \"since\", name: \"Customer since\", type: \"date\" },\n];\n```\n\n`onImport` receives `{ rows, columns, skipped }`:\n\n- `rows` — typed values keyed by field (`{ id, name: \"Ada\", mrr: 1250,\n since: \"2025-03-14\", … }`). The `id` is a temporary one (\"import-3\");\n drop it and let the database make its own.\n- `columns` — the fields the rows use. Into existing fields, a select\n value that was not an option yet (\"Trial\") arrives as a new option on\n its field (`hue` set): save the options with the rows. As a new table,\n these are the new table's fields.\n- `skipped` — how many rows with problems were left out.\n\n## Props\n\n- `onImport({ rows, columns, skipped })` — required; async, answers\n `{ ok: true }` (optionally `imported`: the count you actually saved) or\n `{ ok: false, error }`. A failure keeps everything as it was and shows\n the error; the person can try again.\n- `fields` — the table to import into. Leave it out to make a new table.\n Read-only and computed fields are not offered.\n- `people` — `[{ id, name, email? }]`, for `person` fields.\n- `noun` — what a row is (`\"contact\"`): \"Import 8 contacts\". Default\n `\"row\"`.\n- `title` — the heading (default \"Import contacts from a CSV\");\n `description` — the line under it.\n- `maxRows` — the most rows one file may hold (`50000`).\n- `example` — `{ key: value }` for the template's example line (with\n `fields`, \"Download a template\" writes the field names and this line).\n `templateName` — the template's file name (`\"contacts-template.csv\"`).\n- `initialText` — CSV text to start from (skips the upload step);\n `initialName` — what to call it (`\"Pasted text\"`).\n- `onCancel` — shows Cancel on the first step.\n- `onDone` — the success screen's main button (`doneLabel`, default\n \"View contacts\"); without it only \"Import another file\" shows.\n- `locale` — for amounts and dates; `today` — `\"YYYY-MM-DD\"` for the date\n picker.\n\nThe helpers, for a flow of your own: `parseCsvText(text)` → `{ data,\ndelimiter, errors }` · `readCsvFile(file)` · `headersOf(data, hasHeader)` ·\n`autoMatch(headers, fields)` → a field key (or null) per column ·\n`guessColumns(headers, body)` → new fields with types and options ·\n`buildRows(body, mapping, fields, { ctx })` → `{ rows, problems,\ncreates, fields }` (each row's `__problems` is `{ key: message }`) ·\n`cleanRow(row)` · `templateCsv(fields, example)`.\n\n## Example\n\n```jsx\n\"use client\";\nimport { useRouter } from \"next/navigation\";\nimport { insertRow } from \"../lib/actions\";\nimport { CsvImport } from \"./blocks/csv-import\";\n\nexport default function ImportContacts({ fields, team }) {\n const router = useRouter();\n return (\n <CsvImport\n fields={fields}\n people={team}\n noun=\"contact\"\n onImport={async ({ rows }) => {\n let imported = 0;\n for (const { id, ...values } of rows) {\n const r = await insertRow({ source: \"public.contacts\", values });\n if (!r.ok) return { ok: false, error: `Stopped after ${imported} contacts: ${r.error}` };\n imported++;\n }\n return { ok: true, imported };\n }}\n onDone={() => router.push(\"/contacts\")}\n />\n );\n}\n```\n\n## What it will not do\n\n- Excel files (.xlsx) are not read: save the sheet as CSV first (the\n upload step says so when one is dropped).\n- No merging with rows already in the table (update by email, skip\n duplicates): every imported row is new. Match on your side in\n `onImport` if you need it.\n- One file at a time; nothing is uploaded or kept — the rows go to\n `onImport` and nowhere else.\n- The review can fix cells and delete rows, not add rows or change the\n target fields (as a new table, it can rename and retype them).\n"
38
+ }
39
+ ],
40
+ "categories": [
41
+ "csv",
42
+ "import",
43
+ "upload",
44
+ "tsv",
45
+ "excel",
46
+ "spreadsheet",
47
+ "map columns",
48
+ "field mapping",
49
+ "validate",
50
+ "bulk",
51
+ "papaparse",
52
+ "migration",
53
+ "onboarding",
54
+ "template"
55
+ ],
56
+ "docs": "Installed to components/makersclaw/blocks/csv-import. Import: import { CsvImport, parseCsvText, readCsvFile, headersOf, autoMatch, guessColumns, buildRows, cleanRow, templateCsv } from \"@/components/makersclaw/blocks/csv-import\". Props and an example: components/makersclaw/blocks/csv-import/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
57
+ "meta": {
58
+ "makersclaw": {
59
+ "version": "0.3.0",
60
+ "useWhen": "People bring records in from a spreadsheet or another tool: contacts, leads, products, a price list, an old CRM export, a first table for a new app. Built on spreadsheet. Not for uploading files to keep (file-upload) or for editing records already in the app (spreadsheet, data-table)."
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,69 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "data-table",
4
+ "type": "registry:block",
5
+ "title": "Data table",
6
+ "description": "Records as a table the founder can work: sort by a column (shift for several), search every column, faceted filters with counts, column visibility, row selection with a bulk-actions bar, a menu per row or its first actions as buttons in the row (async, with a spinner and a failure notice), row click, pagination with a page size, loading, empty and no-matches states, and cards on a phone. Opt in to expandable rows, row groups with subtotals, an aligned totals row, arrow-key navigation and a sticky first column. Client-side over rows, or driven by a server.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/core.json"
9
+ ],
10
+ "dependencies": [],
11
+ "files": [
12
+ {
13
+ "path": "registry/blocks/data-table/index.js",
14
+ "type": "registry:component",
15
+ "target": "@components/makersclaw/blocks/data-table/index.js",
16
+ "content": "// The DataTable block — `makersclaw add data-table`. Fixed: compose it\n// through its props (README.md beside this file); a table it cannot be is\n// your own component in components/, built from the kit's Table parts.\nexport { DataTable } from \"./data-table\";\nexport { useControllable, processRows, sortRows, searchRows, filterRows, facetOptions, nextSort, groupRows, groupValueOf } from \"./model\";\n"
17
+ },
18
+ {
19
+ "path": "registry/blocks/data-table/data-table.jsx",
20
+ "type": "registry:component",
21
+ "target": "@components/makersclaw/blocks/data-table/data-table.jsx",
22
+ "content": "\"use client\";\n\nimport { isValidElement, useEffect, useId, useMemo, useState } from \"react\";\nimport { isSample } from \"../../parts\";\nimport {\n Button,\n Checkbox,\n Combobox,\n Empty,\n Icon,\n IconButton,\n InputGroup,\n Menu,\n Notice,\n Pagination,\n Skeleton,\n Table,\n TableBody,\n TableCell,\n TableFooter,\n TableHead,\n TableHeader,\n TableRow,\n Tooltip,\n cx,\n} from \"../../ui\";\nimport { facetOptions, groupKeyOf, groupRows, groupValueOf, nextSort, processRows, useControllable } from \"./model\";\n\n// DataTable — records as a table the founder can work: sort by a column\n// (shift for more than one), search every column, narrow by facets with\n// counts, pick which columns show, select rows and act on them together,\n// act on one from its menu, open one with a click, move a page at a\n// time. Client-side over `rows` by default; with `manual` the rows are\n// already the page a server chose, and the table only reports what the\n// founder asked for. Every piece of state (search, filters, sort,\n// selection, page, page size, hidden columns) is controlled when its\n// value prop is given and the table's own otherwise.\n//\n// The block owns the behaviour a hand-drawn table gets wrong: a sort that\n// keeps blanks last, a select-all that means this page, a page that goes\n// back to 1 when the filter changes, \"no matches\" told apart from \"no\n// rows\", a bulk action that can fail and says so, examples that are\n// never acted on, and a 390px window where the table scrolls inside\n// itself (or turns into cards). The surface owns what a cell says\n// (`cell`), what a row can do (`rowActions`, `bulkActions`) and how a row\n// is painted (`rowTone`, `rowClassName`).\n//\n// <DataTable rows={invoices} columns={COLUMNS}\n// facets={[{ key: \"status\", label: \"Status\" }]}\n// bulkActions={[{ label: \"Mark paid\", onSelect: markPaid }]}\n// rowActions={(r) => [{ label: \"Open\", onSelect: () => open(r) }]}\n// onRowClick={open} />\n//\n// Opt-in on top: a row's first actions as buttons in the row\n// (`inlineActions`), rows that open under themselves (`renderExpanded`), rows\n// filed under group headings with a subtotal each (`groupBy`,\n// `groupSummary`, `groupFooter`), an aligned totals row (`totals`), arrow\n// keys over the rows (`keyboard`) and a first column that stays put while\n// a wide table scrolls sideways (`stickyFirstColumn`).\n//\n// README.md beside this file lists every prop.\n\nconst TONES = {\n success: \"bg-success/10 [tr:hover>&]:bg-success/15\",\n info: \"bg-info/10 [tr:hover>&]:bg-info/15\",\n warning: \"bg-warning/10 [tr:hover>&]:bg-warning/15\",\n danger: \"bg-destructive/10 [tr:hover>&]:bg-destructive/15\",\n highlighted: \"bg-primary/5 [tr:hover>&]:bg-primary/10\",\n};\nconst CARD_TONES = {\n success: \"border-success/40 bg-success/10\",\n info: \"border-info/40 bg-info/10\",\n warning: \"border-warning/50 bg-warning/10\",\n danger: \"border-destructive/50 bg-destructive/10\",\n highlighted: \"border-primary/40 bg-primary/5\",\n};\n// A highlighted row (a pinned or best row, not a status) carries a bar\n// at its leading edge as well as the tint.\nconst BAR = \"shadow-[inset_3px_0_0_var(--color-primary)]\";\n// A sticky cell is opaque (the columns scroll under it), so a tone is a\n// flat gradient layered over the card instead of a see-through fill.\nconst STICKY_TONES = {\n success: \"bg-linear-to-r from-success/10 to-success/10 [tr:hover>&]:from-success/15 [tr:hover>&]:to-success/15\",\n info: \"bg-linear-to-r from-info/10 to-info/10 [tr:hover>&]:from-info/15 [tr:hover>&]:to-info/15\",\n warning: \"bg-linear-to-r from-warning/10 to-warning/10 [tr:hover>&]:from-warning/15 [tr:hover>&]:to-warning/15\",\n danger: \"bg-linear-to-r from-destructive/10 to-destructive/10 [tr:hover>&]:from-destructive/15 [tr:hover>&]:to-destructive/15\",\n highlighted: \"bg-linear-to-r from-primary/5 to-primary/5 [tr:hover>&]:from-primary/10 [tr:hover>&]:to-primary/10\",\n};\nconst STICKY_HOVER = \"[tr:hover>&]:bg-linear-to-r [tr:hover>&]:from-muted/50 [tr:hover>&]:to-muted/50\";\nconst STICKY_EDGE = \"shadow-[inset_-1px_0_0_var(--color-border)]\";\nconst BAR_EDGE = \"shadow-[inset_3px_0_0_var(--color-primary),inset_-1px_0_0_var(--color-border)]\";\n// The leading columns' widths, so a sticky first column knows its offset.\nconst EXPANDER_W = 2.5; // rem — w-10\nconst SELECT_W = 3; // rem — w-12\nconst INTERACTIVE = \"button, a, input, select, textarea, label, [role=menuitem], [role=menu], [popover]\";\nconst NO_FILTERS = {};\nconst NONE = [];\n\nfunction humanize(key) {\n const text = String(key ?? \"\").replace(/[_-]+/g, \" \").trim();\n return text ? text.charAt(0).toUpperCase() + text.slice(1) : \"\";\n}\n\nfunction plural(n, one, many = `${one}s`) {\n return `${n.toLocaleString()} ${n === 1 ? one : many}`;\n}\n\n// A per-column map ({ amount: \"$4,200\" }) rather than one node.\nconst isColumnMap = (v) => Boolean(v) && typeof v === \"object\" && !Array.isArray(v) && !isValidElement(v);\nconst domId = (s) => String(s).replace(/[^\\w-]/g, \"_\");\n\nexport function DataTable({\n rows,\n columns,\n idColumn = \"id\",\n label = \"Table\",\n // search\n searchable = true,\n searchPlaceholder = \"Search…\",\n search,\n defaultSearch = \"\",\n onSearchChange,\n // facets\n facets,\n filters,\n defaultFilters = NO_FILTERS,\n onFiltersChange,\n // sort\n sort,\n defaultSort = NONE,\n onSortChange,\n multiSort = true,\n // selection\n selectable,\n selection,\n defaultSelection = NONE,\n onSelectionChange,\n bulkActions,\n // columns\n hiddenColumns,\n defaultHiddenColumns,\n onHiddenColumnsChange,\n // pagination\n page,\n defaultPage = 1,\n onPageChange,\n pageSize,\n defaultPageSize = 10,\n onPageSizeChange,\n pageSizes = [10, 25, 50, 100],\n paginate = true,\n // server mode\n manual = false,\n rowCount,\n // rows\n rowActions,\n inlineActions,\n onRowClick,\n rowTone,\n rowClassName,\n // expandable rows\n renderExpanded,\n canExpand,\n expanded,\n defaultExpanded = NONE,\n onExpandedChange,\n // groups\n groupBy,\n groupOrder,\n groupLabel,\n groupSummary,\n groupFooter,\n // totals\n totals,\n totalsLabel = \"Total\",\n // keyboard and layout\n keyboard = false,\n stickyFirstColumn = false,\n // slots and states\n toolbar,\n footer,\n loading = false,\n error,\n emptyTitle = \"Nothing here yet\",\n emptyBody,\n emptyAction,\n emptyIcon = \"inbox\",\n noun = \"row\",\n mobile = \"scroll\",\n cardTitle,\n variant = \"outline\",\n className,\n}) {\n const uid = useId().replace(/[^\\w-]/g, \"\");\n const all = rows ?? NONE;\n const cols = useMemo(() => (columns ?? []).map((c) => ({ ...c, key: String(c.key), header: c.header ?? humanize(c.key) })), [columns]);\n const facetList = useMemo(\n () =>\n (facets ?? []).map((f) => {\n const col = cols.find((c) => c.key === String(f.key));\n return { ...f, key: String(f.key), label: f.label ?? (typeof col?.header === \"string\" ? col.header : humanize(f.key)) };\n }),\n [facets, cols],\n );\n const keyOf = (row) => String(row?.[idColumn]);\n\n const [query, setQuery] = useControllable(search, defaultSearch, onSearchChange);\n const [chosen, setChosen] = useControllable(filters, defaultFilters, onFiltersChange);\n const [order, setOrder] = useControllable(sort, defaultSort, onSortChange);\n const [picked, setPicked] = useControllable(selection, defaultSelection, onSelectionChange);\n const [hidden, setHidden] = useControllable(\n hiddenColumns,\n defaultHiddenColumns ?? cols.filter((c) => c.hidden).map((c) => c.key),\n onHiddenColumnsChange,\n );\n const [current, setPage] = useControllable(page, defaultPage, onPageChange);\n const [size, setSize] = useControllable(pageSize, defaultPageSize, onPageSizeChange);\n const [bulkError, setBulkError] = useState(null);\n const [bulkNote, setBulkNote] = useState(null);\n const [running, setRunning] = useState(null);\n const [announce, setAnnounce] = useState(\"\");\n const [openIds, setOpenIds] = useControllable(expanded, defaultExpanded, onExpandedChange);\n const [activeId, setActiveId] = useState(null);\n const [rowBusy, setRowBusy] = useState(null);\n const [rowError, setRowError] = useState(null);\n\n const canSelect = selectable ?? Boolean(bulkActions || onSelectionChange || selection);\n const hiddenSet = useMemo(() => new Set((hidden ?? NONE).map(String)), [hidden]);\n const shown = cols.filter((c) => !hiddenSet.has(c.key));\n const pickedSet = useMemo(() => new Set((picked ?? NONE).map(String)), [picked]);\n const filterState = chosen ?? NO_FILTERS;\n const activeFilters = facetList.filter((f) => (filterState[f.key] ?? []).length > 0).length;\n const narrowed = Boolean(String(query ?? \"\").trim()) || activeFilters > 0;\n\n // Client mode: search, facets, sort, then a page. Manual: the rows are\n // the page already, and `rowCount` is how many the server has.\n const matched = useMemo(\n () => (manual ? all : processRows(all, { query, filters: filterState, facets: facetList, sort: order, columns: cols })),\n [manual, all, query, filterState, facetList, order, cols],\n );\n const total = manual ? rowCount ?? all.length : matched.length;\n const per = paginate ? Math.max(1, Number(size) || 10) : Math.max(1, total);\n const pageCount = Math.max(1, Math.ceil(total / per));\n const pageNow = Math.min(Math.max(1, Number(current) || 1), pageCount);\n // Groups: sort first, then file each row under its group, so a sort\n // orders rows within their groups. Groups keep their place (`groupOrder`,\n // else the order they first appear in `rows`) whatever the sort.\n const grouped = groupBy !== undefined && groupBy !== null && groupBy !== false;\n const groups = useMemo(() => {\n if (!grouped) return null;\n const seen = all.map((r) => groupKeyOf(groupValueOf(groupBy, r)));\n const order = typeof groupOrder === \"function\" ? groupOrder : [...(Array.isArray(groupOrder) ? groupOrder : NONE), ...seen];\n return groupRows(matched, groupBy, order);\n }, [grouped, groupBy, groupOrder, matched, all]);\n const arranged = useMemo(() => (groups ? groups.flatMap((g) => g.rows) : matched), [groups, matched]);\n const visible = manual || !paginate ? arranged : arranged.slice((pageNow - 1) * per, pageNow * per);\n\n // A page past the end (the filter narrowed the rows) goes back to the last.\n useEffect(() => {\n if (!loading && Number(current) > pageCount) setPage(pageCount);\n }, [current, pageCount, loading]); // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n if (!loading && narrowed) setAnnounce(total === 0 ? \"No matches\" : `${plural(total, noun)} match`);\n }, [total, narrowed, loading, noun]);\n\n // ---- changes that move back to page 1 ---------------------------------\n\n function changeQuery(v) {\n setQuery(v);\n if (pageNow !== 1) setPage(1);\n }\n function changeFacet(key, values) {\n setChosen((f) => {\n const next = { ...(f ?? {}) };\n if (values.length) next[key] = values;\n else delete next[key];\n return next;\n });\n if (pageNow !== 1) setPage(1);\n }\n function clearFilters() {\n setQuery(\"\");\n setChosen({});\n if (pageNow !== 1) setPage(1);\n }\n function pressSort(e, key) {\n setOrder((s) => nextSort(s, key, multiSort && e?.shiftKey));\n if (pageNow !== 1) setPage(1);\n }\n\n // ---- selection ---------------------------------------------------------\n\n const pageIds = visible.map(keyOf);\n const pagePicked = pageIds.filter((id) => pickedSet.has(id)).length;\n const allOnPage = pageIds.length > 0 && pagePicked === pageIds.length;\n const someOnPage = pagePicked > 0 && !allOnPage;\n\n function toggleRow(row) {\n const id = keyOf(row);\n setPicked((p) => {\n const list = (p ?? []).map(String);\n return list.includes(id) ? list.filter((x) => x !== id) : [...list, id];\n });\n }\n function togglePage() {\n setPicked((p) => {\n const list = (p ?? []).map(String);\n if (allOnPage) return list.filter((id) => !pageIds.includes(id));\n return [...list, ...pageIds.filter((id) => !list.includes(id))];\n });\n }\n function selectAllMatches() {\n setPicked((p) => [...new Set([...(p ?? []).map(String), ...matched.map(keyOf)])]);\n }\n function clearSelection() {\n setPicked([]);\n setBulkNote(null);\n }\n\n // ---- expandable rows ---------------------------------------------------\n\n const expanding = typeof renderExpanded === \"function\";\n const openSet = useMemo(() => new Set((openIds ?? NONE).map(String)), [openIds]);\n const expandable = (row) => expanding && (canExpand ? Boolean(canExpand(row)) : true);\n function toggleOpen(id, force) {\n setOpenIds((o) => {\n const list = (o ?? []).map(String);\n const on = force ?? !list.includes(id);\n if (on === list.includes(id)) return list;\n return on ? [...list, id] : list.filter((x) => x !== id);\n });\n }\n\n const selectedRows = useMemo(() => all.filter((r) => pickedSet.has(keyOf(r))), [all, pickedSet]); // eslint-disable-line react-hooks/exhaustive-deps\n const count = manual ? pickedSet.size : selectedRows.length;\n const bulkItems = (typeof bulkActions === \"function\" ? bulkActions(selectedRows) : bulkActions ?? NONE).filter(Boolean);\n\n async function runBulk(action) {\n // Examples are shown, never acted on: they are not rows.\n const real = selectedRows.filter((r) => !isSample(r));\n setBulkError(null);\n setBulkNote(null);\n if (real.length === 0 && selectedRows.length > 0) {\n setBulkNote(\"These are examples. Nothing was changed.\");\n return;\n }\n setRunning(action.label);\n let result;\n try {\n result = await action.onSelect?.(real, manual ? [...pickedSet] : real.map(keyOf));\n } catch (e) {\n result = { ok: false, error: e?.message ?? String(e) };\n }\n setRunning(null);\n if (result && result.ok === false) {\n setBulkError(`${action.label} failed: ${result.error ?? \"nothing was changed\"}`);\n return;\n }\n if (action.keepSelection !== true) setPicked([]);\n setAnnounce(`${action.label}: done for ${plural(real.length, noun)}`);\n }\n\n // ---- drawing -----------------------------------------------------------\n\n const toneOf = (row) => (rowTone ? rowTone(row) : null);\n const hasActions = typeof rowActions === \"function\";\n // Inline buttons widen the actions column past the menu's w-12.\n const inlining = hasActions && Boolean(inlineActions);\n // A click opens the row (`onRowClick`); with no onRowClick, a row that\n // expands opens its details instead.\n const activate = typeof onRowClick === \"function\" ? onRowClick : expanding ? (row) => expandable(row) && toggleOpen(keyOf(row)) : null;\n const clickable = Boolean(activate);\n const lead = (expanding ? 1 : 0) + (canSelect ? 1 : 0);\n const width = lead + shown.length + (hasActions ? 1 : 0);\n const titleOf = (row) => {\n if (cardTitle) return cardTitle(row);\n const first = shown[0];\n return first ? (first.cell ? first.cell(row) : row?.[first.key]) : keyOf(row);\n };\n const nameOf = (row) => {\n const first = shown[0];\n const v = first ? row?.[first.key] : null;\n return typeof v === \"string\" || typeof v === \"number\" ? String(v) : keyOf(row);\n };\n\n function onRowActivate(e, row) {\n if (!clickable) return;\n if (e.target.closest?.(INTERACTIVE)) return;\n if (e.type === \"keydown\") {\n if (e.key !== \"Enter\" && e.key !== \" \") return;\n if (e.target !== e.currentTarget) return;\n // With the keyboard grid, Space selects (the grid's handler does it).\n if (e.key === \" \" && keyboard && canSelect) return;\n e.preventDefault();\n }\n activate(row);\n }\n\n // ---- keyboard grid -------------------------------------------------------\n // One row is in the tab order at a time (roving tabindex): the last one\n // focused, else the first. Arrows, Home and End move it; Space selects;\n // Enter opens (as a click does); → and ← open and close a row's details;\n // Esc clears the selection.\n\n const byId = new Map(visible.map((r) => [keyOf(r), r]));\n const tabStop = keyboard ? (activeId !== null && byId.has(activeId) ? activeId : visible.length ? keyOf(visible[0]) : null) : null;\n\n function onGridKey(e) {\n if (!keyboard) return;\n if (e.target.closest?.(\"[role=menu], [popover], input:not([type=checkbox]), textarea, select\")) return;\n if (e.key === \"Escape\") {\n if (canSelect && count > 0) {\n e.preventDefault();\n clearSelection();\n setAnnounce(\"Selection cleared\");\n }\n return;\n }\n const el = e.target.closest?.(\"[data-row]\");\n if (!el || el !== e.target) return;\n const list = [...e.currentTarget.querySelectorAll(\"[data-row]\")];\n const at = list.indexOf(el);\n const row = byId.get(el.dataset.row);\n let to = null;\n if (e.key === \"ArrowDown\") to = at + 1;\n else if (e.key === \"ArrowUp\") to = at - 1;\n else if (e.key === \"Home\") to = 0;\n else if (e.key === \"End\") to = list.length - 1;\n else if (e.key === \" \" && canSelect && row) {\n e.preventDefault();\n const on = !pickedSet.has(el.dataset.row);\n toggleRow(row);\n setAnnounce(`${nameOf(row)} ${on ? \"selected\" : \"deselected\"}`);\n return;\n } else if ((e.key === \"ArrowRight\" || e.key === \"ArrowLeft\") && row && expandable(row)) {\n e.preventDefault();\n toggleOpen(el.dataset.row, e.key === \"ArrowRight\");\n return;\n }\n if (to === null) return;\n e.preventDefault();\n const next = list[Math.max(0, Math.min(list.length - 1, to))];\n if (next) {\n next.focus();\n setActiveId(next.dataset.row);\n }\n }\n\n // What a row (or card) needs to be clicked, focused and moved through.\n function rowProps(row, id) {\n return {\n tabIndex: keyboard ? (id === tabStop ? 0 : -1) : clickable ? 0 : undefined,\n onClick: clickable ? (e) => onRowActivate(e, row) : undefined,\n onKeyDown: clickable ? (e) => onRowActivate(e, row) : undefined,\n onFocus: keyboard ? (e) => e.target === e.currentTarget && setActiveId(id) : undefined,\n };\n }\n // The ring is a layer over the whole row, above sticky cells (an outline\n // on the row would be painted under them).\n const focusRing =\n \"relative outline-hidden focus-visible:after:pointer-events-none focus-visible:after:absolute focus-visible:after:inset-0 focus-visible:after:z-[3] focus-visible:after:ring-2 focus-visible:after:ring-inset focus-visible:after:ring-ring\";\n\n // ---- sticky first column -------------------------------------------------\n\n const stickyBg = variant === \"plain\" ? \"bg-background\" : \"bg-card\";\n const offsets = { expander: 0, select: expanding ? EXPANDER_W : 0, first: (expanding ? EXPANDER_W : 0) + (canSelect ? SELECT_W : 0) };\n // Classes and style for a cell in a leading slot (\"expander\" | \"select\" |\n // \"first\"). `layer` is what the cell paints: \"head\", \"foot\", \"body\".\n function stick(slot, layer, { tone, selected, bar } = {}) {\n if (!stickyFirstColumn) return { className: undefined, style: undefined };\n const edge = slot === \"first\";\n const paint =\n layer === \"head\"\n ? stickyBg\n : layer === \"foot\"\n ? cx(stickyBg, \"bg-linear-to-r from-muted/50 to-muted/50\")\n : layer === \"sub\"\n ? \"bg-muted\"\n : cx(selected ? \"bg-muted\" : stickyBg, tone && STICKY_TONES[tone] ? STICKY_TONES[tone] : !selected && STICKY_HOVER);\n return {\n className: cx(\"sticky\", layer === \"head\" ? \"z-[2]\" : \"z-[1]\", paint, edge && (bar ? BAR_EDGE : STICKY_EDGE)),\n style: { left: `${offsets[slot]}rem` },\n };\n }\n\n function checkbox(row) {\n return (\n <Checkbox\n checked={pickedSet.has(keyOf(row))}\n onChange={() => toggleRow(row)}\n aria-label={`Select ${nameOf(row)}`}\n className=\"px-1\"\n />\n );\n }\n\n // A row's actions, split: the ones drawn as buttons in the row\n // (`inlineActions` of them from the front, or any with `inline: true`)\n // and the rest, which stay in the \"…\" menu. Separators and labels stay\n // in the menu, tidied so none leads, trails or doubles.\n function splitActions(row) {\n const items = (hasActions ? rowActions(row) ?? NONE : NONE).filter(Boolean);\n let left = inlineActions === true ? Infinity : Math.max(0, Number(inlineActions) || 0);\n const inline = [];\n const menu = [];\n for (const a of items) {\n const plain = a.type === undefined || a.type === \"item\";\n const pick = plain && a.inline !== false && (a.inline === true || left > 0);\n if (pick) {\n inline.push(a);\n if (a.inline !== true) left--;\n } else menu.push(a);\n }\n const tidy = menu.filter((a, i, all) => {\n if (a.type !== \"separator\") return true;\n const prev = all.slice(0, i).filter((x) => x.type !== \"separator\").length;\n const next = all[i + 1];\n return prev > 0 && next && next.type !== \"separator\";\n });\n return { inline, menu: inline.length ? tidy : items };\n }\n\n // An inline action may be async: its button spins, the row's other\n // buttons wait, and { ok: false, error } (or a throw) is said above the\n // table. The menu's items keep their own behaviour.\n async function runRowAction(row, action) {\n const id = keyOf(row);\n setRowError(null);\n setRowBusy({ id, label: action.label });\n let result;\n try {\n result = await action.onSelect?.(row);\n } catch (e) {\n result = { ok: false, error: e?.message ?? String(e) };\n }\n setRowBusy(null);\n if (result && result.ok === false) {\n setRowError(`${action.label} failed for ${nameOf(row)}: ${result.error ?? \"nothing was changed\"}`);\n return;\n }\n setAnnounce(`${action.label}: done for ${nameOf(row)}`);\n }\n\n function inlineButton(row, a, card) {\n const id = keyOf(row);\n const busy = rowBusy?.id === id;\n const props = {\n variant: a.tone === \"danger\" ? \"danger\" : \"secondary\",\n loading: busy && rowBusy.label === a.label,\n disabled: (busy && rowBusy.label !== a.label) || a.disabled,\n onClick: () => runRowAction(row, a),\n };\n // On a card there is room for the words; in a row, the glyph and a tip.\n if (card || !a.icon) {\n return (\n <Button key={a.label} {...props} size=\"md\" icon={a.icon}>\n {a.label}\n </Button>\n );\n }\n return (\n <Tooltip key={a.label} label={a.label}>\n <IconButton {...props} icon={a.icon} label={`${a.label}: ${nameOf(row)}`} size=\"md\" />\n </Tooltip>\n );\n }\n\n function rowMenu(row, split = splitActions(row)) {\n return <Menu items={split.menu} label={`Actions for ${nameOf(row)}`} size=\"md\" />;\n }\n\n function actionsCell(row) {\n const split = splitActions(row);\n if (!split.inline.length) return rowMenu(row, split);\n return (\n <div className=\"flex items-center justify-end gap-1.5\">\n {split.inline.map((a) => inlineButton(row, a, false))}\n {rowMenu(row, split)}\n </div>\n );\n }\n\n const empty = !loading && (manual ? total === 0 && !narrowed : all.length === 0) ? (\n <Empty icon={emptyIcon} title={emptyTitle} body={emptyBody} action={emptyAction} />\n ) : null;\n const noMatches = !loading && !empty && visible.length === 0 ? (\n <Empty\n icon=\"search\"\n title=\"No matches\"\n body={narrowed ? \"Nothing matches the search and filters.\" : `No ${noun}s on this page.`}\n action={\n narrowed ? (\n <Button size=\"sm\" icon=\"x\" onClick={clearFilters}>\n Clear filters\n </Button>\n ) : null\n }\n />\n ) : null;\n\n const skeletonRows = Array.from({ length: Math.min(per, 8) }, (_, i) => i);\n\n // The rows on this page, in runs of one group each (one run, no heading,\n // when the table is not grouped). A group's summary and subtotal are\n // over all of its rows, not only this page's; its subtotal shows on the\n // page where the group ends.\n const groupByKey = useMemo(() => new Map((groups ?? NONE).map((g) => [g.key, g])), [groups]);\n const runs = [];\n for (const row of visible) {\n const k = grouped ? groupKeyOf(groupValueOf(groupBy, row)) : null;\n let run = runs[runs.length - 1];\n if (!run || run.key !== k) runs.push((run = { key: k, group: groupByKey.get(k), rows: [] }));\n run.rows.push(row);\n }\n const labelOf = (g) => (groupLabel ? groupLabel(g.value, g.rows) : g.key === \"\" ? \"None\" : g.key);\n const totalsMap = typeof totals === \"function\" ? totals(manual ? all : matched) : totals;\n const showTotals = isColumnMap(totalsMap) && !loading && visible.length > 0;\n\n // One aligned row of per-column values (a subtotal, the totals): blank\n // under the leading and trailing controls, the label in the first\n // column when the map has nothing for it.\n function alignedCells(map, labelText, layer) {\n return (\n <>\n {expanding ? <TableCell className={cx(\"w-10 px-1\", stick(\"expander\", layer).className)} style={stick(\"expander\", layer).style} /> : null}\n {canSelect ? <TableCell className={cx(\"w-12\", stick(\"select\", layer).className)} style={stick(\"select\", layer).style} /> : null}\n {shown.map((c, j) => {\n const s = j === 0 ? stick(\"first\", layer) : {};\n const has = Object.prototype.hasOwnProperty.call(map, c.key);\n return (\n <TableCell key={c.key} numeric={c.align === \"end\" || c.numeric} className={cx(s.className, c.className)} style={s.style}>\n {has ? map[c.key] : j === 0 ? labelText : null}\n </TableCell>\n );\n })}\n {hasActions ? <TableCell className=\"w-12\" /> : null}\n </>\n );\n }\n\n function dataRow(row) {\n const id = keyOf(row);\n const tone = toneOf(row);\n const tint = tone ? TONES[tone] : null;\n const selected = pickedSet.has(id);\n const canOpen = expandable(row);\n const open = canOpen && openSet.has(id);\n const xid = `${uid}-x-${domId(id)}`;\n const sx = stick(\"expander\", \"body\", { tone, selected });\n const ss = stick(\"select\", \"body\", { tone, selected });\n const sf = stick(\"first\", \"body\", { tone, selected, bar: tone === \"highlighted\" && lead === 0 });\n const barFirst = tone === \"highlighted\" && lead === 0;\n const out = [\n <TableRow\n key={id}\n data-row={id}\n data-tone={tone && tone !== \"default\" ? tone : undefined}\n data-open={open ? \"\" : undefined}\n selected={selected}\n {...rowProps(row, id)}\n className={cx(\n clickable && \"cursor-pointer\",\n (clickable || keyboard) && cx(focusRing, \"focus-visible:bg-muted\"),\n open && \"border-b-0\",\n rowClassName?.(row),\n )}\n >\n {expanding ? (\n <TableCell className={cx(\"w-10 px-1 py-0\", stickyFirstColumn ? sx.className : tint, tone === \"highlighted\" && BAR)} style={sx.style}>\n {canOpen ? (\n // raw: a bare icon toggle whose glyph turns; no kit Button draws a rotating icon.\n <button\n type=\"button\"\n aria-expanded={open}\n aria-controls={open ? xid : undefined}\n aria-label={`Details for ${nameOf(row)}`}\n onClick={() => toggleOpen(id)}\n className=\"inline-flex size-8 items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <Icon name=\"chevron-right\" className={cx(\"transition-transform duration-150\", open && \"rotate-90\")} />\n </button>\n ) : null}\n </TableCell>\n ) : null}\n {canSelect ? (\n <TableCell className={cx(\"w-12 py-0\", stickyFirstColumn ? ss.className : tint, tone === \"highlighted\" && !expanding && BAR)} style={ss.style}>\n {checkbox(row)}\n </TableCell>\n ) : null}\n {shown.map((c, j) => {\n const s = j === 0 ? sf : {};\n return (\n <TableCell\n key={c.key}\n numeric={c.align === \"end\" || c.numeric}\n className={cx(j === 0 && stickyFirstColumn ? s.className : tint, barFirst && j === 0 && !stickyFirstColumn && BAR, c.className)}\n style={s.style}\n >\n {c.cell ? c.cell(row) : row?.[c.key] ?? \"\"}\n </TableCell>\n );\n })}\n {hasActions ? <TableCell className={cx(inlining ? \"whitespace-nowrap py-1.5\" : \"w-12 py-0\", \"text-right\", tint)}>{actionsCell(row)}</TableCell> : null}\n </TableRow>,\n ];\n if (open) {\n out.push(\n <TableRow key={`${id}-x`} className=\"hover:bg-transparent\">\n {/* raw: a bare cell — TableCell's padding would widen the table past the box. */}\n <td id={xid} colSpan={width} className=\"bg-muted/30 p-0 align-top\">\n <div className={cx(\"sticky left-0 w-[100cqw] px-4 py-3\", expanding && \"sm:pl-12\")}>{renderExpanded(row)}</div>\n </td>\n </TableRow>,\n );\n }\n return out;\n }\n\n function groupHead(run, i) {\n const g = run.group ?? { key: run.key, value: run.key, rows: run.rows };\n const summary = groupSummary ? groupSummary(g.rows) : null;\n return (\n <TableRow key={`${run.key}-h`} data-group={run.key} className={cx(\"bg-muted/40 hover:bg-muted/40\", i > 0 && \"border-t\")}>\n <th scope=\"rowgroup\" colSpan={width} className=\"h-10 px-3 text-left align-middle text-sm font-normal\">\n <div className=\"sticky left-3 flex w-[calc(100cqw-1.5rem)] items-center justify-between gap-3\">\n <span className=\"flex min-w-0 items-center gap-2\">\n <span className=\"font-semibold text-foreground\">{labelOf(g)}</span>\n <span data-nums className=\"text-xs text-muted-foreground\">\n {g.rows.length.toLocaleString()}\n </span>\n </span>\n {summary !== null && summary !== undefined && summary !== false ? (\n <span data-nums className=\"shrink-0 whitespace-nowrap text-muted-foreground\">\n {summary}\n </span>\n ) : null}\n </div>\n </th>\n </TableRow>\n );\n }\n\n function groupFoot(run) {\n const g = run.group;\n if (!groupFooter || !g || run.rows[run.rows.length - 1] !== g.rows[g.rows.length - 1]) return null;\n const content = groupFooter(g.rows);\n if (content === null || content === undefined || content === false) return null;\n return (\n <TableRow key={`${run.key}-f`} data-subtotal={run.key} className=\"bg-muted font-semibold hover:bg-muted\">\n {isColumnMap(content) ? (\n alignedCells(content, \"Subtotal\", \"sub\")\n ) : (\n <TableCell colSpan={width} className=\"text-right\">\n {content}\n </TableCell>\n )}\n </TableRow>\n );\n }\n\n // The table's scroll box is a size container: a details row or a group\n // heading spans the whole (maybe wider) table but is drawn at the width\n // you can see (100cqw), pinned to its left edge.\n const table = (\n <div className=\"min-w-0 max-w-full\" onKeyDown={keyboard ? onGridKey : undefined}>\n <Table variant={variant} aria-label={label} aria-busy={loading || undefined} className=\"@container\">\n <TableHeader>\n <TableRow>\n {expanding ? (\n <TableHead className={cx(\"w-10 px-1\", stick(\"expander\", \"head\").className)} style={stick(\"expander\", \"head\").style}>\n <span className=\"sr-only\">Details</span>\n </TableHead>\n ) : null}\n {canSelect ? (\n <TableHead className={cx(\"w-12 py-0\", stick(\"select\", \"head\").className)} style={stick(\"select\", \"head\").style}>\n <Checkbox\n checked={allOnPage}\n ref={(el) => {\n if (el) el.indeterminate = someOnPage;\n }}\n disabled={loading || visible.length === 0}\n onChange={togglePage}\n aria-label={allOnPage ? \"Deselect this page\" : \"Select this page\"}\n className=\"px-1\"\n />\n </TableHead>\n ) : null}\n {shown.map((c, j) => {\n const at = (order ?? NONE).findIndex((s) => s.key === c.key);\n const dir = at >= 0 ? order[at].dir : \"none\";\n const s = j === 0 ? stick(\"first\", \"head\") : {};\n return (\n <TableHead\n key={c.key}\n numeric={c.align === \"end\" || c.numeric}\n sort={c.sortable ? dir : undefined}\n onSort={c.sortable ? (e) => pressSort(e, c.key) : undefined}\n style={c.width || s.style ? { ...(c.width ? { width: c.width } : null), ...s.style } : undefined}\n sortIndex={c.sortable && at >= 0 && order.length > 1 ? at + 1 : undefined}\n className={cx(s.className, c.headerClassName)}\n title={c.sortable && multiSort ? \"Shift-click to sort by more than one column\" : undefined}\n >\n {c.header}\n </TableHead>\n );\n })}\n {hasActions ? (\n <TableHead className={inlining ? \"text-right\" : \"w-12\"}>\n <span className=\"sr-only\">Actions</span>\n </TableHead>\n ) : null}\n </TableRow>\n </TableHeader>\n {loading ? (\n <TableBody>\n {skeletonRows.map((i) => (\n <TableRow key={`s${i}`}>\n {expanding ? <TableCell className=\"w-10 px-1\" /> : null}\n {canSelect ? (\n <TableCell className=\"py-0\">\n <Skeleton shape=\"rect\" className=\"ml-1 size-4 rounded-sm\" />\n </TableCell>\n ) : null}\n {shown.map((c, j) => (\n <TableCell key={c.key} numeric={c.align === \"end\" || c.numeric}>\n <Skeleton className={cx(\"h-4\", c.align === \"end\" || c.numeric ? \"ml-auto w-16\" : j === 0 ? \"w-32\" : \"w-24\")} />\n </TableCell>\n ))}\n {hasActions ? <TableCell /> : null}\n </TableRow>\n ))}\n </TableBody>\n ) : grouped ? (\n // One <tbody> per group: its heading is a rowgroup header.\n runs.map((run, i) => (\n <TableBody key={run.key}>\n {groupHead(run, i)}\n {run.rows.flatMap(dataRow)}\n {groupFoot(run)}\n </TableBody>\n ))\n ) : (\n <TableBody>{visible.flatMap(dataRow)}</TableBody>\n )}\n {showTotals ? (\n <TableFooter>\n <TableRow data-totals=\"\" className=\"font-semibold hover:bg-transparent\">\n {alignedCells(totalsMap, totalsLabel, \"foot\")}\n </TableRow>\n </TableFooter>\n ) : null}\n </Table>\n </div>\n );\n\n // Phone cards: the same rows, one card each, below 640px. Group headings\n // and subtotals, a row's details and the totals come along.\n function pairs(map) {\n return shown\n .filter((c) => Object.prototype.hasOwnProperty.call(map, c.key))\n .map((c) => (\n <div key={c.key} className=\"min-w-0\">\n <dt className=\"text-xs font-normal text-muted-foreground\">{c.header}</dt>\n <dd data-nums className=\"truncate\">\n {map[c.key]}\n </dd>\n </div>\n ));\n }\n\n function card(row) {\n const id = keyOf(row);\n const tone = toneOf(row);\n const rest = cardTitle ? shown : shown.slice(1);\n const canOpen = expandable(row);\n const open = canOpen && openSet.has(id);\n const xid = `${uid}-cx-${domId(id)}`;\n const split = hasActions ? splitActions(row) : { inline: NONE, menu: NONE };\n return (\n <li\n key={id}\n data-row={id}\n data-tone={tone && tone !== \"default\" ? tone : undefined}\n data-selected={pickedSet.has(id) ? \"\" : undefined}\n {...rowProps(row, id)}\n className={cx(\n \"rounded-lg border border-border bg-card p-3 text-sm text-card-foreground data-[selected]:ring-2 data-[selected]:ring-ring\",\n tone && CARD_TONES[tone],\n clickable && \"cursor-pointer\",\n (clickable || keyboard) && \"outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n rowClassName?.(row),\n )}\n >\n <div className=\"flex items-start gap-1\">\n {canSelect ? <div className=\"-my-2 -ml-1 shrink-0\">{checkbox(row)}</div> : null}\n <div className=\"min-w-0 flex-1 font-medium\">{titleOf(row)}</div>\n {canOpen ? (\n // raw: the same details toggle, on a card.\n <button\n type=\"button\"\n aria-expanded={open}\n aria-controls={open ? xid : undefined}\n aria-label={`Details for ${nameOf(row)}`}\n onClick={() => toggleOpen(id)}\n className=\"-my-1 inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <Icon name=\"chevron-down\" className={cx(\"transition-transform duration-150\", open && \"rotate-180\")} />\n </button>\n ) : null}\n {hasActions ? <div className=\"-my-1 -mr-1 shrink-0\">{rowMenu(row, split)}</div> : null}\n </div>\n {rest.length ? (\n <dl className=\"mt-2 grid grid-cols-2 gap-x-3 gap-y-2\">\n {rest.map((c) => (\n <div key={c.key} className=\"min-w-0\">\n <dt className=\"text-xs text-muted-foreground\">{c.header}</dt>\n <dd className=\"truncate\">{c.cell ? c.cell(row) : row?.[c.key] ?? \"\"}</dd>\n </div>\n ))}\n </dl>\n ) : null}\n {split.inline.length ? <div className=\"mt-3 flex flex-wrap gap-2\">{split.inline.map((a) => inlineButton(row, a, true))}</div> : null}\n {open ? (\n <div id={xid} className=\"mt-3 border-t border-border pt-3\">\n {renderExpanded(row)}\n </div>\n ) : null}\n </li>\n );\n }\n\n const cards = (\n <ul className=\"grid gap-2\" aria-label={label} aria-busy={loading || undefined} onKeyDown={keyboard ? onGridKey : undefined}>\n {loading\n ? skeletonRows.slice(0, 4).map((i) => (\n <li key={`s${i}`} className=\"grid gap-2 rounded-lg border border-border bg-card p-3\">\n <Skeleton className=\"h-4 w-1/2\" />\n <Skeleton className=\"h-3 w-3/4\" />\n <Skeleton className=\"h-3 w-2/3\" />\n </li>\n ))\n : runs.flatMap((run) => {\n if (!grouped) return run.rows.map(card);\n const g = run.group ?? { key: run.key, value: run.key, rows: run.rows };\n const summary = groupSummary ? groupSummary(g.rows) : null;\n const ends = run.group && run.rows[run.rows.length - 1] === g.rows[g.rows.length - 1];\n const foot = groupFooter && ends ? groupFooter(g.rows) : null;\n return [\n <li key={`${run.key}-h`} data-group={run.key} className=\"flex items-baseline justify-between gap-3 px-1 pt-2 text-sm\">\n <span className=\"flex min-w-0 items-baseline gap-2\">\n <span className=\"truncate font-semibold\">{labelOf(g)}</span>\n <span data-nums className=\"text-xs text-muted-foreground\">\n {g.rows.length.toLocaleString()}\n </span>\n </span>\n {summary !== null && summary !== undefined && summary !== false ? (\n <span data-nums className=\"shrink-0 text-muted-foreground\">\n {summary}\n </span>\n ) : null}\n </li>,\n ...run.rows.map(card),\n foot !== null && foot !== undefined && foot !== false ? (\n <li key={`${run.key}-f`} data-subtotal={run.key} className=\"rounded-lg bg-muted px-3 py-2 text-sm font-semibold\">\n {isColumnMap(foot) ? <dl className=\"grid grid-cols-2 gap-x-3 gap-y-1\">{pairs(foot)}</dl> : foot}\n </li>\n ) : null,\n ];\n })}\n {showTotals ? (\n <li data-totals=\"\" className=\"rounded-lg border border-border bg-muted/50 px-3 py-2 text-sm font-semibold\">\n {totalsLabel ? <p className=\"mb-1\">{totalsLabel}</p> : null}\n <dl className=\"grid grid-cols-2 gap-x-3 gap-y-1\">{pairs(totalsMap)}</dl>\n </li>\n ) : null}\n </ul>\n );\n\n const body = empty ?? noMatches;\n const hideable = cols.filter((c) => c.hideable !== false);\n const from = total === 0 ? 0 : (pageNow - 1) * per + 1;\n const to = Math.min(total, pageNow * per);\n\n return (\n <div className={cx(\"grid min-w-0 max-w-full gap-3\", className)}>\n {error ? <Notice tone=\"danger\">{error}</Notice> : null}\n\n {/* The toolbar: search, facets, the surface's own, the columns menu. */}\n <div className=\"flex min-w-0 flex-wrap items-center gap-2\">\n {searchable ? (\n <div className=\"w-full sm:w-64\">\n <InputGroup\n icon=\"search\"\n type=\"search\"\n value={query ?? \"\"}\n onChange={(e) => changeQuery(e.target.value)}\n placeholder={searchPlaceholder}\n aria-label={`Search ${label}`}\n disabled={!manual && all.length === 0 && !loading}\n />\n </div>\n ) : null}\n {facetList.map((f) => {\n const options = facetOptions(f, all, filterState, facetList, query, cols);\n return (\n <div key={f.key} className=\"min-w-0 flex-1 basis-40 sm:w-52 sm:flex-none\">\n <Combobox\n multiple\n searchable={options.length > 7}\n options={options}\n value={(filterState[f.key] ?? NONE).map(String)}\n onValueChange={(values) => changeFacet(f.key, values)}\n placeholder={f.label}\n label={f.label}\n disabled={!manual && all.length === 0}\n />\n </div>\n );\n })}\n {narrowed ? (\n <Button variant=\"quiet\" size=\"sm\" icon=\"x\" onClick={clearFilters}>\n Reset\n </Button>\n ) : null}\n <div className=\"ml-auto flex flex-wrap items-center gap-2\">\n {toolbar}\n {hideable.length > 1 ? (\n <Menu\n label=\"Columns, show or hide\"\n size=\"md\"\n trigger={\n <>\n <Icon name=\"eye\" />\n <span>Columns</span>\n </>\n }\n className=\"border border-border\"\n items={[\n { type: \"label\", label: \"Show columns\" },\n ...hideable.map((c) => {\n const on = !hiddenSet.has(c.key);\n return {\n type: \"checkbox\",\n label: typeof c.header === \"string\" ? c.header : humanize(c.key),\n checked: on,\n // The last one shown stays: a table of no columns is nothing.\n disabled: on && shown.length <= 1,\n onCheckedChange: (next) =>\n setHidden((h) => {\n const list = (h ?? []).map(String);\n return next ? list.filter((k) => k !== c.key) : [...list, c.key];\n }),\n };\n }),\n ]}\n />\n ) : null}\n </div>\n </div>\n\n {/* The bulk bar: what is selected, and what can be done to it. */}\n {canSelect && count > 0 ? (\n <div\n role=\"region\"\n aria-label=\"Selected rows\"\n className=\"flex flex-wrap items-center gap-2 rounded-lg border border-border bg-muted px-3 py-2 text-sm\"\n >\n <span data-nums className=\"font-medium\">\n {count.toLocaleString()} selected\n </span>\n {!manual && allOnPage && count < total ? (\n <Button variant=\"link\" size=\"sm\" onClick={selectAllMatches}>\n Select all {total.toLocaleString()}\n </Button>\n ) : null}\n <Button variant=\"quiet\" size=\"sm\" onClick={clearSelection}>\n Clear\n </Button>\n <div className=\"ml-auto flex flex-wrap items-center gap-2\">\n {bulkItems.map((a) => (\n <Button\n key={a.label}\n size=\"sm\"\n icon={a.icon}\n variant={a.tone === \"danger\" ? \"danger\" : \"secondary\"}\n loading={running === a.label}\n disabled={Boolean(running && running !== a.label) || a.disabled}\n onClick={() => runBulk(a)}\n >\n {a.label}\n </Button>\n ))}\n </div>\n </div>\n ) : null}\n {bulkError ? (\n <Notice tone=\"danger\" onDismiss={() => setBulkError(null)}>\n {bulkError}\n </Notice>\n ) : null}\n {rowError ? (\n <Notice tone=\"danger\" onDismiss={() => setRowError(null)}>\n {rowError}\n </Notice>\n ) : null}\n {bulkNote ? (\n <Notice tone=\"info\" onDismiss={() => setBulkNote(null)}>\n {bulkNote}\n </Notice>\n ) : null}\n\n {body ?? (mobile === \"cards\" ? (\n <>\n <div className=\"grid gap-1 sm:hidden\">\n {canSelect && !loading && visible.length ? (\n <Checkbox\n checked={allOnPage}\n ref={(el) => {\n if (el) el.indeterminate = someOnPage;\n }}\n onChange={togglePage}\n label=\"Select this page\"\n className=\"px-1 text-muted-foreground\"\n />\n ) : null}\n {cards}\n </div>\n <div className=\"hidden sm:block\">{table}</div>\n </>\n ) : (\n table\n ))}\n\n {footer}\n\n {paginate && !body && (total > 0 || loading) ? (\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-2\">\n <p data-nums className=\"w-full text-sm text-muted-foreground sm:w-auto\" id={`${uid}-range`}>\n {loading ? \"Loading…\" : `${from.toLocaleString()}–${to.toLocaleString()} of ${plural(total, noun)}`}\n </p>\n <Pagination\n page={pageNow}\n pageCount={pageCount}\n onPageChange={setPage}\n pageSize={per}\n pageSizes={pageSizes}\n onPageSizeChange={(n) => {\n setSize(n);\n if (pageNow !== 1) setPage(1);\n }}\n size=\"sm\"\n label={`${label} pages`}\n className=\"min-w-0 flex-1\"\n />\n </div>\n ) : null}\n\n <p aria-live=\"polite\" className=\"sr-only\">\n {announce}\n </p>\n </div>\n );\n}\n"
23
+ },
24
+ {
25
+ "path": "registry/blocks/data-table/model.js",
26
+ "type": "registry:component",
27
+ "target": "@components/makersclaw/blocks/data-table/model.js",
28
+ "content": "\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\n\n// The table's arithmetic, apart from its drawing: controlled-or-not\n// state, and the pure pipeline rows go through (search → facets → sort →\n// page). Pure functions, so a block built on DataTable (report-table)\n// reads the same answers the table draws.\n\n/** One piece of state that is the caller's when `value` is given, else\n * the block's own. `onChange` is called either way. */\nexport function useControllable(value, defaultValue, onChange) {\n const controlled = value !== undefined;\n const [own, setOwn] = useState(defaultValue);\n const current = controlled ? value : own;\n const latest = useRef(current);\n latest.current = current;\n const set = useCallback(\n (next) => {\n const v = typeof next === \"function\" ? next(latest.current) : next;\n latest.current = v;\n if (!controlled) setOwn(v);\n onChange?.(v);\n },\n [controlled, onChange],\n );\n return [current, set];\n}\n\nconst isBlank = (v) => v === null || v === undefined || v === \"\";\n\n/** What a column holds for a row, for sorting. */\nexport function sortValueOf(column, row) {\n return column.sortValue ? column.sortValue(row) : row?.[column.key];\n}\n\n/** What a column says for a row, for the search. */\nexport function searchTextOf(column, row) {\n const v = column.searchValue ? column.searchValue(row) : sortValueOf(column, row);\n if (isBlank(v)) return \"\";\n if (v instanceof Date) return v.toISOString();\n return typeof v === \"object\" ? \"\" : String(v);\n}\n\n/** The value a facet groups a row under (always a string key). */\nexport function facetValueOf(facet, row) {\n const v = facet.value ? facet.value(row) : row?.[facet.key];\n return isBlank(v) ? \"\" : String(v);\n}\n\nfunction compare(a, b) {\n // Blanks sort last in either direction (the caller flips only non-blanks).\n if (a instanceof Date) a = a.getTime();\n if (b instanceof Date) b = b.getTime();\n if (typeof a === \"number\" && typeof b === \"number\") return a - b;\n if (typeof a === \"boolean\" && typeof b === \"boolean\") return Number(a) - Number(b);\n return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: \"base\" });\n}\n\n/** Rows ordered by `sort` ([{ key, dir: \"asc\" | \"desc\" }], first wins). */\nexport function sortRows(rows, sort, columns) {\n const keys = (sort ?? []).map((s) => ({ ...s, column: columns.find((c) => c.key === s.key) })).filter((s) => s.column);\n if (keys.length === 0) return rows;\n return rows\n .map((row, i) => ({ row, i }))\n .sort((x, y) => {\n for (const { column, dir } of keys) {\n const a = sortValueOf(column, x.row);\n const b = sortValueOf(column, y.row);\n const ba = isBlank(a);\n const bb = isBlank(b);\n if (ba || bb) {\n if (ba && bb) continue;\n return ba ? 1 : -1;\n }\n const c = compare(a, b);\n if (c !== 0) return dir === \"desc\" ? -c : c;\n }\n return x.i - y.i; // stable\n })\n .map((x) => x.row);\n}\n\n/** Rows whose searchable columns contain every word of `query`. */\nexport function searchRows(rows, query, columns) {\n const words = String(query ?? \"\").trim().toLowerCase().split(/\\s+/).filter(Boolean);\n if (words.length === 0) return rows;\n const searchable = columns.filter((c) => c.searchable !== false);\n return rows.filter((row) => {\n const text = searchable.map((c) => searchTextOf(c, row)).join(\" \\u0001 \").toLowerCase();\n return words.every((w) => text.includes(w));\n });\n}\n\n/** Rows that match every facet's chosen values (`filters`: { key: [v…] }).\n * `except` leaves one facet out — its own counts are taken that way. */\nexport function filterRows(rows, filters, facets, except) {\n const active = (facets ?? []).filter((f) => f.key !== except && (filters?.[f.key] ?? []).length > 0);\n if (active.length === 0) return rows;\n return rows.filter((row) => active.every((f) => filters[f.key].map(String).includes(facetValueOf(f, row))));\n}\n\n/** A facet's options with how many rows each would show, counted over\n * the rows the search and the OTHER facets leave. */\nexport function facetOptions(facet, rows, filters, facets, query, columns) {\n const pool = filterRows(searchRows(rows, query, columns), filters, facets, facet.key);\n const counts = new Map();\n for (const row of pool) {\n const v = facetValueOf(facet, row);\n counts.set(v, (counts.get(v) ?? 0) + 1);\n }\n const given = facet.options?.map((o) => (typeof o === \"object\" ? { ...o, value: String(o.value) } : { value: String(o), label: String(o) }));\n const all = given ?? [...new Set(rows.map((r) => facetValueOf(facet, r)))].sort((a, b) => compare(a, b)).map((v) => ({ value: v, label: v || \"None\" }));\n return all.map((o) => ({ ...o, count: o.count ?? counts.get(o.value) ?? 0 }));\n}\n\n/** Rows through search, facets and sort — the whole of client mode. */\nexport function processRows(rows, { query, filters, facets, sort, columns }) {\n return sortRows(filterRows(searchRows(rows, query, columns), filters, facets), sort, columns);\n}\n\n/** The next sort after a press on `key`: plain press sorts by it alone\n * (asc → desc → off); with `add` it joins or cycles within the list. */\nexport function nextSort(sort, key, add) {\n const now = sort ?? [];\n const at = now.find((s) => s.key === key);\n const cycled = !at ? { key, dir: \"asc\" } : at.dir === \"asc\" ? { key, dir: \"desc\" } : null;\n if (!add) return cycled ? [cycled] : [];\n if (!at) return [...now, cycled];\n return cycled ? now.map((s) => (s.key === key ? cycled : s)) : now.filter((s) => s.key !== key);\n}\n\n/** The value `groupBy` (a key or `(row) => value`) files a row under. */\nexport function groupValueOf(groupBy, row) {\n const v = typeof groupBy === \"function\" ? groupBy(row) : row?.[groupBy];\n return isBlank(v) ? \"\" : v;\n}\n\n/** The string a group is known by (its React key, its place in `order`). */\nexport function groupKeyOf(value) {\n return value instanceof Date ? value.toISOString() : String(value ?? \"\");\n}\n\n/** Rows filed under `groupBy`: [{ key, value, rows }]. Rows keep their\n * order inside a group (sort first, then group: a sort orders rows\n * within their groups and never moves a group). `order` places the\n * groups: an array of group values (those listed first, in that order;\n * the rest by first appearance in `rows`) or a compare function over\n * two group values. */\nexport function groupRows(rows, groupBy, order) {\n if (groupBy === undefined || groupBy === null || groupBy === false) return null;\n const map = new Map();\n for (const row of rows ?? []) {\n const value = groupValueOf(groupBy, row);\n const key = groupKeyOf(value);\n let g = map.get(key);\n if (!g) map.set(key, (g = { key, value, rows: [] }));\n g.rows.push(row);\n }\n const groups = [...map.values()];\n if (typeof order === \"function\") return groups.sort((a, b) => order(a.value, b.value));\n if (!Array.isArray(order)) return groups;\n const rank = new Map();\n for (const v of order) if (!rank.has(groupKeyOf(v))) rank.set(groupKeyOf(v), rank.size);\n return groups\n .map((g, i) => ({ g, i }))\n .sort((x, y) => (rank.get(x.g.key) ?? rank.size + x.i) - (rank.get(y.g.key) ?? rank.size + y.i))\n .map((x) => x.g);\n}\n"
29
+ },
30
+ {
31
+ "path": "registry/blocks/data-table/README.md",
32
+ "type": "registry:file",
33
+ "target": "@components/makersclaw/blocks/data-table/README.md",
34
+ "content": "# Data table\n\nRecords as a table the founder can work. Click a header to sort (shift-click\nto sort by more than one column), search every column, narrow by facets that\nshow how many rows each choice leaves, pick which columns show, select rows\nand act on them together from a bar that appears, act on one row from its\nmenu or from buttons in the row, open a row with a click, and move through\npages of a chosen size. Opt in to rows that open to show their details,\nrows filed under group headings with subtotals, an aligned totals row,\narrow keys over the rows, and a first column that stays put while a wide table scrolls sideways.\nLoading shows skeleton rows; \"no rows yet\" and \"nothing matches\" are two\ndifferent states, the second with a way out. It runs over `rows` in the\nbrowser, or — with `manual` — shows the page a server chose.\n\n```jsx\nimport { DataTable } from \"../components/blocks/data-table\"; // a Makersclaw app (\"./blocks/data-table\" from components/)\nimport { DataTable } from \"@/components/makersclaw/blocks/data-table\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component, and\nload the rows in the server surface (README §4).\n\n## The data\n\nAny array of objects with a unique id (`idColumn`, default `\"id\"`). Each\ncolumn reads one key of a row, or draws the cell itself with `cell(row)`.\n\n```js\nconst COLUMNS = [\n { key: \"number\", header: \"Invoice\", sortable: true, hideable: false },\n { key: \"customer\", header: \"Customer\", sortable: true, cell: (r) => <CustomerCell r={r} /> },\n { key: \"status\", header: \"Status\", sortable: true, cell: (r) => <Badge tone={TONE[r.status]}>{r.status}</Badge> },\n { key: \"amount\", header: \"Amount\", sortable: true, align: \"end\", cell: (r) => money(r.amount) },\n { key: \"method\", header: \"Method\", hidden: true },\n];\n```\n\nA column:\n\n- `key` — the row key it reads (and its id for sort, filters, visibility).\n- `header` — what the header says; default made from the key.\n- `cell(row)` — what the cell draws; default the value as text. Kit parts\n and tokens only.\n- `sortable` — the header becomes a sort button.\n- `sortValue(row)` — what it sorts by, when the cell is not the value\n (a status in its own order, a date string, a nested field). Numbers sort\n as numbers, strings naturally (\"INV-9\" before \"INV-10\"); blanks sort\n last either way.\n- `searchValue(row)` — what the search matches; default `sortValue` or\n the value. `searchable: false` leaves the column out of the search.\n- `align` — `\"start\"` (default) or `\"end\"` (amounts: right-aligned,\n tabular figures). `numeric: true` means the same.\n- `width` — a CSS width for the column (`\"8rem\"`).\n- `hideable` — default true; false keeps it out of the Columns menu.\n- `hidden` — starts hidden (the Columns menu can show it).\n- `className` · `headerClassName` — layout on its cells or its header.\n Never colour.\n\nAny other key on a column is kept and ignored, so a block built on this\none can carry its own (report-table's `kind`).\n\n## Props\n\n- `rows` — the records. Pass `useFixture(rows, SAMPLE).rows` and the\n examples show, sort and filter like rows, but a bulk action never\n receives them (they are not rows): selecting only examples and pressing\n one says so and changes nothing.\n- `columns` — as above.\n- `idColumn` — default `\"id\"`.\n- `label` — what the table is called to a screen reader, default `\"Table\"`.\n- `noun` — what a row is (\"invoice\"): used in \"1–10 of 60 invoices\", the\n facet counts and the announcements. Default `\"row\"`.\n\nSearch\n\n- `searchable` — default true: a search field with a glyph leads the\n toolbar. Every word must appear somewhere in the row.\n- `searchPlaceholder` — default `\"Search…\"`.\n- `search` · `defaultSearch` · `onSearchChange(text)` — the text.\n\nFacets\n\n- `facets` — `[{ key, label?, options?, value?(row) }]`: one multi-choice\n Combobox per facet, each option with the number of rows it would show\n at its right (counted over the search and the other facets). `options`\n `[{ value, label }]` fixes the choices and their order; without it they\n are the values found in `rows`. `value(row)` when the facet is not a\n plain key.\n- `filters` · `defaultFilters` · `onFiltersChange(filters)` — the chosen\n values, `{ status: [\"paid\", \"overdue\"], owner: [\"Maya\"] }`.\n\nSort\n\n- `sort` · `defaultSort` · `onSortChange(sort)` — `[{ key, dir: \"asc\" | \"desc\" }]`,\n the first entry sorts first. A press cycles a column asc → desc → off.\n- `multiSort` — default true: shift-click adds a column to the sort (each\n sorted header shows its place). False: one column at a time.\n\nSelection\n\n- `selectable` — checkboxes on every row and a select-this-page box in\n the header. On by default when `bulkActions`, `selection` or\n `onSelectionChange` is given.\n- `selection` · `defaultSelection` · `onSelectionChange(ids)` — the chosen\n ids, as strings. A selection survives search, filters and paging.\n- `bulkActions` — `[{ label, onSelect(rows, ids), icon?, tone?: \"danger\", disabled?, keepSelection? }]`,\n or `(selectedRows) => [...]`. With rows selected a bar shows the count,\n \"Select all N\" (when the whole page is chosen and more match), Clear,\n and these as buttons. `onSelect` may be async; return\n `{ ok: false, error }` (or throw) and the bar says why it failed and\n keeps the selection. Anything else clears the selection, unless\n `keepSelection: true` (an export).\n\nColumns\n\n- `hiddenColumns` · `defaultHiddenColumns` · `onHiddenColumnsChange(keys)` —\n the keys not shown. Default: the columns marked `hidden`. The Columns\n menu (checkbox items) sets it; the last column shown cannot be hidden.\n\nPages\n\n- `page` · `defaultPage` (1) · `onPageChange(page)` — 1-based. A new\n search, filter, sort or page size goes back to page 1; a page past the\n end (rows removed) goes to the last.\n- `pageSize` · `defaultPageSize` (10) · `onPageSizeChange(n)`, `pageSizes`\n (default `[10, 25, 50, 100]`) — the \"Rows per page\" choice.\n- `paginate` — default true; false shows every row and no pager.\n\nRows\n\n- `rowActions(row)` — the row's \"…\" menu, in kit `Menu` items\n (`[{ label, onSelect, icon?, tone?: \"danger\" }, { type: \"separator\" }, …]`;\n a falsy item is skipped). Nothing returned, no menu.\n- `inlineActions` — draw a row's first actions as buttons in the row\n instead of in the menu: a number (the first N of `rowActions(row)`) or\n `true` (all of them). An action with `inline: true` is always a button,\n one with `inline: false` never is; separators and labels stay in the\n menu, and the \"…\" menu holds whatever is left. A button with an `icon`\n is a square glyph button with a tooltip (on a phone card, the glyph and\n the words); without one it is a small text button (\"View\"). Its\n `onSelect(row)` may be async: the button spins and the row's other\n buttons wait, and `{ ok: false, error }` (or a throw) shows a danger\n notice above the table naming the action and the row. `tone: \"danger\"`\n and `disabled` work as in the menu. Default: none — every action in the\n menu, as before.\n- `onRowClick(row)` — a click on the row (not on its checkbox, menu or a\n link inside a cell), or Enter on a focused row: open your `Sheet`.\n- `rowTone(row)` — `\"success\" | \"info\" | \"warning\" | \"danger\" | \"highlighted\"`\n or nothing: a tint on the row's cells from the theme's status colours\n (hover deepens it). `\"highlighted\"` is a pinned or best row, not a\n status: a light primary tint and a bar at its leading edge. Status tones\n are for status only.\n- `rowClassName(row)` — more classes on the row (layout, weight). Never\n colour; that is `rowTone`.\n\nDetails (expandable rows)\n\n- `renderExpanded(row)` — what shows under a row when it is open. Adds a\n leading chevron column; the chevron turns when the row is open, and is\n a button with `aria-expanded`. A row with no `onRowClick` also opens on\n a click or Enter.\n- `canExpand(row)` — false leaves a row without a chevron. Default: every\n row can open.\n- `expanded` · `defaultExpanded` · `onExpandedChange(ids)` — the open ids,\n as strings.\n\nGroups\n\n- `groupBy` — a key or `(row) => value`: each group gets a heading row\n with its label and how many rows it holds. Sort first, then group: a\n sort orders rows inside their groups and never moves a group. Paging\n counts rows; a group split across pages repeats its heading.\n- `groupOrder` — the group values to put first, in order\n (`[\"overdue\", \"pending\"]`; the rest follow in the order they first\n appear in `rows`), or a compare function over two group values.\n Default: the order groups first appear in `rows`.\n- `groupLabel(value, rows)` — the heading's label. Default the value\n (`\"None\"` for a blank).\n- `groupSummary(rows)` — a node at the heading's right: a sum, a count.\n `rows` is the whole group, not only this page's part.\n- `groupFooter(rows)` — a subtotal row under the group, bold on a tinted\n background, on the page where the group ends. Return a map of column\n key to node (`{ amount: \"$4,200\" }`) for cells aligned under their\n columns (\"Subtotal\" fills the first column unless the map has it), or\n one node for a single right-aligned cell.\n\nTotals\n\n- `totals` — a map of column key to node, or `(rows) => map` over every\n row that matches the search and filters (all pages; in `manual`, the\n rows given): one row in the table's `<tfoot>`, each value under its\n column. `footer` is still the free node under the table.\n- `totalsLabel` — default `\"Total\"`, in the first column when the map\n has nothing for it; `null` for none.\n\nKeyboard\n\n- `keyboard` — arrow keys over the rows. One row is in the tab order at\n a time (the last one focused, else the first) and shows a focus ring.\n ↑ ↓ move, Home and End go to the first and last row on the page, Space\n selects the row (when `selectable`), Enter opens it as a click does,\n → and ← open and close its details, Esc clears the selection. Works on\n the phone cards too.\n\nLayout\n\n- `stickyFirstColumn` — the first shown column (and the chevron and\n checkbox before it) stays put while the table scrolls sideways, with an\n edge line. A wide table always scrolls inside its own box, never the\n page; details rows and group headings are drawn at the box's visible\n width.\n\nSlots and states\n\n- `toolbar` — your controls at the toolbar's end, before Columns: the one\n \"New invoice\" button, an export.\n- `footer` — a node under the table, above the pager: a total, a note.\n- `loading` — skeleton rows in place of the body (the toolbar stays).\n- `error` — a danger Notice above the table (a failed read).\n- `emptyTitle` · `emptyBody` · `emptyAction` · `emptyIcon` — the state for\n no rows at all. No matches has its own state with \"Clear filters\".\n- `mobile` — `\"scroll\"` (default): under 640px the table scrolls sideways\n inside its own box; the page never does. `\"cards\"`: under 640px each row\n is a card — the first shown column as its title, the others as\n label/value pairs, with its checkbox and menu. Pick cards when rows are\n read more than compared; scroll when columns are compared.\n- `cardTitle(row)` — the card's title instead of the first column (the\n card then lists every shown column).\n- `variant` — `\"outline\"` (a bordered card; default) or `\"plain\"` (no\n border, for a table already inside a `Card`).\n- `className` — layout around it (width, margin). Never colour.\n\n### Controlled or not — letting a server drive it\n\nEvery piece of state has three props: the value, a `default…` and an\n`on…Change`. Pass only the default (or nothing) and the table keeps it.\nPass the value and the table shows what you pass and calls `on…Change`\nwith what the founder asked for; you set it back. Each is independent, so\nyou can own just the page and leave the rest to the table.\n\nFor a table too big for the browser, set `manual`: the table stops\nsearching, filtering, sorting and slicing, `rows` is the current page, and\n`rowCount` is how many rows the server has. Own `search`, `filters`,\n`sort`, `page` and `pageSize`, re-read on each change, and give each facet\nits `options` with a `count` (`{ value, label, count }`), since the table\ncannot count rows it does not have. With `manual`, a bulk action's second\nargument, `ids`, is every selected id, including rows not on this page.\n\n```jsx\nconst [q, setQ] = useState({ search: \"\", filters: {}, sort: [], page: 1, pageSize: 25 });\n// …read rows + total for q on the server (a Server Action, a route)…\n<DataTable manual rows={pageRows} rowCount={total} loading={pending}\n search={q.search} onSearchChange={(search) => setQ((s) => ({ ...s, search }))}\n filters={q.filters} onFiltersChange={(filters) => setQ((s) => ({ ...s, filters }))}\n sort={q.sort} onSortChange={(sort) => setQ((s) => ({ ...s, sort }))}\n page={q.page} onPageChange={(page) => setQ((s) => ({ ...s, page }))}\n pageSize={q.pageSize} onPageSizeChange={(pageSize) => setQ((s) => ({ ...s, pageSize }))}\n columns={COLUMNS} facets={[{ key: \"status\", options: statusCounts }]} />\n```\n\nThe pure helpers the table uses are exported too — `processRows`,\n`searchRows`, `filterRows`, `sortRows`, `facetOptions`, `nextSort`,\n`groupRows`, `groupValueOf` and `useControllable` — so a surface can compute the same answer the table\nshows (a total over the filtered rows) without drawing it twice.\n\n## Example\n\n```jsx\n// components/Invoices.jsx — client: the table, the row sheet\n\"use client\";\nimport { useState } from \"react\";\nimport { updateRow } from \"../lib/actions\";\nimport { Badge, Button, Sheet } from \"./ui\";\nimport { FixtureNotice, useFixture } from \"./parts\";\nimport { DataTable } from \"./blocks/data-table\";\n\nconst TONE = { paid: \"success\", pending: \"info\", overdue: \"danger\", draft: \"neutral\" };\nconst COLUMNS = [\n { key: \"number\", header: \"Invoice\", sortable: true, hideable: false },\n { key: \"customer\", header: \"Customer\", sortable: true },\n { key: \"status\", header: \"Status\", sortable: true, cell: (r) => <Badge tone={TONE[r.status]}>{r.status}</Badge> },\n { key: \"amount\", header: \"Amount\", sortable: true, align: \"end\", cell: (r) => `$${r.amount.toLocaleString()}` },\n { key: \"due\", header: \"Due\", sortable: true },\n];\nconst SAMPLE = [\n { number: \"INV-1001\", customer: \"Northwind\", status: \"paid\", amount: 1200, due: \"2026-10-01\" },\n { number: \"INV-1002\", customer: \"Acme Ltd\", status: \"overdue\", amount: 450, due: \"2026-09-01\" },\n];\n\nexport default function Invoices({ invoices }) {\n const fixture = useFixture(invoices, SAMPLE);\n const [open, setOpen] = useState(null);\n const markPaid = (rows) =>\n Promise.all(rows.map((r) => updateRow({ source: \"public.invoices\", id: r.id, patch: { status: \"paid\" } })))\n .then((all) => all.find((r) => r.ok === false) ?? { ok: true });\n return (\n <div className=\"grid gap-4\">\n {fixture.isFixture ? <FixtureNotice onClear={fixture.clear} /> : null}\n <DataTable\n label=\"Invoices\"\n noun=\"invoice\"\n rows={fixture.rows}\n columns={COLUMNS}\n facets={[{ key: \"status\", label: \"Status\" }]}\n defaultSort={[{ key: \"due\", dir: \"asc\" }]}\n bulkActions={[{ label: \"Mark paid\", icon: \"check\", onSelect: markPaid }]}\n rowActions={(r) => [{ label: \"Open\", icon: \"eye\", onSelect: () => setOpen(r) }]}\n onRowClick={setOpen}\n rowTone={(r) => (r.status === \"overdue\" ? \"warning\" : null)}\n toolbar={<Button variant=\"primary\" icon=\"plus\">New invoice</Button>}\n mobile=\"cards\"\n emptyTitle=\"No invoices yet\"\n />\n <Sheet open={Boolean(open)} onClose={() => setOpen(null)} title={open?.number}>\n {/* the invoice's fields */}\n </Sheet>\n </div>\n );\n}\n```\n\n### Details, groups and totals\n\nA budget: line items under departments, sorted within them, a subtotal\nper department, a total, the months scrolling under the line item.\n\n```jsx\nconst MONTHS = [\"jan\", \"feb\", \"mar\"];\nconst money = (n) => `$${n.toLocaleString()}`;\nconst sumOf = (rows, key) => rows.reduce((s, r) => s + r[key], 0);\nconst sums = (rows) => Object.fromEntries([...MONTHS, \"total\"].map((k) => [k, money(sumOf(rows, k))]));\nconst COLUMNS = [\n { key: \"item\", header: \"Line item\", sortable: true },\n ...MONTHS.map((m) => ({ key: m, align: \"end\", sortable: true, cell: (r) => money(r[m]) })),\n { key: \"total\", align: \"end\", sortable: true, cell: (r) => money(r.total) },\n];\n\n<DataTable label=\"Budget\" noun=\"line item\" rows={lines} columns={COLUMNS}\n paginate={false} keyboard stickyFirstColumn\n groupBy=\"department\" groupOrder={[\"Engineering\"]}\n groupSummary={(rows) => `${money(sumOf(rows, \"total\"))} this quarter`}\n groupFooter={sums} totals={sums}\n renderExpanded={(r) => <p className=\"text-sm\">{r.note}</p>}\n canExpand={(r) => Boolean(r.note)}\n defaultSort={[{ key: \"total\", dir: \"desc\" }]} />\n```\n\n### Buttons in the row\n\nTasks whose actions depend on their state, the common ones as buttons in\nthe row and the rest in the menu:\n\n```jsx\n<DataTable label=\"Tasks\" noun=\"task\" rows={tasks} columns={COLUMNS}\n rowActions={(t) => [\n t.status === \"todo\" && { label: \"Start\", icon: \"play\", inline: true, onSelect: (r) => setStatus(r, \"doing\") },\n t.status === \"doing\" && { label: \"Complete\", icon: \"check\", inline: true, onSelect: (r) => setStatus(r, \"done\") },\n { label: \"Delete\", icon: \"trash\", tone: \"danger\", inline: true, onSelect: remove }, // async, may return { ok: false, error }\n { label: \"Copy ID\", icon: \"copy\", onSelect: copyId },\n ]} />\n```\n\n`setStatus` and `remove` return a promise of `{ ok }`, as a Server Action\ndoes; a failure is said above the table.\n\n## What it will not do\n\n- Edit a cell in place: open the row (`onRowClick`) in a `Sheet`.\n- Fetch: rows come in as props; with `manual` you read each page.\n- Collapse a group, pin more than the first column, resize or reorder\n columns, or virtualise thousands of rows — page them (`manual`)\n instead.\n- Colour a column: a cell says what it means with kit parts (`Badge`,\n `StatusDot`); a row's tone is `rowTone`, and only for status.\n\nA table it cannot be is your own component in `components/`, built from\nthe kit's `Table` parts — not an edit to this block. Its files are fixed:\n`npm run check` refuses a change, and `makersclaw add data-table` restores\nthem.\n\n## Credits\n\nThe row buttons and the orders example follow blocks.so\n(https://github.com/ephraimduncan/blocks, MIT) and Creative Tim UI\n(https://github.com/creativetimofficial/ui, MIT); this is a rewrite on the\nMakersclaw kit, not their code.\n"
35
+ }
36
+ ],
37
+ "categories": [
38
+ "table",
39
+ "data",
40
+ "records",
41
+ "list",
42
+ "grid",
43
+ "sort",
44
+ "filter",
45
+ "search",
46
+ "facets",
47
+ "select",
48
+ "bulk",
49
+ "pagination",
50
+ "columns",
51
+ "crud",
52
+ "expand",
53
+ "group",
54
+ "subtotal",
55
+ "totals",
56
+ "keyboard",
57
+ "sticky",
58
+ "row actions",
59
+ "inline actions",
60
+ "orders"
61
+ ],
62
+ "docs": "Installed to components/makersclaw/blocks/data-table. Import: import { DataTable, useControllable, processRows, sortRows, searchRows, filterRows, facetOptions, nextSort, groupRows, groupValueOf } from \"@/components/makersclaw/blocks/data-table\". Props and an example: components/makersclaw/blocks/data-table/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
63
+ "meta": {
64
+ "makersclaw": {
65
+ "version": "0.3.0",
66
+ "useWhen": "A list of records to scan, find, sort and act on: invoices, customers, orders, leads, users, tickets. Not for records that move through stages (a board), a few key numbers (stats), or a grid of cards whose picture matters more than its columns."
67
+ }
68
+ }
69
+ }
package/r/diagram.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "diagram",
4
+ "type": "registry:block",
5
+ "title": "Diagram",
6
+ "description": "Draws Mermaid text (flowcharts, sequence, class, state, ER, Gantt, pie, mind maps, timelines, git graphs…) in the workspace theme: the tokens are read from the page at run time and handed to Mermaid, so nodes, lines, text and chart colours are the theme's and follow light/dark. Zoom and pan (buttons, Cmd/Ctrl+scroll, pinch, drag, keys), fit to view, a Source tab with line numbers, copy the source or the SVG, download SVG. A mistake shows its line and Mermaid's words, and keeps the last drawing that worked. On the server the frame shows the Mermaid text.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/core.json"
9
+ ],
10
+ "dependencies": [
11
+ "mermaid@12.0.0"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "registry/blocks/diagram/index.js",
16
+ "type": "registry:component",
17
+ "target": "@components/makersclaw/blocks/diagram/index.js",
18
+ "content": "// The Diagram block — `makersclaw add diagram`. Fixed: compose it through\n// its props (README.md beside this file). The helpers read Mermaid text\n// anywhere, the server included; blocks built on this one draw through\n// <Diagram>, never mermaid directly.\nexport { Diagram } from \"./diagram\";\nexport { diagramKind, diagramTitle, describeError } from \"./source\";\nexport { mermaidTheme } from \"./palette\";\n"
19
+ },
20
+ {
21
+ "path": "registry/blocks/diagram/diagram.jsx",
22
+ "type": "registry:component",
23
+ "target": "@components/makersclaw/blocks/diagram/diagram.jsx",
24
+ "content": "\"use client\";\n\n// Diagram — Mermaid text drawn in the workspace theme, with zoom and pan.\n//\n// Client only, one static bundle: Mermaid needs the DOM to measure text,\n// and the app's check refuses import() (so no next/dynamic). The modules\n// load with the page; the drawing happens after the first effect (the\n// mount guard). Until then — on the server and in the first client paint —\n// the frame shows the Mermaid text itself, so the HTML already says what\n// the diagram is.\n//\n// Colours: the tokens are read from the page at run time (palette.js) and\n// handed to Mermaid as its theme; a change of workspace theme or of\n// light/dark reads them again and redraws.\nimport { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport { Button, Empty, Icon, IconButton, Menu, Notice, Skeleton, ToggleGroup, cx, toast } from \"../../ui\";\nimport { renderMermaid } from \"./mermaid\";\nimport { mermaidTheme, paletteKey, resolvePalette } from \"./palette\";\nimport { diagramKind, diagramTitle } from \"./source\";\n\nconst PAD = 24;\nconst MIN_K = 0.2;\nconst MAX_K = 4;\nconst px = (v) => (typeof v === \"number\" ? `${v}px` : v);\nconst clamp = (v, a, b) => Math.max(a, Math.min(b, v));\n\n// The layout effect runs only in the browser; on the server it would warn.\nconst useIsoLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\nexport function Diagram(props) {\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n if (!mounted) return <DiagramPlaceholder {...props} />;\n return <LiveDiagram {...props} />;\n}\n\nconst FRAME = \"flex min-w-0 flex-col overflow-hidden rounded-lg border border-border bg-card text-card-foreground\";\n\nfunction heading(source, title) {\n return title ?? diagramTitle(source) ?? diagramKind(source) ?? \"Diagram\";\n}\n\n/** What the server renders: the frame, its heading, and the Mermaid text. */\nfunction DiagramPlaceholder({ source = \"\", title, height, minHeight = 160, toolbar = true, className }) {\n const kind = diagramKind(source);\n return (\n <figure className={cx(FRAME, className)} aria-busy=\"true\">\n {toolbar ? (\n <figcaption className=\"flex min-h-12 items-center gap-2 border-b border-border ps-4 pe-2\">\n <span className=\"truncate text-sm font-medium\">{heading(source, title)}</span>\n {kind && title ? <span className=\"truncate text-xs text-muted-foreground max-sm:hidden\">{kind}</span> : null}\n </figcaption>\n ) : null}\n <pre className=\"overflow-hidden px-4 py-3 font-mono text-xs leading-relaxed text-muted-foreground\" style={height != null ? { height: px(height) } : { minHeight: px(minHeight), maxHeight: px(minHeight * 2) }}>\n {source}\n </pre>\n </figure>\n );\n}\n\nfunction LiveDiagram({ source = \"\", title, label, height, minHeight = 160, maxHeight = 560, toolbar = true, look = \"classic\", onRender, className }) {\n const frame = useRef(null);\n const viewport = useRef(null);\n const content = useRef(null);\n const [palette, setPalette] = useState(null);\n const [result, setResult] = useState(null);\n const [good, setGood] = useState(null);\n const [size, setSize] = useState(null);\n const [box, setBox] = useState({ w: 0, h: 0 });\n const [t, setT] = useState({ k: 1, x: 0, y: 0 });\n const [pristine, setPristine] = useState(true);\n const [smooth, setSmooth] = useState(false);\n const [tab, setTab] = useState(\"diagram\");\n const [dragging, setDragging] = useState(false);\n const onRenderRef = useRef(onRender);\n onRenderRef.current = onRender;\n\n // The tokens, read now and again whenever the theme may have changed.\n useEffect(() => {\n const el = frame.current;\n let raf = 0;\n const read = () => {\n cancelAnimationFrame(raf);\n raf = requestAnimationFrame(() => {\n const p = resolvePalette(el);\n setPalette((prev) => (prev && paletteKey(prev) === paletteKey(p) ? prev : p));\n });\n };\n read();\n const mo = new MutationObserver(read);\n const attrs = { attributes: true, attributeFilter: [\"class\", \"style\", \"data-theme\"] };\n mo.observe(document.documentElement, attrs);\n mo.observe(document.head, { childList: true });\n const themed = el.parentElement?.closest(\"[data-theme]\");\n if (themed) mo.observe(themed, attrs);\n const mq = window.matchMedia?.(\"(prefers-color-scheme: dark)\");\n mq?.addEventListener?.(\"change\", read);\n // A theme stylesheet that finishes loading changes the tokens too.\n const onLoad = (e) => {\n if (e.target?.tagName === \"LINK\") read();\n };\n document.addEventListener(\"load\", onLoad, true);\n return () => {\n cancelAnimationFrame(raf);\n mo.disconnect();\n mq?.removeEventListener?.(\"change\", read);\n document.removeEventListener(\"load\", onLoad, true);\n };\n }, []);\n\n // Draw: after the text rests for a moment (someone may be typing it).\n useEffect(() => {\n if (!palette) return;\n if (!source.trim()) {\n setResult(null);\n setGood(null);\n return;\n }\n let live = true;\n const timer = setTimeout(() => {\n const font = getComputedStyle(frame.current).fontFamily;\n renderMermaid(source, { themeVariables: mermaidTheme(palette, { fontFamily: font }), look, fontFamily: font }).then((r) => {\n if (!live) return;\n setResult(r);\n if (r.ok) setGood(r.svg);\n onRenderRef.current?.(r);\n });\n }, result ? 150 : 0);\n return () => {\n live = false;\n clearTimeout(timer);\n };\n // `result` only picks the delay; it must not redraw.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [source, palette, look]);\n\n // The drawing's own size, once it is in the page; Mermaid's width=\"100%\"\n // and max-width are taken off so zoom is ours.\n useIsoLayoutEffect(() => {\n const svg = content.current?.querySelector(\"svg\");\n if (!svg) return;\n const vb = svg.viewBox?.baseVal;\n let w = vb?.width || 0;\n let h = vb?.height || 0;\n if (!w || !h) {\n const r = svg.getBoundingClientRect();\n w = r.width || 400;\n h = r.height || 300;\n }\n svg.style.maxWidth = \"none\";\n svg.setAttribute(\"width\", String(w));\n svg.setAttribute(\"height\", String(h));\n svg.setAttribute(\"aria-hidden\", \"true\");\n setSize((s) => (s && s.w === w && s.h === h ? s : { w, h }));\n }, [good, tab]);\n\n // The viewport's size.\n useEffect(() => {\n const el = viewport.current;\n if (!el) return;\n const measure = () => setBox((b) => (b.w === el.clientWidth && b.h === el.clientHeight ? b : { w: el.clientWidth, h: el.clientHeight }));\n measure();\n const ro = new ResizeObserver(measure);\n ro.observe(el);\n return () => ro.disconnect();\n }, [tab, good]);\n\n // How tall the canvas is: `height`, else the drawing at the width's fit.\n const autoH = size && box.w ? size.h * Math.min(1, (box.w - 2 * PAD) / size.w) + 2 * PAD : minHeight;\n const canvasH = height != null ? px(height) : `${Math.round(clamp(autoH, minHeight, maxHeight))}px`;\n\n const fitted = () => {\n if (!size || !box.w || !box.h) return null;\n const k = clamp(Math.min(1, (box.w - 2 * PAD) / size.w, (box.h - 2 * PAD) / size.h), MIN_K, MAX_K);\n return { k, x: (box.w - size.w * k) / 2, y: (box.h - size.h * k) / 2 };\n };\n\n // Untouched, the drawing follows the frame (a resize, a new drawing).\n useEffect(() => {\n if (!pristine) return;\n const f = fitted();\n if (f) setT(f);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [size, box.w, box.h, pristine]);\n\n const glide = () => {\n setSmooth(true);\n setTimeout(() => setSmooth(false), 180);\n };\n const zoomAt = (factor, cx0 = box.w / 2, cy0 = box.h / 2, animate = true) => {\n setPristine(false);\n if (animate) glide();\n setT((cur) => {\n const k = clamp(cur.k * factor, MIN_K, MAX_K);\n return { k, x: cx0 - ((cx0 - cur.x) * k) / cur.k, y: cy0 - ((cy0 - cur.y) * k) / cur.k };\n });\n };\n const fit = () => {\n glide();\n setPristine(true);\n const f = fitted();\n if (f) setT(f);\n };\n const actual = () => {\n if (!size) return;\n glide();\n setPristine(false);\n setT({ k: 1, x: (box.w - size.w) / 2, y: (box.h - size.h) / 2 });\n };\n const pan = (dx, dy) => {\n setPristine(false);\n setT((cur) => ({ ...cur, x: cur.x + dx, y: cur.y + dy }));\n };\n\n // Pointer: drag pans; two fingers pinch; Cmd/Ctrl+wheel (and a trackpad\n // pinch) zooms at the pointer. A plain wheel scrolls the page, as it should.\n const pointers = useRef(new Map());\n const gesture = useRef(null);\n const local = (e) => {\n const r = viewport.current.getBoundingClientRect();\n return { x: e.clientX - r.left, y: e.clientY - r.top };\n };\n const onPointerDown = (e) => {\n if (e.button !== 0 || !good) return;\n viewport.current.setPointerCapture?.(e.pointerId);\n pointers.current.set(e.pointerId, local(e));\n const pts = [...pointers.current.values()];\n gesture.current = pts.length === 2 ? { pinch: Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y), t } : { from: local(e), t };\n setDragging(true);\n };\n const onPointerMove = (e) => {\n if (!pointers.current.has(e.pointerId) || !gesture.current) return;\n pointers.current.set(e.pointerId, local(e));\n const pts = [...pointers.current.values()];\n const g = gesture.current;\n setPristine(false);\n if (pts.length === 2 && g.pinch) {\n const d = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);\n const mid = { x: (pts[0].x + pts[1].x) / 2, y: (pts[0].y + pts[1].y) / 2 };\n const k = clamp(g.t.k * (d / g.pinch), MIN_K, MAX_K);\n setT({ k, x: mid.x - ((mid.x - g.t.x) * k) / g.t.k, y: mid.y - ((mid.y - g.t.y) * k) / g.t.k });\n } else if (g.from) {\n const p = local(e);\n setT({ ...g.t, x: g.t.x + p.x - g.from.x, y: g.t.y + p.y - g.from.y });\n }\n };\n const onPointerUp = (e) => {\n pointers.current.delete(e.pointerId);\n const pts = [...pointers.current.values()];\n gesture.current = pts.length === 1 ? { from: pts[0], t } : null;\n if (!pts.length) setDragging(false);\n };\n useEffect(() => {\n const el = viewport.current;\n if (!el) return;\n const onWheel = (e) => {\n if (!(e.ctrlKey || e.metaKey)) return;\n e.preventDefault();\n const r = el.getBoundingClientRect();\n zoomAt(Math.exp(-e.deltaY * 0.01), e.clientX - r.left, e.clientY - r.top, false);\n };\n el.addEventListener(\"wheel\", onWheel, { passive: false });\n return () => el.removeEventListener(\"wheel\", onWheel);\n });\n const onKeyDown = (e) => {\n const step = e.shiftKey ? 160 : 48;\n const keys = {\n \"+\": () => zoomAt(1.25),\n \"=\": () => zoomAt(1.25),\n \"-\": () => zoomAt(0.8),\n _: () => zoomAt(0.8),\n 0: fit,\n 1: actual,\n ArrowLeft: () => pan(step, 0),\n ArrowRight: () => pan(-step, 0),\n ArrowUp: () => pan(0, step),\n ArrowDown: () => pan(0, -step),\n };\n const fn = keys[e.key];\n if (!fn || e.metaKey || e.ctrlKey || e.altKey) return;\n e.preventDefault();\n fn();\n };\n\n async function copy(text, what) {\n try {\n await navigator.clipboard.writeText(text);\n toast.success(`Copied ${what}`);\n } catch {\n toast.error(\"Could not reach the clipboard\");\n }\n }\n const download = () => {\n const blob = new Blob([good], { type: \"image/svg+xml\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = `${(title ?? diagramTitle(source) ?? diagramKind(source) ?? \"diagram\").toLowerCase().replace(/[^a-z0-9]+/g, \"-\").replace(/^-|-$/g, \"\") || \"diagram\"}.svg`;\n a.click();\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n };\n\n const kind = diagramKind(source);\n const error = result && !result.ok ? result.error : null;\n const empty = !source.trim();\n const name = label ?? `${heading(source, title)}${kind && title ? `, ${kind.toLowerCase()}` : \"\"}`;\n\n return (\n <figure ref={frame} className={cx(FRAME, className)}>\n {toolbar ? (\n <figcaption className=\"flex min-h-12 items-center gap-2 border-b border-border py-1.5 ps-4 pe-1.5\">\n <div className=\"me-auto flex min-w-0 items-baseline gap-2\">\n <span className=\"truncate text-sm font-medium\">{heading(source, title)}</span>\n {kind && title ? <span className=\"truncate text-xs text-muted-foreground max-sm:hidden\">{kind}</span> : null}\n </div>\n <ToggleGroup\n size=\"sm\"\n aria-label=\"Show\"\n value={tab}\n onChange={setTab}\n items={[\n { value: \"diagram\", label: \"Diagram\" },\n { value: \"source\", label: \"Source\" },\n ]}\n />\n <Menu\n label=\"Diagram actions\"\n items={[\n { label: \"Copy Mermaid source\", icon: \"copy\", disabled: empty, onSelect: () => copy(source, \"the Mermaid source\") },\n { label: \"Copy SVG\", icon: \"code\", disabled: !good, onSelect: () => copy(good, \"the SVG\") },\n { label: \"Download SVG\", icon: \"download\", disabled: !good, onSelect: download },\n { type: \"separator\" },\n { label: \"Fit to view\", icon: \"maximize\", shortcut: \"0\", disabled: !good || tab !== \"diagram\", onSelect: fit },\n { label: \"Actual size\", icon: \"search\", shortcut: \"1\", disabled: !good || tab !== \"diagram\", onSelect: actual },\n ]}\n />\n </figcaption>\n ) : null}\n\n {tab === \"source\" ? (\n <SourceView source={source} errorLine={error?.line} height={height != null ? px(height) : canvasH} onCopy={() => copy(source, \"the Mermaid source\")} />\n ) : empty ? (\n <div className=\"p-3\">\n <Empty icon=\"git-branch\" title=\"No diagram yet\" body=\"When there is Mermaid text — a flowchart, a sequence, a timeline — it is drawn here.\" />\n </div>\n ) : (\n // raw: the canvas — a focusable surface that pans and zooms with the\n // pointer, wheel and keys; no kit part is a zoomable viewport.\n <div\n ref={viewport}\n role=\"img\"\n aria-label={name}\n aria-roledescription=\"diagram\"\n tabIndex={good ? 0 : -1}\n onKeyDown={onKeyDown}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n onPointerCancel={onPointerUp}\n onDoubleClick={(e) => {\n if (!good) return;\n const p = local(e);\n zoomAt(e.shiftKey ? 0.5 : 2, p.x, p.y);\n }}\n className={cx(\n \"relative touch-none overflow-hidden outline-none select-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset\",\n \"bg-[radial-gradient(var(--color-border)_1px,transparent_1px)] [background-size:16px_16px]\",\n good && (dragging ? \"cursor-grabbing\" : \"cursor-grab\"),\n )}\n style={{ height: canvasH }}\n >\n {good ? (\n <div\n ref={content}\n className={cx(\"absolute left-0 top-0 origin-top-left will-change-transform\", smooth && \"transition-transform duration-150 ease-out motion-reduce:transition-none\", error && \"opacity-40\")}\n style={{ transform: `translate(${t.x}px, ${t.y}px) scale(${t.k})`, visibility: size ? \"visible\" : \"hidden\" }}\n dangerouslySetInnerHTML={{ __html: good }}\n />\n ) : null}\n\n {!result && !good ? (\n <div className=\"absolute inset-0 grid place-items-center p-6\" aria-busy=\"true\">\n <Skeleton shape=\"rect\" className=\"h-full max-h-64 w-full max-w-md\" />\n <span className=\"sr-only\">Drawing the diagram</span>\n </div>\n ) : null}\n\n {error && !good ? <ErrorPanel error={error} source={source} onShow={() => setTab(\"source\")} /> : null}\n\n {error && good ? (\n <div className=\"absolute inset-x-3 top-3\" onPointerDown={(e) => e.stopPropagation()}>\n <Notice\n tone=\"warning\"\n title={`${error.message} — showing the last version that worked`}\n action={\n <Button size=\"sm\" variant=\"quiet\" onClick={() => setTab(\"source\")}>\n Show source\n </Button>\n }\n >\n {error.detail}\n </Notice>\n </div>\n ) : null}\n\n {good ? (\n // raw: a floating cluster (the map-style zoom pill); its buttons are kit parts.\n <div\n className=\"absolute bottom-3 right-3 flex items-center gap-0.5 rounded-md border border-border bg-popover p-0.5 text-popover-foreground shadow-sm\"\n onPointerDown={(e) => e.stopPropagation()}\n onDoubleClick={(e) => e.stopPropagation()}\n >\n <IconButton icon=\"minus\" size=\"sm\" label=\"Zoom out\" onClick={() => zoomAt(0.8)} disabled={t.k <= MIN_K} />\n <Button variant=\"quiet\" size=\"sm\" onClick={actual} className=\"tabular-nums max-sm:hidden\" title=\"Actual size (1)\">\n {Math.round(t.k * 100)}%\n </Button>\n <IconButton icon=\"plus\" size=\"sm\" label=\"Zoom in\" onClick={() => zoomAt(1.25)} disabled={t.k >= MAX_K} />\n <IconButton icon=\"maximize\" size=\"sm\" label=\"Fit to view (0)\" onClick={fit} />\n </div>\n ) : null}\n </div>\n )}\n </figure>\n );\n}\n\n/** A mistake with nothing drawn yet: what, where, and the lines around it. */\nfunction ErrorPanel({ error, source, onShow }) {\n const lines = source.split(\"\\n\");\n const at = error.line ?? null;\n const from = at ? Math.max(1, at - 2) : 1;\n const to = at ? Math.min(lines.length, at + 2) : Math.min(lines.length, 5);\n return (\n <div className=\"absolute inset-0 overflow-auto p-4\" onPointerDown={(e) => e.stopPropagation()}>\n <div role=\"alert\" className=\"mx-auto grid max-w-xl gap-3\">\n <div className=\"flex items-start gap-3\">\n <Icon name=\"warning\" className=\"mt-0.5 shrink-0 text-destructive\" />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium\">{error.message}</p>\n {error.detail ? <p className=\"mt-1 text-sm text-muted-foreground\">{error.detail}</p> : null}\n </div>\n </div>\n <pre className=\"overflow-x-auto rounded-md border border-border bg-background py-2 font-mono text-xs leading-relaxed\">\n {lines.slice(from - 1, to).map((l, i) => {\n const n = from + i;\n const bad = n === at;\n return (\n <div key={n} className={cx(\"flex pe-3\", bad && \"bg-destructive/10\")}>\n <span className={cx(\"w-10 shrink-0 pe-3 text-right tabular-nums select-none\", bad ? \"text-destructive\" : \"text-muted-foreground\")}>{n}</span>\n <span className=\"whitespace-pre\">{l || \" \"}</span>\n </div>\n );\n })}\n </pre>\n <div>\n <Button size=\"sm\" icon=\"code\" onClick={onShow}>\n Show source\n </Button>\n </div>\n </div>\n </div>\n );\n}\n\n/** The Mermaid text, numbered, the mistake's line marked. */\nfunction SourceView({ source, errorLine, height, onCopy }) {\n const lines = source.split(\"\\n\");\n return (\n <div className=\"relative\">\n <pre className=\"overflow-auto py-3 font-mono text-xs leading-relaxed\" style={{ height }} tabIndex={0} aria-label=\"Mermaid source\">\n {lines.map((l, i) => (\n <div key={i} className={cx(\"flex pe-12\", errorLine === i + 1 && \"bg-destructive/10\")}>\n <span className={cx(\"w-12 shrink-0 pe-3 text-right tabular-nums select-none\", errorLine === i + 1 ? \"text-destructive\" : \"text-muted-foreground\")}>{i + 1}</span>\n <span className=\"whitespace-pre\">{l || \" \"}</span>\n </div>\n ))}\n </pre>\n <IconButton icon=\"copy\" size=\"sm\" label=\"Copy source\" onClick={onCopy} className=\"absolute right-2 top-2\" />\n </div>\n );\n}\n"
25
+ },
26
+ {
27
+ "path": "registry/blocks/diagram/mermaid.js",
28
+ "type": "registry:component",
29
+ "target": "@components/makersclaw/blocks/diagram/mermaid.js",
30
+ "content": "// The one door to Mermaid (MIT). Every file in this block reaches it\n// through here; a block built on this one renders through <Diagram>.\n//\n// Mermaid is one object for the whole page and its settings are global,\n// so renders go through a queue: each one sets the theme it was asked\n// for and draws, and the next waits. Mermaid loads on the server too (it\n// touches no browser global while it loads — checked in node); it only\n// ever renders in the browser, after the mount guard.\nimport mermaid from \"mermaid\";\nimport { describeError } from \"./source\";\n\nlet queue = Promise.resolve();\nlet seq = 0;\n\n/**\n * Draw Mermaid text. Resolves { ok: true, svg } or { ok: false, error:\n * { line, message, detail } } — never rejects.\n * themeVariables from mermaidTheme (palette.js)\n * look \"classic\" (default) · \"neo\" · \"handDrawn\"\n */\nexport function renderMermaid(source, { themeVariables = {}, look = \"classic\", fontFamily } = {}) {\n const job = queue.then(async () => {\n const id = `mc-diagram-${Date.now().toString(36)}-${++seq}`;\n try {\n mermaid.initialize({\n startOnLoad: false,\n securityLevel: \"strict\",\n theme: \"base\",\n look,\n themeVariables,\n fontFamily: fontFamily || themeVariables.fontFamily,\n suppressErrorRendering: true,\n htmlLabels: true,\n flowchart: { curve: \"basis\", padding: 12 },\n sequence: { mirrorActors: false },\n });\n await mermaid.parse(source);\n const { svg } = await mermaid.render(id, source);\n return { ok: true, svg };\n } catch (e) {\n return { ok: false, error: describeError(e, source) };\n } finally {\n // A failed render can leave its scratch element in the body.\n if (typeof document !== \"undefined\") {\n document.getElementById(`d${id}`)?.remove();\n document.getElementById(id)?.remove();\n }\n }\n });\n queue = job.catch(() => {});\n return job;\n}\n"
31
+ },
32
+ {
33
+ "path": "registry/blocks/diagram/palette.js",
34
+ "type": "registry:component",
35
+ "target": "@components/makersclaw/blocks/diagram/palette.js",
36
+ "content": "// The diagram's colours are the workspace's tokens.\n//\n// Mermaid draws with concrete colours and derives shades from them, so it\n// cannot read var(--color-primary). At run time the block reads each token\n// from the page (resolvePalette, the browser half at the bottom), and\n// mermaidTheme turns that palette into Mermaid's `themeVariables`: node\n// fills are soft tints of the primary and the chart hues on the card,\n// lines are muted-foreground, text is the foreground, notes a warning tint,\n// and the pie / git / mind-map colours are the chart hues. No colour value\n// is written in this file: every one comes from the page.\n//\n// Everything but resolvePalette() is a plain function of plain objects,\n// so it runs in node and on the server.\n\n/** The tokens read from the page. */\nexport const SLOTS = [\n \"background\",\n \"foreground\",\n \"card\",\n \"card-foreground\",\n \"muted\",\n \"muted-foreground\",\n \"border\",\n \"primary\",\n \"primary-foreground\",\n \"accent\",\n \"destructive\",\n \"success\",\n \"warning\",\n \"info\",\n \"chart-1\",\n \"chart-2\",\n \"chart-3\",\n \"chart-4\",\n \"chart-5\",\n];\n\nconst HEXISH = /^#?([0-9a-f]{6})([0-9a-f]{2})?$/i;\n\n/** \"#rrggbb\" → [r, g, b], or null. */\nexport function parseHex(value) {\n const m = HEXISH.exec(String(value ?? \"\").trim());\n if (!m) return null;\n const n = parseInt(m[1], 16);\n return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\nconst byte = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, \"0\");\nexport const toHex = ([r, g, b]) => \"#\" + byte(r) + byte(g) + byte(b);\n\n/** `t` of colour a over (1 - t) of colour b, both hex. */\nexport function mix(a, b, t) {\n const x = parseHex(a);\n const y = parseHex(b);\n if (!x || !y) return a ?? b ?? null;\n const k = Math.max(0, Math.min(1, t));\n return toHex([0, 1, 2].map((i) => x[i] * k + y[i] * (1 - k)));\n}\n\n/** Relative luminance, 0 (black) … 1 (white). */\nexport function luminance(hex) {\n const c = parseHex(hex);\n if (!c) return 0;\n const [r, g, b] = c.map((v) => {\n const s = v / 255;\n return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n });\n return 0.2126 * r + 0.7152 * g + 0.0722 * b;\n}\n\nconst contrast = (a, b) => {\n const [x, y] = [luminance(a), luminance(b)].sort((p, q) => q - p);\n return (x + 0.05) / (y + 0.05);\n};\n\n/** Of the theme's own ink and paper, whichever reads on `fill`. */\nexport function readableOn(fill, palette) {\n const ink = palette.foreground;\n const paper = palette.background;\n if (!ink || !paper) return ink ?? paper;\n return contrast(fill, ink) >= contrast(fill, paper) ? ink : paper;\n}\n\n/** A stable fingerprint: equal strings, nothing to redraw. */\nexport const paletteKey = (p) =>\n Object.keys(p ?? {})\n .sort()\n .map((k) => `${k}=${p[k]}`)\n .join(\";\");\n\n/**\n * Mermaid's `themeVariables` (theme \"base\") from a palette of hex colours.\n * An empty palette gives {} — Mermaid's own base look — so a page without\n * the tokens still draws.\n */\nexport function mermaidTheme(palette, { fontFamily } = {}) {\n const p = palette ?? {};\n if (!p.foreground || !p.card) return fontFamily ? { fontFamily } : {};\n const card = p.card;\n const fg = p[\"card-foreground\"] ?? p.foreground;\n const soft = (slot, t = 0.16) => mix(p[slot] ?? p.primary ?? fg, card, t);\n const edge = (slot, t = 0.6) => mix(p[slot] ?? p.primary ?? fg, card, t);\n const dark = luminance(card) < 0.4;\n const hues = [\"chart-1\", \"chart-2\", \"chart-3\", \"chart-4\", \"chart-5\"].map((s) => p[s] ?? p.primary ?? fg);\n const hueAt = (i) => hues[i % hues.length];\n const softAt = (i) => mix(hueAt(i), card, i < 5 ? 0.22 : 0.4);\n const out = {\n darkMode: dark,\n background: card,\n fontFamily: fontFamily || undefined,\n fontSize: \"14px\",\n useGradient: false,\n dropShadow: \"none\",\n\n primaryColor: soft(\"primary\"),\n primaryTextColor: fg,\n primaryBorderColor: edge(\"primary\"),\n secondaryColor: soft(\"chart-2\"),\n secondaryTextColor: fg,\n secondaryBorderColor: edge(\"chart-2\"),\n tertiaryColor: p.muted ?? soft(\"primary\", 0.08),\n tertiaryTextColor: fg,\n tertiaryBorderColor: p.border ?? edge(\"primary\", 0.3),\n lineColor: p[\"muted-foreground\"] ?? fg,\n arrowheadColor: p[\"muted-foreground\"] ?? fg,\n textColor: fg,\n titleColor: fg,\n mainBkg: soft(\"primary\"),\n nodeBkg: soft(\"primary\"),\n nodeBorder: edge(\"primary\"),\n nodeTextColor: fg,\n clusterBkg: p.muted ?? card,\n clusterBorder: p.border ?? edge(\"primary\", 0.3),\n defaultLinkColor: p[\"muted-foreground\"] ?? fg,\n edgeLabelBackground: card,\n labelBackgroundColor: card,\n\n noteBkgColor: soft(\"warning\", 0.16),\n noteTextColor: fg,\n noteBorderColor: edge(\"warning\", 0.5),\n\n // sequence\n actorBkg: soft(\"primary\"),\n actorBorder: edge(\"primary\"),\n actorTextColor: fg,\n actorLineColor: p[\"muted-foreground\"] ?? fg,\n signalColor: fg,\n signalTextColor: fg,\n labelBoxBkgColor: p.muted ?? card,\n labelBoxBorderColor: p.border ?? fg,\n labelTextColor: fg,\n loopTextColor: fg,\n activationBkgColor: p.muted ?? card,\n activationBorderColor: p.border ?? fg,\n sequenceNumberColor: readableOn(p[\"muted-foreground\"] ?? fg, p),\n\n // gantt\n sectionBkgColor: p.muted ?? card,\n altSectionBkgColor: card,\n sectionBkgColor2: p.muted ?? card,\n gridColor: p.border ?? fg,\n taskBkgColor: soft(\"primary\", 0.3),\n taskBorderColor: edge(\"primary\"),\n taskTextColor: fg,\n taskTextLightColor: fg,\n taskTextDarkColor: fg,\n taskTextOutsideColor: fg,\n taskTextClickableColor: p.primary ?? fg,\n activeTaskBkgColor: soft(\"chart-2\", 0.3),\n activeTaskBorderColor: edge(\"chart-2\"),\n doneTaskBkgColor: p.muted ?? card,\n doneTaskBorderColor: p.border ?? fg,\n critBkgColor: soft(\"destructive\", 0.24),\n critBorderColor: p.destructive ?? fg,\n todayLineColor: p.destructive ?? fg,\n vertLineColor: p[\"muted-foreground\"] ?? fg,\n excludeBkgColor: p.muted ?? card,\n\n // state, class, er\n stateBkg: soft(\"primary\"),\n stateLabelColor: fg,\n compositeBackground: p.muted ?? card,\n compositeTitleBackground: p.muted ?? card,\n compositeBorder: p.border ?? fg,\n altBackground: p.muted ?? card,\n innerEndBackground: fg,\n specialStateColor: fg,\n transitionColor: p[\"muted-foreground\"] ?? fg,\n transitionLabelColor: fg,\n classText: fg,\n rowOdd: card,\n rowEven: p.muted ?? card,\n relationColor: p[\"muted-foreground\"] ?? fg,\n relationLabelBackground: card,\n relationLabelColor: fg,\n requirementBackground: soft(\"primary\"),\n requirementBorderColor: edge(\"primary\"),\n requirementTextColor: fg,\n\n errorBkgColor: soft(\"destructive\", 0.2),\n errorTextColor: p.destructive ?? fg,\n\n // pie\n pieTitleTextColor: fg,\n pieSectionTextColor: readableOn(hueAt(1), p),\n pieLegendTextColor: fg,\n pieStrokeColor: card,\n pieOuterStrokeColor: p.border ?? card,\n pieOpacity: \"1\",\n\n // quadrant\n quadrant1Fill: softAt(0),\n quadrant2Fill: softAt(1),\n quadrant3Fill: softAt(2),\n quadrant4Fill: softAt(3),\n quadrant1TextFill: fg,\n quadrant2TextFill: fg,\n quadrant3TextFill: fg,\n quadrant4TextFill: fg,\n quadrantPointFill: p.primary ?? fg,\n quadrantPointTextFill: fg,\n quadrantXAxisTextFill: fg,\n quadrantYAxisTextFill: fg,\n quadrantTitleFill: fg,\n quadrantInternalBorderStrokeFill: p.border ?? fg,\n quadrantExternalBorderStrokeFill: p.border ?? fg,\n\n // xy chart\n xyChart: {\n backgroundColor: card,\n titleColor: fg,\n xAxisLabelColor: fg,\n xAxisTitleColor: fg,\n xAxisTickColor: p[\"muted-foreground\"] ?? fg,\n xAxisLineColor: p.border ?? fg,\n yAxisLabelColor: fg,\n yAxisTitleColor: fg,\n yAxisTickColor: p[\"muted-foreground\"] ?? fg,\n yAxisLineColor: p.border ?? fg,\n plotColorPalette: hues.join(\",\"),\n },\n scaleLabelColor: fg,\n commitLabelColor: fg,\n commitLabelBackground: p.muted ?? card,\n tagLabelColor: fg,\n tagLabelBackground: p.muted ?? card,\n tagLabelBorder: p.border ?? fg,\n };\n for (let i = 0; i < 12; i++) {\n out[`pie${i + 1}`] = i < 5 ? hueAt(i) : mix(hueAt(i), card, 0.55);\n out[`cScale${i}`] = softAt(i);\n out[`cScaleLabel${i}`] = fg;\n }\n for (let i = 0; i < 8; i++) {\n out[`git${i}`] = hueAt(i);\n out[`gitInv${i}`] = readableOn(hueAt(i), p);\n out[`gitBranchLabel${i}`] = readableOn(hueAt(i), p);\n }\n for (let i = 0; i < 8; i++) out[`fillType${i}`] = softAt(i);\n return out;\n}\n\n// ---- the browser half -------------------------------------------------------\n\n/** Read every slot from the CSS variables in effect at `el` (so a theme\n * scoped to an ancestor counts). A probe takes `color: var(--color-…)`;\n * the browser resolves var(), light-dark(), mixes and relative colours\n * into its computed colour, and a 1×1 canvas turns that into bytes\n * whatever syntax it came back in. */\nexport function resolvePalette(el) {\n if (typeof document === \"undefined\" || !el) return {};\n const probe = document.createElement(\"span\");\n probe.setAttribute(\"aria-hidden\", \"true\");\n probe.style.position = \"absolute\";\n probe.style.visibility = \"hidden\";\n probe.style.pointerEvents = \"none\";\n el.appendChild(probe);\n const canvas = document.createElement(\"canvas\");\n canvas.width = 1;\n canvas.height = 1;\n const ctx = canvas.getContext?.(\"2d\", { willReadFrequently: true });\n const out = {};\n // The card first: a see-through token (the border is 15% ink) is laid\n // over it, so what Mermaid gets is the colour as it looks on the card.\n const order = [\"card\", ...SLOTS.filter((s) => s !== \"card\")];\n try {\n for (const slot of order) {\n probe.style.color = \"\";\n probe.style.color = `var(--color-${slot})`;\n const computed = getComputedStyle(probe).color;\n if (!computed) continue;\n if (!ctx) {\n const hex = parseHex(computed);\n if (hex) out[slot] = toHex(hex);\n continue;\n }\n ctx.clearRect(0, 0, 1, 1);\n if (out.card) {\n ctx.fillStyle = out.card;\n ctx.fillRect(0, 0, 1, 1);\n }\n ctx.fillStyle = \"transparent\";\n ctx.fillStyle = computed;\n ctx.fillRect(0, 0, 1, 1);\n const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;\n if (a === 0) continue;\n out[slot] = toHex([r, g, b]);\n }\n } finally {\n probe.remove();\n }\n return out;\n}\n"
37
+ },
38
+ {
39
+ "path": "registry/blocks/diagram/source.js",
40
+ "type": "registry:component",
41
+ "target": "@components/makersclaw/blocks/diagram/source.js",
42
+ "content": "// Reading Mermaid text: what kind of diagram it is, and what went wrong.\n// Plain functions of strings, so they run in node and on the server.\n\nconst KINDS = [\n [/^flowchart\\b|^graph\\b/, \"Flowchart\"],\n [/^sequenceDiagram\\b/, \"Sequence diagram\"],\n [/^classDiagram(?:-v2)?\\b/, \"Class diagram\"],\n [/^stateDiagram(?:-v2)?\\b/, \"State diagram\"],\n [/^erDiagram\\b/, \"Entity relationships\"],\n [/^journey\\b/, \"User journey\"],\n [/^gantt\\b/, \"Gantt chart\"],\n [/^pie\\b/, \"Pie chart\"],\n [/^quadrantChart\\b/, \"Quadrant chart\"],\n [/^requirementDiagram\\b/, \"Requirements\"],\n [/^gitGraph\\b/, \"Git graph\"],\n [/^mindmap\\b/, \"Mind map\"],\n [/^timeline\\b/, \"Timeline\"],\n [/^sankey(?:-beta)?\\b/, \"Sankey diagram\"],\n [/^xychart(?:-beta)?\\b/, \"XY chart\"],\n [/^block(?:-beta)?\\b/, \"Block diagram\"],\n [/^packet(?:-beta)?\\b/, \"Packet diagram\"],\n [/^kanban\\b/, \"Kanban\"],\n [/^architecture(?:-beta)?\\b/, \"Architecture\"],\n [/^radar(?:-beta)?\\b/, \"Radar chart\"],\n [/^C4\\w*/, \"C4 diagram\"],\n];\n\n/** The first line that says something: past blank lines, %% comments and\n * a --- front-matter block. */\nexport function firstStatement(source) {\n const lines = String(source ?? \"\").split(\"\\n\");\n let i = 0;\n if (lines[0]?.trim() === \"---\") {\n i = 1;\n while (i < lines.length && lines[i].trim() !== \"---\") i++;\n i++;\n }\n for (; i < lines.length; i++) {\n const t = lines[i].trim();\n if (t && !t.startsWith(\"%%\")) return { line: i + 1, text: t };\n }\n return null;\n}\n\n/** \"Flowchart\", \"Sequence diagram\"… or null when the text names no kind. */\nexport function diagramKind(source) {\n const first = firstStatement(source);\n if (!first) return null;\n for (const [re, name] of KINDS) if (re.test(first.text)) return name;\n return null;\n}\n\n/** The diagram's own title (`title …` in front matter or a line), or null. */\nexport function diagramTitle(source) {\n const m = /^\\s*(?:title\\s*:?\\s*)(.+)$/m.exec(String(source ?? \"\"));\n return m ? m[1].trim().replace(/^[\"']|[\"']$/g, \"\") : null;\n}\n\n/**\n * What a Mermaid error means, for people: { line, message, detail }.\n * `line` is 1-based in the source, or null when Mermaid did not say.\n * `message` is ours (\"Line 3 has a mistake\"), `detail` Mermaid's own\n * words, trimmed of the ASCII art pointer.\n */\nexport function describeError(error, source) {\n const raw = String(error?.message ?? error?.str ?? error ?? \"\").trim();\n const hash = error?.hash;\n let line = null;\n if (Number.isFinite(hash?.loc?.first_line)) line = hash.loc.first_line;\n else if (Number.isFinite(hash?.line)) line = hash.line + 1;\n else {\n const m = /\\bline[:\\s]+(\\d+)/i.exec(raw);\n if (m) line = Number(m[1]);\n }\n const lines = String(source ?? \"\").split(\"\\n\");\n if (line != null) line = Math.max(1, Math.min(lines.length, line));\n\n if (!firstStatement(source)) return { line: null, message: \"There is no diagram here yet\", detail: \"Start with a type on the first line: flowchart LR, sequenceDiagram, gantt, pie…\" };\n if (/No diagram type detected|UnknownDiagramError/i.test(raw) || (!diagramKind(source) && !line)) {\n const first = firstStatement(source);\n return {\n line: first.line,\n message: \"The first line should name the diagram type\",\n detail: `\"${first.text.slice(0, 40)}\" is not one Mermaid knows. Try flowchart LR, sequenceDiagram, classDiagram, stateDiagram-v2, erDiagram, gantt, pie, mindmap or timeline.`,\n };\n }\n const detail = raw\n .split(\"\\n\")\n .filter((l) => !/^\\s*-*\\^\\s*$/.test(l) && !/^\\.\\.\\./.test(l.trim()))\n .filter((l) => !/^Parse error on line \\d+:?$/i.test(l.trim()))\n .join(\" \")\n .replace(/\\s+/g, \" \")\n .replace(/^Error:\\s*/, \"\")\n .replace(/^Parsing failed:\\s*/, \"\")\n .replace(/ at offset: \\d+, skipped \\d+ characters?\\./g, \".\")\n .trim();\n return { line, message: line ? `Line ${line} has a mistake` : \"Mermaid could not read this diagram\", detail: detail || null };\n}\n"
43
+ },
44
+ {
45
+ "path": "registry/blocks/diagram/README.md",
46
+ "type": "registry:file",
47
+ "target": "@components/makersclaw/blocks/diagram/README.md",
48
+ "content": "# Diagram\n\nDraws a diagram from [Mermaid](https://mermaid.js.org) text — flowcharts,\nsequence diagrams, class and state diagrams, entity relationships, Gantt\ncharts, pies, mind maps, timelines, git graphs and the rest — in the\nworkspace theme. Nodes are soft tints of the primary and the chart hues on\nthe card, lines are muted, text is the theme's own face and ink, and it\nfollows light and dark.\n\nIt opens fitted to its frame. Zoom with the pill in the corner, with\nCmd/Ctrl+scroll (or a trackpad pinch) at the pointer, with two fingers on\na phone, or by double-clicking (Shift to zoom out); drag to move. A plain\nscroll still scrolls the page. The Source tab shows the text with line\nnumbers; the ⋯ menu copies the source or the SVG and downloads the SVG.\n\nA mistake says which line and what Mermaid expected, and shows the lines\naround it. When an earlier version drew, that drawing stays up (dimmed)\nunder the message, so a diagram being written live never blanks out.\n\nIt is Mermaid (MIT) underneath. Mermaid needs concrete colours, so the\nblock reads each token from the page at run time (`--color-card`,\n`--color-primary`, `--color-chart-1…5`, `--color-muted-foreground`…),\nmixes the tints, and hands them to Mermaid as its theme; when the\nworkspace theme or light/dark changes it reads them again and redraws.\n\n```jsx\nimport { Diagram } from \"../components/blocks/diagram\"; // a Makersclaw app (\"./blocks/diagram\" from components/)\nimport { Diagram } from \"@/components/makersclaw/blocks/diagram\"; // a shadcn project\n```\n\n`Diagram` is a client component: render it from a `\"use client\"`\ncomponent. `makersclaw add diagram` adds `mermaid` (pinned, exact) to\npackage.json: run `npm install` after. It loads with the page (no\n`import()`); the drawing happens after the first render, and until then —\non the server — the frame shows the Mermaid text.\n\n## Props\n\n- `source` — the Mermaid text. Required (an empty string shows \"No\n diagram yet\"). Change it and the diagram redraws a moment after the\n last change, so it can follow someone typing.\n- `title` — the frame's heading. Without it: the diagram's own `title`\n line, else its kind (\"Flowchart\", \"Sequence diagram\"…).\n- `label` — what a screen reader calls the diagram (default: the title\n and kind). Say what it shows: \"Order flow from cart to delivery\".\n- `height` — a fixed canvas height (px number or CSS length). Without it\n the canvas is as tall as the drawing at the frame's width, between\n `minHeight` (160) and `maxHeight` (560).\n- `look` — `\"classic\"` (default), `\"neo\"` (Mermaid's newer, lighter\n shapes) or `\"handDrawn\"` (a sketchy line, for early ideas).\n- `toolbar` — `true` (default): the heading, Diagram/Source and the ⋯\n menu. `false` leaves the canvas and its zoom pill only.\n- `onRender(result)` — after each drawing: `{ ok: true, svg }` or\n `{ ok: false, error: { line, message, detail } }`. Pair it with a\n `code-editor` to mark the line: `diagnostics={[{ line, message }]}`.\n- `className` — the frame's classes.\n\n### Keys\n\nOn the focused canvas: `+` / `-` zoom · `0` fit · `1` actual size ·\narrows move (Shift for bigger steps).\n\n## The helpers\n\nPlain functions, on the server too.\n\n- `diagramKind(source)` → `\"Flowchart\"`, `\"Gantt chart\"`… or null.\n- `diagramTitle(source)` → its `title`, or null.\n- `describeError(error, source)` → `{ line, message, detail }` for a\n Mermaid error.\n- `mermaidTheme(palette, { fontFamily })` → Mermaid `themeVariables`\n from `{ card, foreground, primary, \"chart-1\"…, … }` hex colours.\n\n## Example\n\n```jsx\n\"use client\";\nimport { Diagram } from \"../components/blocks/diagram\";\n\nconst ONBOARDING = `flowchart LR\n signup([Sign-up]) --> verify{Email verified?}\n verify -- yes --> workspace[Create workspace]\n verify -- no --> remind[Send reminder] --> verify\n workspace --> invite[Invite team] --> done([First project])`;\n\nexport function OnboardingFlow() {\n return <Diagram title=\"Onboarding\" source={ONBOARDING} label=\"Onboarding flow from sign-up to first project\" />;\n}\n```\n\nA live editor, with the mistake marked in the text:\n\n```jsx\nconst [text, setText] = useState(ONBOARDING);\nconst [problem, setProblem] = useState([]);\n<CodeEditor value={text} onChange={setText} diagnostics={problem} />\n<Diagram source={text} onRender={(r) => setProblem(r.ok || !r.error.line ? [] : [{ line: r.error.line, message: r.error.detail ?? r.error.message }])} />\n```\n\n## What it will not do\n\n- It draws; it does not edit. People change a diagram by changing its\n text (put a `code-editor` beside it).\n- No clicks or links inside the drawing: Mermaid runs in its strict\n mode, so `click` lines and HTML in labels are dropped.\n- Colours in the text (`style`, `classDef … fill:#…`) are Mermaid's,\n not the theme's; leave them out and the theme paints everything.\n- PNG export is not built in; Download SVG is.\n"
49
+ }
50
+ ],
51
+ "categories": [
52
+ "diagram",
53
+ "mermaid",
54
+ "flowchart",
55
+ "sequence diagram",
56
+ "er diagram",
57
+ "gantt",
58
+ "mind map",
59
+ "timeline",
60
+ "state diagram",
61
+ "class diagram",
62
+ "svg",
63
+ "zoom",
64
+ "docs",
65
+ "architecture"
66
+ ],
67
+ "docs": "Installed to components/makersclaw/blocks/diagram. Import: import { Diagram, diagramKind, diagramTitle, describeError, mermaidTheme } from \"@/components/makersclaw/blocks/diagram\". Props and an example: components/makersclaw/blocks/diagram/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
68
+ "meta": {
69
+ "makersclaw": {
70
+ "version": "0.3.0",
71
+ "useWhen": "A screen shows a diagram written as text: a process or decision flow an agent wrote, a sequence of API calls, a data model, a project timeline, an org or system sketch in docs, a mind map. Not for a board people arrange by hand (flow-canvas), freehand drawing (sketch-canvas), or charts of numbers (chart)."
72
+ }
73
+ }
74
+ }