makersclaw 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (475) hide show
  1. package/THIRD-PARTY-NOTICES.md +180 -0
  2. package/package.json +3 -2
  3. package/r/annotate.json +62 -0
  4. package/r/approval-request.json +52 -0
  5. package/r/audio-player.json +74 -0
  6. package/r/auth-form.json +55 -0
  7. package/r/board.json +10 -6
  8. package/r/calendar-view.json +105 -0
  9. package/r/cart.json +62 -0
  10. package/r/chart.json +83 -0
  11. package/r/chat.json +95 -0
  12. package/r/code-editor.json +94 -0
  13. package/r/comment-box.json +64 -0
  14. package/r/contact-section.json +53 -0
  15. package/r/core.json +205 -8
  16. package/r/csv-import.json +63 -0
  17. package/r/data-table.json +69 -0
  18. package/r/diagram.json +74 -0
  19. package/r/doc-editor.json +82 -0
  20. package/r/doc-page.json +61 -0
  21. package/r/email-render.json +73 -0
  22. package/r/faq.json +50 -0
  23. package/r/file-upload.json +54 -0
  24. package/r/flow-canvas.json +93 -0
  25. package/r/footer.json +52 -0
  26. package/r/gantt.json +70 -0
  27. package/r/job-panel.json +12 -7
  28. package/r/json-editor.json +66 -0
  29. package/r/map.json +89 -0
  30. package/r/menubar.json +52 -0
  31. package/r/mind-map.json +67 -0
  32. package/r/navigation-menu.json +52 -0
  33. package/r/order-detail.json +54 -0
  34. package/r/org-chart.json +66 -0
  35. package/r/page-builder.json +105 -0
  36. package/r/payment-methods.json +51 -0
  37. package/r/pdf-document.json +79 -0
  38. package/r/pdf-viewer.json +88 -0
  39. package/r/product-detail.json +76 -0
  40. package/r/product-grid.json +65 -0
  41. package/r/record-form.json +67 -0
  42. package/r/record-notes.json +55 -0
  43. package/r/registry.json +3411 -29
  44. package/r/search-dialog.json +51 -0
  45. package/r/settings-list.json +66 -0
  46. package/r/setup-checklist.json +51 -0
  47. package/r/share-dialog.json +52 -0
  48. package/r/sidebar.json +69 -0
  49. package/r/signature-pad.json +72 -0
  50. package/r/sketch-canvas.json +99 -0
  51. package/r/spreadsheet.json +90 -0
  52. package/r/stat-cards.json +66 -0
  53. package/r/task-list.json +63 -0
  54. package/r/task-model.json +98 -0
  55. package/r/testimonials.json +52 -0
  56. package/r/transaction-list.json +53 -0
  57. package/r/whiteboard.json +66 -0
  58. package/registry/blocks/annotate/README.md +179 -0
  59. package/registry/blocks/annotate/annotate-data.js +250 -0
  60. package/registry/blocks/annotate/annotate.jsx +646 -0
  61. package/registry/blocks/annotate/block.json +10 -0
  62. package/registry/blocks/annotate/demo.jsx +135 -0
  63. package/registry/blocks/annotate/index.js +15 -0
  64. package/registry/blocks/approval-request/README.md +116 -0
  65. package/registry/blocks/approval-request/approval-request.jsx +386 -0
  66. package/registry/blocks/approval-request/block.json +10 -0
  67. package/registry/blocks/approval-request/demo.jsx +127 -0
  68. package/registry/blocks/approval-request/index.js +4 -0
  69. package/registry/blocks/audio-player/README.md +167 -0
  70. package/registry/blocks/audio-player/audio-player.jsx +623 -0
  71. package/registry/blocks/audio-player/block.json +14 -0
  72. package/registry/blocks/audio-player/colors.js +109 -0
  73. package/registry/blocks/audio-player/demo.jsx +209 -0
  74. package/registry/blocks/audio-player/index.js +6 -0
  75. package/registry/blocks/audio-player/time.js +67 -0
  76. package/registry/blocks/audio-player/wavesurfer.js +9 -0
  77. package/registry/blocks/auth-form/README.md +126 -0
  78. package/registry/blocks/auth-form/auth-form.jsx +568 -0
  79. package/registry/blocks/auth-form/block.json +10 -0
  80. package/registry/blocks/auth-form/demo.jsx +131 -0
  81. package/registry/blocks/auth-form/index.js +3 -0
  82. package/registry/blocks/board/README.md +55 -5
  83. package/registry/blocks/board/block.json +2 -2
  84. package/registry/blocks/board/board.jsx +230 -86
  85. package/registry/blocks/board/demo.jsx +103 -24
  86. package/registry/blocks/calendar-view/README.md +180 -0
  87. package/registry/blocks/calendar-view/agenda.jsx +147 -0
  88. package/registry/blocks/calendar-view/block.json +76 -0
  89. package/registry/blocks/calendar-view/calendar-model.js +442 -0
  90. package/registry/blocks/calendar-view/calendar.jsx +627 -0
  91. package/registry/blocks/calendar-view/demo.jsx +195 -0
  92. package/registry/blocks/calendar-view/draft.jsx +49 -0
  93. package/registry/blocks/calendar-view/index.js +39 -0
  94. package/registry/blocks/calendar-view/month.jsx +346 -0
  95. package/registry/blocks/calendar-view/paint.js +69 -0
  96. package/registry/blocks/calendar-view/time-grid.jsx +527 -0
  97. package/registry/blocks/calendar-view/tray.jsx +87 -0
  98. package/registry/blocks/cart/README.md +146 -0
  99. package/registry/blocks/cart/block.json +10 -0
  100. package/registry/blocks/cart/cart.jsx +597 -0
  101. package/registry/blocks/cart/demo.jsx +98 -0
  102. package/registry/blocks/cart/index.js +4 -0
  103. package/registry/blocks/cart/money.js +28 -0
  104. package/registry/blocks/chart/README.md +316 -0
  105. package/registry/blocks/chart/block.json +10 -0
  106. package/registry/blocks/chart/cartesian.jsx +642 -0
  107. package/registry/blocks/chart/core.jsx +262 -0
  108. package/registry/blocks/chart/demo.jsx +357 -0
  109. package/registry/blocks/chart/heatmap.jsx +336 -0
  110. package/registry/blocks/chart/index.js +8 -0
  111. package/registry/blocks/chart/pie.jsx +245 -0
  112. package/registry/blocks/chart/sparkline.jsx +88 -0
  113. package/registry/blocks/chat/README.md +273 -0
  114. package/registry/blocks/chat/attachment.jsx +95 -0
  115. package/registry/blocks/chat/block.json +10 -0
  116. package/registry/blocks/chat/chat.jsx +132 -0
  117. package/registry/blocks/chat/composer.jsx +364 -0
  118. package/registry/blocks/chat/demo.jsx +578 -0
  119. package/registry/blocks/chat/header.jsx +34 -0
  120. package/registry/blocks/chat/index.js +9 -0
  121. package/registry/blocks/chat/message.jsx +450 -0
  122. package/registry/blocks/chat/scroller.jsx +247 -0
  123. package/registry/blocks/chat/text.jsx +104 -0
  124. package/registry/blocks/code-editor/README.md +210 -0
  125. package/registry/blocks/code-editor/block.json +22 -0
  126. package/registry/blocks/code-editor/code-editor.jsx +534 -0
  127. package/registry/blocks/code-editor/codemirror.js +38 -0
  128. package/registry/blocks/code-editor/demo.jsx +205 -0
  129. package/registry/blocks/code-editor/index.js +8 -0
  130. package/registry/blocks/code-editor/json.js +317 -0
  131. package/registry/blocks/code-editor/sql.js +136 -0
  132. package/registry/blocks/code-editor/text.js +101 -0
  133. package/registry/blocks/code-editor/theme.js +186 -0
  134. package/registry/blocks/comment-box/README.md +123 -0
  135. package/registry/blocks/comment-box/block.json +10 -0
  136. package/registry/blocks/comment-box/comment-body.jsx +217 -0
  137. package/registry/blocks/comment-box/comment-box.jsx +198 -0
  138. package/registry/blocks/comment-box/comment-time.jsx +77 -0
  139. package/registry/blocks/comment-box/demo.jsx +141 -0
  140. package/registry/blocks/comment-box/index.js +8 -0
  141. package/registry/blocks/contact-section/README.md +152 -0
  142. package/registry/blocks/contact-section/block.json +11 -0
  143. package/registry/blocks/contact-section/contact-section.jsx +509 -0
  144. package/registry/blocks/contact-section/demo.jsx +93 -0
  145. package/registry/blocks/contact-section/index.js +4 -0
  146. package/registry/blocks/csv-import/README.md +133 -0
  147. package/registry/blocks/csv-import/block.json +13 -0
  148. package/registry/blocks/csv-import/csv-import.jsx +479 -0
  149. package/registry/blocks/csv-import/demo.jsx +106 -0
  150. package/registry/blocks/csv-import/import.js +200 -0
  151. package/registry/blocks/csv-import/index.js +7 -0
  152. package/registry/blocks/data-table/README.md +386 -0
  153. package/registry/blocks/data-table/block.json +10 -0
  154. package/registry/blocks/data-table/data-table.jsx +1164 -0
  155. package/registry/blocks/data-table/demo.jsx +588 -0
  156. package/registry/blocks/data-table/index.js +5 -0
  157. package/registry/blocks/data-table/model.js +169 -0
  158. package/registry/blocks/diagram/README.md +108 -0
  159. package/registry/blocks/diagram/block.json +13 -0
  160. package/registry/blocks/diagram/demo.jsx +113 -0
  161. package/registry/blocks/diagram/diagram.jsx +481 -0
  162. package/registry/blocks/diagram/index.js +7 -0
  163. package/registry/blocks/diagram/mermaid.js +52 -0
  164. package/registry/blocks/diagram/palette.js +313 -0
  165. package/registry/blocks/diagram/source.js +98 -0
  166. package/registry/blocks/doc-editor/README.md +215 -0
  167. package/registry/blocks/doc-editor/block.json +15 -0
  168. package/registry/blocks/doc-editor/demo.jsx +225 -0
  169. package/registry/blocks/doc-editor/doc-editor.jsx +291 -0
  170. package/registry/blocks/doc-editor/doc.js +309 -0
  171. package/registry/blocks/doc-editor/index.js +7 -0
  172. package/registry/blocks/doc-editor/schema.jsx +56 -0
  173. package/registry/blocks/doc-editor/shadcn-classes.js +110 -0
  174. package/registry/blocks/doc-editor/theme.js +82 -0
  175. package/registry/blocks/doc-page/README.md +119 -0
  176. package/registry/blocks/doc-page/autosave.js +104 -0
  177. package/registry/blocks/doc-page/block.json +10 -0
  178. package/registry/blocks/doc-page/demo.jsx +137 -0
  179. package/registry/blocks/doc-page/doc-page.jsx +276 -0
  180. package/registry/blocks/doc-page/index.js +4 -0
  181. package/registry/blocks/email-render/README.md +151 -0
  182. package/registry/blocks/email-render/block.json +14 -0
  183. package/registry/blocks/email-render/demo.jsx +153 -0
  184. package/registry/blocks/email-render/doc-email.jsx +210 -0
  185. package/registry/blocks/email-render/email.jsx +221 -0
  186. package/registry/blocks/email-render/index.js +10 -0
  187. package/registry/blocks/email-render/render.js +50 -0
  188. package/registry/blocks/email-render/theme.js +215 -0
  189. package/registry/blocks/faq/README.md +101 -0
  190. package/registry/blocks/faq/block.json +11 -0
  191. package/registry/blocks/faq/demo.jsx +89 -0
  192. package/registry/blocks/faq/faq.jsx +220 -0
  193. package/registry/blocks/faq/index.js +4 -0
  194. package/registry/blocks/file-upload/README.md +138 -0
  195. package/registry/blocks/file-upload/block.json +10 -0
  196. package/registry/blocks/file-upload/demo.jsx +204 -0
  197. package/registry/blocks/file-upload/file-upload.jsx +635 -0
  198. package/registry/blocks/file-upload/index.js +4 -0
  199. package/registry/blocks/flow-canvas/README.md +420 -0
  200. package/registry/blocks/flow-canvas/block.json +13 -0
  201. package/registry/blocks/flow-canvas/board.js +819 -0
  202. package/registry/blocks/flow-canvas/chrome.jsx +649 -0
  203. package/registry/blocks/flow-canvas/context.jsx +35 -0
  204. package/registry/blocks/flow-canvas/demo.jsx +249 -0
  205. package/registry/blocks/flow-canvas/edges.jsx +107 -0
  206. package/registry/blocks/flow-canvas/flow-canvas.jsx +1329 -0
  207. package/registry/blocks/flow-canvas/index.js +41 -0
  208. package/registry/blocks/flow-canvas/nodes.jsx +392 -0
  209. package/registry/blocks/flow-canvas/theme.js +174 -0
  210. package/registry/blocks/footer/README.md +133 -0
  211. package/registry/blocks/footer/block.json +11 -0
  212. package/registry/blocks/footer/demo.jsx +119 -0
  213. package/registry/blocks/footer/footer.jsx +406 -0
  214. package/registry/blocks/footer/index.js +4 -0
  215. package/registry/blocks/gantt/README.md +220 -0
  216. package/registry/blocks/gantt/block.json +10 -0
  217. package/registry/blocks/gantt/demo.jsx +148 -0
  218. package/registry/blocks/gantt/gantt.jsx +923 -0
  219. package/registry/blocks/gantt/index.js +7 -0
  220. package/registry/blocks/gantt/rows.jsx +468 -0
  221. package/registry/blocks/gantt/schedule.js +278 -0
  222. package/registry/blocks/job-panel/README.md +80 -0
  223. package/registry/blocks/job-panel/block.json +3 -3
  224. package/registry/blocks/job-panel/demo.jsx +230 -3
  225. package/registry/blocks/job-panel/job-panel.jsx +169 -3
  226. package/registry/blocks/json-editor/README.md +133 -0
  227. package/registry/blocks/json-editor/block.json +10 -0
  228. package/registry/blocks/json-editor/demo.jsx +92 -0
  229. package/registry/blocks/json-editor/index.js +7 -0
  230. package/registry/blocks/json-editor/json-editor.jsx +305 -0
  231. package/registry/blocks/json-editor/json-tree.jsx +393 -0
  232. package/registry/blocks/json-editor/tree.js +225 -0
  233. package/registry/blocks/map/README.md +221 -0
  234. package/registry/blocks/map/block.json +14 -0
  235. package/registry/blocks/map/demo.jsx +201 -0
  236. package/registry/blocks/map/geo.js +198 -0
  237. package/registry/blocks/map/index.js +8 -0
  238. package/registry/blocks/map/list.jsx +128 -0
  239. package/registry/blocks/map/map-view.jsx +715 -0
  240. package/registry/blocks/map/maplibre.js +30 -0
  241. package/registry/blocks/map/marks.jsx +146 -0
  242. package/registry/blocks/map/theme.js +142 -0
  243. package/registry/blocks/menubar/README.md +109 -0
  244. package/registry/blocks/menubar/block.json +10 -0
  245. package/registry/blocks/menubar/demo.jsx +128 -0
  246. package/registry/blocks/menubar/index.js +4 -0
  247. package/registry/blocks/menubar/menubar.jsx +199 -0
  248. package/registry/blocks/mind-map/README.md +206 -0
  249. package/registry/blocks/mind-map/block.json +10 -0
  250. package/registry/blocks/mind-map/demo.jsx +103 -0
  251. package/registry/blocks/mind-map/index.js +31 -0
  252. package/registry/blocks/mind-map/mind-map.jsx +1125 -0
  253. package/registry/blocks/mind-map/topic.jsx +143 -0
  254. package/registry/blocks/mind-map/tree.js +645 -0
  255. package/registry/blocks/navigation-menu/README.md +124 -0
  256. package/registry/blocks/navigation-menu/block.json +10 -0
  257. package/registry/blocks/navigation-menu/demo.jsx +84 -0
  258. package/registry/blocks/navigation-menu/index.js +4 -0
  259. package/registry/blocks/navigation-menu/navigation-menu.jsx +448 -0
  260. package/registry/blocks/order-detail/README.md +109 -0
  261. package/registry/blocks/order-detail/block.json +10 -0
  262. package/registry/blocks/order-detail/demo.jsx +88 -0
  263. package/registry/blocks/order-detail/index.js +4 -0
  264. package/registry/blocks/order-detail/order-detail.jsx +354 -0
  265. package/registry/blocks/org-chart/README.md +145 -0
  266. package/registry/blocks/org-chart/block.json +10 -0
  267. package/registry/blocks/org-chart/demo.jsx +94 -0
  268. package/registry/blocks/org-chart/index.js +7 -0
  269. package/registry/blocks/org-chart/org-chart.jsx +631 -0
  270. package/registry/blocks/org-chart/org.js +245 -0
  271. package/registry/blocks/org-chart/person.jsx +96 -0
  272. package/registry/blocks/page-builder/README.md +257 -0
  273. package/registry/blocks/page-builder/block.json +14 -0
  274. package/registry/blocks/page-builder/demo.jsx +145 -0
  275. package/registry/blocks/page-builder/grapes.js +18 -0
  276. package/registry/blocks/page-builder/index.js +11 -0
  277. package/registry/blocks/page-builder/page-builder.jsx +704 -0
  278. package/registry/blocks/page-builder/page-view.jsx +33 -0
  279. package/registry/blocks/page-builder/page.js +118 -0
  280. package/registry/blocks/page-builder/panels.jsx +588 -0
  281. package/registry/blocks/page-builder/sections.js +209 -0
  282. package/registry/blocks/page-builder/theme.js +87 -0
  283. package/registry/blocks/page-builder/thumbs.jsx +189 -0
  284. package/registry/blocks/page-builder/tokens.js +329 -0
  285. package/registry/blocks/payment-methods/README.md +104 -0
  286. package/registry/blocks/payment-methods/block.json +10 -0
  287. package/registry/blocks/payment-methods/demo.jsx +71 -0
  288. package/registry/blocks/payment-methods/index.js +4 -0
  289. package/registry/blocks/payment-methods/payment-methods.jsx +302 -0
  290. package/registry/blocks/pdf-document/README.md +213 -0
  291. package/registry/blocks/pdf-document/block.json +13 -0
  292. package/registry/blocks/pdf-document/demo.jsx +136 -0
  293. package/registry/blocks/pdf-document/index.js +33 -0
  294. package/registry/blocks/pdf-document/money.js +83 -0
  295. package/registry/blocks/pdf-document/pdf-document.jsx +144 -0
  296. package/registry/blocks/pdf-document/primitives.jsx +271 -0
  297. package/registry/blocks/pdf-document/quote.jsx +179 -0
  298. package/registry/blocks/pdf-document/theme.js +193 -0
  299. package/registry/blocks/pdf-viewer/README.md +191 -0
  300. package/registry/blocks/pdf-viewer/block.json +14 -0
  301. package/registry/blocks/pdf-viewer/demo.jsx +252 -0
  302. package/registry/blocks/pdf-viewer/helpers.js +270 -0
  303. package/registry/blocks/pdf-viewer/index.js +8 -0
  304. package/registry/blocks/pdf-viewer/notes.jsx +195 -0
  305. package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1368 -0
  306. package/registry/blocks/pdf-viewer/pdfjs.js +37 -0
  307. package/registry/blocks/pdf-viewer/ssr-shim-end.js +6 -0
  308. package/registry/blocks/pdf-viewer/ssr-shim.js +44 -0
  309. package/registry/blocks/product-detail/README.md +217 -0
  310. package/registry/blocks/product-detail/block.json +10 -0
  311. package/registry/blocks/product-detail/demo.jsx +233 -0
  312. package/registry/blocks/product-detail/index.js +7 -0
  313. package/registry/blocks/product-detail/price.jsx +115 -0
  314. package/registry/blocks/product-detail/product-detail.jsx +348 -0
  315. package/registry/blocks/product-detail/product-media.jsx +102 -0
  316. package/registry/blocks/product-detail/product-options.jsx +144 -0
  317. package/registry/blocks/product-grid/README.md +162 -0
  318. package/registry/blocks/product-grid/block.json +10 -0
  319. package/registry/blocks/product-grid/demo.jsx +255 -0
  320. package/registry/blocks/product-grid/index.js +5 -0
  321. package/registry/blocks/product-grid/product-card.jsx +232 -0
  322. package/registry/blocks/product-grid/product-grid.jsx +235 -0
  323. package/registry/blocks/record-form/README.md +196 -0
  324. package/registry/blocks/record-form/block.json +10 -0
  325. package/registry/blocks/record-form/demo.jsx +525 -0
  326. package/registry/blocks/record-form/index.js +4 -0
  327. package/registry/blocks/record-form/record-fields.jsx +457 -0
  328. package/registry/blocks/record-form/record-form.jsx +415 -0
  329. package/registry/blocks/record-form/rules.js +81 -0
  330. package/registry/blocks/record-notes/README.md +139 -0
  331. package/registry/blocks/record-notes/block.json +10 -0
  332. package/registry/blocks/record-notes/demo.jsx +179 -0
  333. package/registry/blocks/record-notes/index.js +4 -0
  334. package/registry/blocks/record-notes/record-notes.jsx +266 -0
  335. package/registry/blocks/search-dialog/README.md +108 -0
  336. package/registry/blocks/search-dialog/block.json +10 -0
  337. package/registry/blocks/search-dialog/demo.jsx +116 -0
  338. package/registry/blocks/search-dialog/index.js +3 -0
  339. package/registry/blocks/search-dialog/search-dialog.jsx +235 -0
  340. package/registry/blocks/settings-list/README.md +171 -0
  341. package/registry/blocks/settings-list/block.json +10 -0
  342. package/registry/blocks/settings-list/demo.jsx +333 -0
  343. package/registry/blocks/settings-list/index.js +4 -0
  344. package/registry/blocks/settings-list/row-state.jsx +93 -0
  345. package/registry/blocks/settings-list/settings-list.jsx +349 -0
  346. package/registry/blocks/settings-list/settings-matrix.jsx +118 -0
  347. package/registry/blocks/setup-checklist/README.md +142 -0
  348. package/registry/blocks/setup-checklist/block.json +10 -0
  349. package/registry/blocks/setup-checklist/demo.jsx +159 -0
  350. package/registry/blocks/setup-checklist/index.js +4 -0
  351. package/registry/blocks/setup-checklist/setup-checklist.jsx +446 -0
  352. package/registry/blocks/share-dialog/README.md +113 -0
  353. package/registry/blocks/share-dialog/block.json +10 -0
  354. package/registry/blocks/share-dialog/demo.jsx +140 -0
  355. package/registry/blocks/share-dialog/index.js +3 -0
  356. package/registry/blocks/share-dialog/share-dialog.jsx +384 -0
  357. package/registry/blocks/sidebar/README.md +190 -0
  358. package/registry/blocks/sidebar/block.json +43 -0
  359. package/registry/blocks/sidebar/demo.jsx +577 -0
  360. package/registry/blocks/sidebar/index.js +4 -0
  361. package/registry/blocks/sidebar/sidebar-panel.jsx +149 -0
  362. package/registry/blocks/sidebar/sidebar.jsx +864 -0
  363. package/registry/blocks/signature-pad/README.md +137 -0
  364. package/registry/blocks/signature-pad/block.json +13 -0
  365. package/registry/blocks/signature-pad/colors.js +115 -0
  366. package/registry/blocks/signature-pad/demo.jsx +94 -0
  367. package/registry/blocks/signature-pad/index.js +6 -0
  368. package/registry/blocks/signature-pad/pad.js +7 -0
  369. package/registry/blocks/signature-pad/signature-pad.jsx +434 -0
  370. package/registry/blocks/signature-pad/signature.js +68 -0
  371. package/registry/blocks/sketch-canvas/README.md +280 -0
  372. package/registry/blocks/sketch-canvas/block.json +13 -0
  373. package/registry/blocks/sketch-canvas/demo.jsx +159 -0
  374. package/registry/blocks/sketch-canvas/excalidraw.js +40 -0
  375. package/registry/blocks/sketch-canvas/index.js +9 -0
  376. package/registry/blocks/sketch-canvas/palette.js +226 -0
  377. package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1188 -0
  378. package/registry/blocks/sketch-canvas/sketch.js +179 -0
  379. package/registry/blocks/sketch-canvas/ssr-shim-end.js +6 -0
  380. package/registry/blocks/sketch-canvas/ssr-shim.js +45 -0
  381. package/registry/blocks/sketch-canvas/stamps.js +104 -0
  382. package/registry/blocks/sketch-canvas/theme.js +131 -0
  383. package/registry/blocks/spreadsheet/README.md +307 -0
  384. package/registry/blocks/spreadsheet/block.json +13 -0
  385. package/registry/blocks/spreadsheet/cells.jsx +455 -0
  386. package/registry/blocks/spreadsheet/chrome.jsx +325 -0
  387. package/registry/blocks/spreadsheet/demo.jsx +177 -0
  388. package/registry/blocks/spreadsheet/grid.js +387 -0
  389. package/registry/blocks/spreadsheet/index.js +34 -0
  390. package/registry/blocks/spreadsheet/spreadsheet.jsx +1732 -0
  391. package/registry/blocks/spreadsheet/types.js +473 -0
  392. package/registry/blocks/stat-cards/README.md +173 -0
  393. package/registry/blocks/stat-cards/block.json +10 -0
  394. package/registry/blocks/stat-cards/demo.jsx +257 -0
  395. package/registry/blocks/stat-cards/index.js +4 -0
  396. package/registry/blocks/stat-cards/stat-cards.jsx +307 -0
  397. package/registry/blocks/stat-cards/usage.jsx +243 -0
  398. package/registry/blocks/task-list/README.md +176 -0
  399. package/registry/blocks/task-list/block.json +10 -0
  400. package/registry/blocks/task-list/demo.jsx +134 -0
  401. package/registry/blocks/task-list/index.js +5 -0
  402. package/registry/blocks/task-list/task-list.jsx +907 -0
  403. package/registry/blocks/task-model/README.md +455 -0
  404. package/registry/blocks/task-model/block.json +167 -0
  405. package/registry/blocks/task-model/changes.js +89 -0
  406. package/registry/blocks/task-model/dates.js +371 -0
  407. package/registry/blocks/task-model/demo.jsx +215 -0
  408. package/registry/blocks/task-model/dnd.jsx +240 -0
  409. package/registry/blocks/task-model/index.js +97 -0
  410. package/registry/blocks/task-model/model.js +919 -0
  411. package/registry/blocks/task-model/parts.jsx +449 -0
  412. package/registry/blocks/task-model/toolbar.jsx +271 -0
  413. package/registry/blocks/testimonials/README.md +139 -0
  414. package/registry/blocks/testimonials/block.json +11 -0
  415. package/registry/blocks/testimonials/demo.jsx +137 -0
  416. package/registry/blocks/testimonials/index.js +4 -0
  417. package/registry/blocks/testimonials/testimonials.jsx +430 -0
  418. package/registry/blocks/transaction-list/README.md +126 -0
  419. package/registry/blocks/transaction-list/block.json +10 -0
  420. package/registry/blocks/transaction-list/demo.jsx +79 -0
  421. package/registry/blocks/transaction-list/index.js +4 -0
  422. package/registry/blocks/transaction-list/transaction-list.jsx +383 -0
  423. package/registry/blocks/whiteboard/README.md +179 -0
  424. package/registry/blocks/whiteboard/block.json +10 -0
  425. package/registry/blocks/whiteboard/demo.jsx +169 -0
  426. package/registry/blocks/whiteboard/index.js +6 -0
  427. package/registry/blocks/whiteboard/templates.js +150 -0
  428. package/registry/blocks/whiteboard/whiteboard.jsx +721 -0
  429. package/src/cli.mjs +19 -6
  430. package/src/install.mjs +18 -0
  431. package/src/registry.mjs +38 -4
  432. package/src/shadcn.mjs +14 -1
  433. package/templates/next/files/README.md +145 -11
  434. package/templates/next/files/app/globals.css +55 -4
  435. package/templates/next/files/check/run.mjs +3 -3
  436. package/templates/next/files/components/ui/avatar.jsx +141 -0
  437. package/templates/next/files/components/ui/breadcrumb.jsx +149 -0
  438. package/templates/next/files/components/ui/button-group.jsx +70 -0
  439. package/templates/next/files/components/ui/button.jsx +132 -12
  440. package/templates/next/files/components/ui/calendar.jsx +609 -0
  441. package/templates/next/files/components/ui/carousel.jsx +281 -0
  442. package/templates/next/files/components/ui/collapsible.jsx +208 -0
  443. package/templates/next/files/components/ui/combobox.jsx +608 -0
  444. package/templates/next/files/components/ui/command.jsx +577 -0
  445. package/templates/next/files/components/ui/context-menu.jsx +191 -0
  446. package/templates/next/files/components/ui/date-picker.jsx +360 -0
  447. package/templates/next/files/components/ui/distribution.jsx +117 -0
  448. package/templates/next/files/components/ui/feedback.jsx +143 -0
  449. package/templates/next/files/components/ui/field.jsx +115 -3
  450. package/templates/next/files/components/ui/file-input.jsx +234 -0
  451. package/templates/next/files/components/ui/icon.jsx +130 -8
  452. package/templates/next/files/components/ui/image.jsx +143 -0
  453. package/templates/next/files/components/ui/index.js +47 -2
  454. package/templates/next/files/components/ui/input-group.jsx +213 -0
  455. package/templates/next/files/components/ui/input-otp.jsx +170 -0
  456. package/templates/next/files/components/ui/item.jsx +206 -0
  457. package/templates/next/files/components/ui/menu.jsx +454 -82
  458. package/templates/next/files/components/ui/notice.jsx +126 -14
  459. package/templates/next/files/components/ui/pagination.jsx +156 -0
  460. package/templates/next/files/components/ui/popover.jsx +284 -0
  461. package/templates/next/files/components/ui/radio.jsx +193 -0
  462. package/templates/next/files/components/ui/rating.jsx +131 -0
  463. package/templates/next/files/components/ui/resizable.jsx +338 -0
  464. package/templates/next/files/components/ui/scroll-area.jsx +123 -0
  465. package/templates/next/files/components/ui/separator.jsx +119 -0
  466. package/templates/next/files/components/ui/sheet.jsx +96 -11
  467. package/templates/next/files/components/ui/slider.jsx +174 -0
  468. package/templates/next/files/components/ui/stat.jsx +574 -0
  469. package/templates/next/files/components/ui/steps.jsx +338 -0
  470. package/templates/next/files/components/ui/swatch.jsx +33 -0
  471. package/templates/next/files/components/ui/table.jsx +130 -0
  472. package/templates/next/files/components/ui/toast.jsx +440 -0
  473. package/templates/next/files/components/ui/toggle.jsx +195 -0
  474. package/templates/next/files/components/ui/tooltip.jsx +270 -0
  475. package/templates/next/template.json +264 -8
@@ -0,0 +1,51 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "search-dialog",
4
+ "type": "registry:block",
5
+ "title": "SearchDialog",
6
+ "description": "One ⌘K search over every kind of record — chats, documents, images, projects, people. Before typing it shows your sections (Last opened, Recent); while typing, ranked matches with the typed text marked, a date, image thumbnails, and kind chips (All, Chats, Images…) to narrow them. Searches your list in the browser, or your server through an async onSearch. Built on the kit's Command; SearchPanel is the same box inline.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.1/r/core.json"
9
+ ],
10
+ "dependencies": [],
11
+ "files": [
12
+ {
13
+ "path": "registry/blocks/search-dialog/index.js",
14
+ "type": "registry:component",
15
+ "target": "@components/makersclaw/blocks/search-dialog/index.js",
16
+ "content": "// The SearchDialog block — `makersclaw add search-dialog`. Fixed:\n// compose it through its props (README.md beside this file).\nexport { SearchDialog, SearchPanel } from \"./search-dialog\";\n"
17
+ },
18
+ {
19
+ "path": "registry/blocks/search-dialog/search-dialog.jsx",
20
+ "type": "registry:component",
21
+ "target": "@components/makersclaw/blocks/search-dialog/search-dialog.jsx",
22
+ "content": "\"use client\";\n\nimport { isValidElement, useEffect, useMemo, useRef, useState } from \"react\";\nimport { Command, IconButton, Image, Sheet, ToggleGroup, commandScore, cx } from \"../../ui\";\n\n// SearchDialog — one ⌘K box over every kind of record: chats, documents,\n// images, projects, people. Before anything is typed it shows your\n// sections (\"Last opened\", \"Recent\"); while typing, the matches with the\n// typed text marked, and kind chips (All, Chats, Images…) to narrow them.\n// Built on the kit's Command (the list, keys and ARIA) inside the kit's\n// Sheet. SearchPanel is the same box inline. See README.md beside this file.\n\nconst KINDS = {\n chat: { label: \"Chats\", icon: \"message-circle\" },\n document: { label: \"Documents\", icon: \"file\" },\n image: { label: \"Images\", icon: \"image\" },\n project: { label: \"Projects\", icon: \"folder\" },\n person: { label: \"People\", icon: \"user\" },\n company: { label: \"Companies\", icon: \"building\" },\n task: { label: \"Tasks\", icon: \"circle-check\" },\n page: { label: \"Pages\", icon: \"file\" },\n};\n\nconst cap = (s) => String(s).charAt(0).toUpperCase() + String(s).slice(1);\n\nfunction kindOf(kinds, value) {\n return kinds.find((k) => k.value === value) ?? { value, label: KINDS[value]?.label ?? cap(value), icon: KINDS[value]?.icon ?? \"file\" };\n}\n\n/** The typed text marked inside `text`, once, ignoring case. */\nfunction Marked({ text, needle }) {\n const s = String(text ?? \"\");\n const at = needle ? s.toLowerCase().indexOf(needle) : -1;\n if (at < 0) return s;\n return (\n <>\n {s.slice(0, at)}\n <mark className=\"rounded-sm bg-transparent font-semibold text-foreground underline decoration-primary decoration-2 underline-offset-2\">{s.slice(at, at + needle.length)}</mark>\n {s.slice(at + needle.length)}\n </>\n );\n}\n\nfunction Thumb({ thumb, title }) {\n if (isValidElement(thumb)) return <span className=\"flex size-8 shrink-0 overflow-hidden rounded-sm\">{thumb}</span>;\n return <Image src={thumb || undefined} alt=\"\" ratio=\"square\" rounded=\"sm\" className=\"size-8 shrink-0\" title={title} />;\n}\n\n// The best of a result's fields against the query, 0 when none match.\nfunction score(r, q) {\n return Math.max(commandScore(r.title, q), r.snippet ? commandScore(r.snippet, q) * 0.7 : 0, ...(r.keywords ?? []).map((k) => commandScore(k, q) * 0.9));\n}\n\n/** A server search: debounced, the older query aborted, failures kept. */\nfunction useServerSearch(onSearch, query) {\n const fn = useRef(onSearch);\n fn.current = onSearch;\n const on = Boolean(onSearch) && Boolean(query);\n const [state, setState] = useState({ items: null, loading: false, error: null, query: \"\" });\n useEffect(() => {\n if (!on) return;\n const ctl = new AbortController();\n setState((s) => ({ ...s, loading: true, error: null }));\n const timer = setTimeout(async () => {\n try {\n const items = await fn.current(query, { signal: ctl.signal });\n if (!ctl.signal.aborted) setState({ items: items ?? [], loading: false, error: null, query });\n } catch (error) {\n if (!ctl.signal.aborted) setState((s) => ({ ...s, loading: false, error }));\n }\n }, 200);\n return () => {\n clearTimeout(timer);\n ctl.abort();\n };\n }, [on, query]);\n return state;\n}\n\nexport function SearchPanel({\n results = [],\n sections,\n kinds: kindsProp,\n onSelect,\n onSearch,\n loading = false,\n placeholder = \"Search…\",\n label = \"Search\",\n hint = \"Type to search.\",\n showDate = true,\n onClose,\n autoFocus = false,\n bare = false,\n className,\n}) {\n const [query, setQuery] = useState(\"\");\n const [kind, setKind] = useState(\"all\");\n const needle = query.trim().toLowerCase();\n const server = useServerSearch(onSearch, needle);\n\n // The chips: the kinds given, or every kind the data holds, in order.\n const kinds = useMemo(() => {\n if (kindsProp) return kindsProp;\n const seen = [];\n for (const r of [...results, ...(sections ?? []).flatMap((s) => s.results ?? [])]) if (r?.kind && !seen.includes(r.kind)) seen.push(r.kind);\n return seen.map((v) => kindOf([], v));\n }, [kindsProp, results, sections]);\n\n const pass = (r) => kind === \"all\" || r.kind === kind;\n\n // `at` keeps values unique when one result sits in two sections.\n const toItem = (r, group, at = \"\") => {\n const k = kindOf(kinds, r.kind);\n const meta = [r.snippet, showDate && needle ? r.date : null].filter(Boolean);\n return {\n value: `${at}${r.id}`,\n label: <Marked text={r.title} needle={needle} />,\n description: meta.length ? (\n <>\n {r.snippet ? <Marked text={r.snippet} needle={needle} /> : null}\n {meta.length > 1 ? \" · \" : null}\n {showDate && needle && r.date ? r.date : null}\n </>\n ) : undefined,\n icon: r.kind === \"image\" || r.thumb ? <Thumb thumb={r.thumb} title={r.title} /> : r.icon ?? k.icon,\n group,\n result: r,\n };\n };\n\n let items;\n if (!needle) {\n items = (sections ?? []).flatMap((s, n) => (s.results ?? []).filter(pass).map((r) => toItem(r, s.heading, `${n}:`)));\n } else if (onSearch) {\n items = (server.items ?? []).filter(pass).map((r) => toItem(r));\n } else {\n items = results\n .map((r, i) => ({ r, i, s: pass(r) ? score(r, needle) : 0 }))\n .filter((x) => x.s > 0)\n .sort((a, b) => b.s - a.s || a.i - b.i)\n .map((x) => toItem(x.r));\n }\n\n const busy = loading || (Boolean(onSearch) && Boolean(needle) && (server.loading || server.query !== needle) && !server.error);\n const kindLabel = kind === \"all\" ? \"\" : ` in ${kindOf(kinds, kind).label.toLowerCase()}`;\n const empty = server.error && needle\n ? \"Search failed. Check your connection and try again.\"\n : needle\n ? `No results for “${query.trim()}”${kindLabel}.`\n : kind !== \"all\" && (sections ?? []).length\n ? `Nothing recent${kindLabel}. Type to search.`\n : hint;\n\n const chips = kinds.length > 1 ? [{ value: \"all\", label: \"All\" }, ...kinds.map((k) => ({ value: k.value, label: k.label }))] : null;\n\n return (\n <div className={cx(\"flex min-w-0 flex-col overflow-hidden bg-popover text-popover-foreground\", !bare && \"rounded-lg border border-border\", className)}>\n <Command\n bare\n label={label}\n placeholder={placeholder}\n items={items}\n filter={false}\n query={query}\n onQueryChange={setQuery}\n loading={busy}\n empty={empty}\n autoFocus={autoFocus}\n listClassName=\"max-h-[min(24rem,55dvh)]\"\n onSelect={(item) => onSelect?.(item.result)}\n />\n {chips || onClose ? (\n <div className=\"flex items-center gap-2 border-t border-border px-2 py-1.5\">\n {chips ? (\n <div className=\"min-w-0 flex-1 overflow-x-auto\">\n <ToggleGroup size=\"sm\" aria-label=\"Show only\" items={chips} value={kind} onChange={setKind} className=\"w-max\" />\n </div>\n ) : (\n <span className=\"flex-1\" />\n )}\n {onClose ? <IconButton icon=\"x\" label=\"Close search\" size=\"sm\" onClick={onClose} /> : null}\n </div>\n ) : null}\n </div>\n );\n}\n\n/** SearchPanel in the kit's Sheet: a dialog near the top of a wide window,\n * a bottom sheet on a phone. `open` + `onOpenChange`, or uncontrolled\n * with `defaultOpen`; `hotkey=\"k\"` binds ⌘K / Ctrl+K to open and close\n * it. Choosing a result closes it. */\nexport function SearchDialog({ open, defaultOpen = false, onOpenChange, hotkey, label = \"Search\", onSelect, className, ...panel }) {\n const controlled = open !== undefined;\n const [own, setOwn] = useState(defaultOpen);\n const isOpen = controlled ? open : own;\n const openRef = useRef(isOpen);\n openRef.current = isOpen;\n const changeRef = useRef(null);\n changeRef.current = (next) => {\n if (next === openRef.current) return;\n openRef.current = next;\n if (!controlled) setOwn(next);\n onOpenChange?.(next);\n };\n\n useEffect(() => {\n if (!hotkey) return;\n function onKey(e) {\n if (e.key?.toLowerCase() !== String(hotkey).toLowerCase() || !(e.metaKey || e.ctrlKey) || e.altKey || e.shiftKey) return;\n e.preventDefault();\n changeRef.current(!openRef.current);\n }\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [hotkey]);\n\n return (\n <Sheet bare label={label} open={isOpen} onClose={() => changeRef.current(false)} side=\"center\" className={cx(\"sm:mt-[12dvh] sm:max-w-xl\", className)}>\n {/* Mounted only while open, so each opening starts empty on \"All\". */}\n {isOpen ? (\n <SearchPanel\n bare\n autoFocus\n label={label}\n onClose={() => changeRef.current(false)}\n onSelect={(r) => {\n changeRef.current(false);\n onSelect?.(r);\n }}\n {...panel}\n />\n ) : null}\n </Sheet>\n );\n}\n"
23
+ },
24
+ {
25
+ "path": "registry/blocks/search-dialog/README.md",
26
+ "type": "registry:file",
27
+ "target": "@components/makersclaw/blocks/search-dialog/README.md",
28
+ "content": "# SearchDialog\n\nOne search over every kind of record in an app: chats, documents, images,\nprojects, people. Before anything is typed it shows your sections (\"Last\nopened\", \"Recent chats\"). While typing it shows the matches, best first,\nwith the typed text marked, a date, and a thumbnail for an image. Chips\nunder the list (All, Chats, Images, Documents, Projects) narrow it to one\nkind. Arrow keys move, Enter opens, Esc closes, ⌘K / Ctrl+K toggles it.\n\nIt is the kit's `Command` (the list, the keys, the screen-reader wiring)\ninside the kit's `Sheet`: a dialog near the top of a wide window, a\nbottom sheet on a phone. `SearchPanel` is the same box inline, for a\nsearch page or a sidebar.\n\n```jsx\nimport { SearchDialog, SearchPanel } from \"../components/blocks/search-dialog\"; // a Makersclaw app (\"./blocks/search-dialog\" from components/)\nimport { SearchDialog, SearchPanel } from \"@/components/makersclaw/blocks/search-dialog\"; // a shadcn project\n```\n\nA client component: render it from a `\"use client\"` component.\n\n## The data\n\nA result is `{ id, kind, title, snippet?, date?, thumb?, icon?, keywords? }`.\n\n- `id` — unique within its kind list.\n- `kind` — `\"chat\"`, `\"document\"`, `\"image\"`, `\"project\"`, `\"person\"`,\n `\"company\"`, `\"task\"`, `\"page\"`, or your own word. It picks the icon\n and the chip.\n- `title` — the line that is searched and marked.\n- `snippet` — a line under it: an excerpt, a file type. Searched too.\n- `date` — shown after the snippet while searching (\"Sep 4\", \"Today\").\n Format it yourself.\n- `thumb` — an image URL (or a node) shown instead of the icon. Every\n `image` result gets a square, with an image glyph when there is no URL.\n- `icon` — a kit `Icon` name, instead of the kind's.\n- `keywords` — matched, never shown (a person's company, a project's client).\n\n## Props\n\nBoth components take these.\n\n- `results` — everything that can be found, searched in the browser. Leave\n it out when `onSearch` does the searching.\n- `onSelect(result)` — the result chosen (click or Enter). Open it here.\n The dialog closes itself first.\n- `sections` — what shows before anything is typed:\n `[{ heading, results }]`, in order. A result may sit in two sections.\n Leave it out and the empty box shows `hint`.\n- `onSearch(query, { signal })` — async; return the results for `query`.\n Called 200ms after typing stops. A newer query aborts the older one\n (pass `signal` to `fetch`). A throw shows \"Search failed\". The chips\n filter what comes back.\n- `kinds` — the chips, in order: `[{ value, label, icon? }]`. Default:\n every kind in `results` and `sections`, in order of first appearance,\n labelled from the list above (\"Chats\", \"Images\"). One kind, no chips.\n- `loading` — `true` while you fetch `results` yourself.\n- `placeholder` — default \"Search…\".\n- `hint` — what the empty box says before typing. Default \"Type to search.\"\n- `showDate` — default `true`.\n- `label` — what a screen reader calls the box. Default \"Search\".\n- `className` — on the box (`SearchPanel`) or the dialog (`SearchDialog`).\n\n`SearchDialog` also takes:\n\n- `open` · `onOpenChange(open)` — controlled; or `defaultOpen`.\n- `hotkey` — `\"k\"` binds ⌘K / Ctrl+K anywhere on the page to open and\n close it. Bind no key an input on the page already uses.\n\n## Example\n\n```jsx\n\"use client\";\nimport { useRouter } from \"next/navigation\";\nimport { useState } from \"react\";\nimport { Button } from \"../components/ui\";\nimport { SearchDialog } from \"../components/blocks/search-dialog\";\n\nexport function GlobalSearch({ recent, search }) {\n const router = useRouter();\n const [open, setOpen] = useState(false);\n return (\n <>\n <Button icon=\"search\" onClick={() => setOpen(true)}>Search</Button>\n <SearchDialog\n open={open}\n onOpenChange={setOpen}\n hotkey=\"k\"\n sections={[{ heading: \"Recent\", results: recent }]}\n onSearch={(q) => search(q)} // a server action returning results\n onSelect={(r) => router.push(`/${r.kind}s/${r.id}`)}\n />\n </>\n );\n}\n```\n\n## What it will not do\n\n- Index anything. It searches the list you give it, or asks your server.\n- Lay images out as a grid. An image result is a row with a thumbnail.\n- Remember recent searches. Keep them yourself and pass them as a section.\n- Run commands (\"New invoice\", \"Go to settings\"). For a command palette\n use the kit's `CommandDialog`.\n\n## Credits\n\nPorted from blocks.so (https://blocks.so, command-menu-04, MIT), rewritten on the Makersclaw kit.\n"
29
+ }
30
+ ],
31
+ "categories": [
32
+ "search",
33
+ "global search",
34
+ "command",
35
+ "cmdk",
36
+ "command palette",
37
+ "spotlight",
38
+ "find",
39
+ "filter",
40
+ "recent",
41
+ "quick open",
42
+ "jump to"
43
+ ],
44
+ "docs": "Installed to components/makersclaw/blocks/search-dialog. Import: import { SearchDialog, SearchPanel } from \"@/components/makersclaw/blocks/search-dialog\". Props and an example: components/makersclaw/blocks/search-dialog/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
45
+ "meta": {
46
+ "makersclaw": {
47
+ "version": "0.3.1",
48
+ "useWhen": "An app has more than one kind of record and wants one global search box or ⌘K palette to find any of them, or a search page with type filters."
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,66 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "settings-list",
4
+ "type": "registry:block",
5
+ "title": "SettingsList",
6
+ "description": "A card of settings, one row each — icon, label, description, meta lines, a badge, and its control: a Switch, a Select, a Button, a remove button or a menu of actions, with optional details that open under the row. Every change saves on its own through an async callback; the row shows the new value at once, a spinner while it saves, and on failure the old value back with the reason and Try again. variant=\"matrix\" is the notification grid: events by group down the side, channels (email, push, SMS) across, a switch per cell, stacked on a phone.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.1/r/core.json"
9
+ ],
10
+ "dependencies": [],
11
+ "files": [
12
+ {
13
+ "path": "registry/blocks/settings-list/index.js",
14
+ "type": "registry:component",
15
+ "target": "@components/makersclaw/blocks/settings-list/index.js",
16
+ "content": "// The SettingsList block — `makersclaw add settings-list`. Fixed: compose\n// it through its props (README.md beside this file); a settings screen it\n// cannot be is your own component in components/.\nexport { SettingsList } from \"./settings-list\";\n"
17
+ },
18
+ {
19
+ "path": "registry/blocks/settings-list/settings-list.jsx",
20
+ "type": "registry:component",
21
+ "target": "@components/makersclaw/blocks/settings-list/settings-list.jsx",
22
+ "content": "\"use client\";\n\nimport { useId, useState } from \"react\";\nimport { Badge, Button, Card, Confirm, Empty, Icon, IconButton, Kicker, Menu, Notice, Select, Skeleton, Switch, cx } from \"../../ui\";\nimport { Failure, Saving, useSaves } from \"./row-state\";\nimport { SettingsMatrix } from \"./settings-matrix\";\n\n// SettingsList — a card of settings, one row each: an icon, a label, a\n// description, meta lines, a badge, and the row's control (a Switch, a\n// Select, a Button, or a menu of actions). Every change saves on its own\n// through an async callback; the row shows its new value at once, a\n// spinner while it saves, and — when the save fails — the old value back\n// and why, with Try again. `variant=\"matrix\"` is the notification grid:\n// events down the side, channels across the top.\n\n/** A badge from a string or { label, tone, hue, icon }. */\nfunction RowBadge({ badge }) {\n if (!badge) return null;\n const b = typeof badge === \"string\" ? { label: badge } : badge;\n return (\n <Badge tone={b.tone} hue={b.hue} variant={b.variant ?? \"soft\"}>\n {b.icon ? <Icon name={b.icon} size={12} className=\"-ml-0.5 mr-1 inline-block align-[-0.125em]\" /> : null}\n {b.label}\n </Badge>\n );\n}\n\n/** The meta lines: strings, or { icon, text } for \"globe London\". */\nfunction Meta({ meta }) {\n const list = (Array.isArray(meta) ? meta : [meta]).filter(Boolean);\n if (list.length === 0) return null;\n return (\n <ul className=\"mt-1 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground\">\n {list.map((m, i) => {\n const item = typeof m === \"object\" && !Array.isArray(m) && m !== null && \"text\" in m ? m : { text: m };\n return (\n <li key={i} className=\"inline-flex min-w-0 items-center gap-1.5\">\n {item.icon ? <Icon name={item.icon} size={14} className=\"shrink-0\" /> : null}\n <span className=\"min-w-0 break-words\">{item.text}</span>\n </li>\n );\n })}\n </ul>\n );\n}\n\nfunction Header({ title, description, icon, badge, actions }) {\n if (!title && !description && !actions && !badge) return null;\n return (\n <div className=\"flex flex-wrap items-start justify-between gap-3 px-4 pb-3 pt-4\">\n <div className=\"flex min-w-0 flex-1 items-start gap-3\">\n {icon ? (\n <span className=\"grid size-10 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground\">\n <Icon name={icon} size={18} />\n </span>\n ) : null}\n <div className=\"min-w-0\">\n {title ? <h2 className=\"text-base font-semibold leading-tight\">{title}</h2> : null}\n {description ? <p className=\"mt-0.5 text-sm text-muted-foreground\">{description}</p> : null}\n </div>\n </div>\n {badge || actions ? (\n <div className=\"flex shrink-0 flex-wrap items-center gap-2\">\n <RowBadge badge={badge} />\n {actions}\n </div>\n ) : null}\n </div>\n );\n}\n\n/** Rows under their `group` headings, in the order groups first appear. */\nexport function grouped(rows) {\n const out = [];\n for (const r of rows) {\n const g = r.group ?? null;\n let bucket = out.find((b) => b.group === g);\n if (!bucket) out.push((bucket = { group: g, rows: [] }));\n bucket.rows.push(r);\n }\n return out;\n}\n\nexport function GroupHeading({ children }) {\n return (\n <li className=\"border-t border-border bg-muted/50 px-4 py-2\" role=\"presentation\">\n <Kicker>{children}</Kicker>\n </li>\n );\n}\n\nfunction Row({ row, saves, onChange, onAction, ask, disabled }) {\n const id = useId();\n const [open, setOpen] = useState(row.defaultOpen ?? false);\n const c = row.control ?? null;\n const key = row.key;\n const busy = saves.isBusy(key);\n const failure = saves.failure(key);\n const off = disabled || row.disabled;\n const detailsId = `${id}-details`;\n const details = typeof row.details === \"function\" ? row.details(row) : row.details;\n const inline = c?.type === \"switch\";\n const switchId = `${id}-switch`;\n const noAction = !onAction;\n\n function change(value) {\n if (!onChange) return;\n saves.run(key, () => onChange(row, value), { value, stored: c.value });\n }\n\n function act(action) {\n const go = () => saves.run(key, () => onAction(row, action.key ?? action.label), { fallback: `${action.label} did not go through.` });\n if (action.confirm) ask({ ...action.confirm, run: go, tone: action.confirm.tone ?? (action.tone === \"danger\" ? \"danger\" : undefined) });\n else go();\n }\n\n let control = null;\n if (c?.type === \"switch\") {\n const on = saves.valueOf(key, c.value) === true;\n control = (\n <Switch id={switchId} checked={on} disabled={off || busy || !onChange} onChange={(e) => change(e.target.checked)} />\n );\n } else if (c?.type === \"select\") {\n control = (\n <Select\n aria-label={row.label}\n value={saves.valueOf(key, c.value) ?? \"\"}\n options={c.options}\n disabled={off || busy || !onChange}\n onChange={(e) => change(e.target.value)}\n className=\"sm:w-48\"\n />\n );\n } else if (c?.type === \"button\") {\n const action = { key: c.key ?? \"button\", label: c.label, tone: c.tone, confirm: c.confirm };\n control = (\n <Button\n size=\"sm\"\n icon={c.icon}\n variant={c.variant ?? (c.tone === \"danger\" ? \"danger\" : \"secondary\")}\n loading={busy}\n disabled={off || noAction}\n onClick={() => act(action)}\n >\n {c.label}\n </Button>\n );\n } else if (c?.type === \"actions\") {\n const items = (c.items ?? []).filter(Boolean);\n control = (\n <div className=\"flex items-center gap-1\">\n {busy ? <Saving label=\"Working\" /> : null}\n {c.menu || items.length > 2 ? (\n <Menu\n label={`${row.label}, actions`}\n items={items.map((a) => ({ label: a.label, icon: a.icon, tone: a.tone, disabled: off || busy || noAction, onSelect: () => act(a) }))}\n />\n ) : (\n items.map((a) => (\n <Button key={a.key ?? a.label} size=\"sm\" variant={a.tone === \"danger\" ? \"danger\" : \"quiet\"} icon={a.icon} disabled={off || busy || noAction} onClick={() => act(a)}>\n {a.label}\n </Button>\n ))\n )}\n </div>\n );\n } else if (c?.type === \"remove\") {\n control = (\n <IconButton\n icon=\"x\"\n label={c.label ?? `Remove ${row.label}`}\n variant=\"secondary\"\n size=\"sm\"\n disabled={off || busy || noAction}\n onClick={() => act({ key: \"remove\", label: c.label ?? \"Remove\", tone: \"danger\", confirm: c.confirm })}\n />\n );\n }\n\n return (\n <li className=\"border-t border-border px-4 py-4\">\n <div className={cx(\"flex items-start gap-3\", !inline && \"flex-wrap\")}>\n {row.media ? (\n <span className=\"shrink-0\">{row.media}</span>\n ) : row.icon ? (\n <span className=\"grid size-10 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground\">\n <Icon name={row.icon} size={18} />\n </span>\n ) : null}\n <div className={cx(\"min-w-0 flex-1\", !inline && \"basis-52\")}>\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n {inline ? (\n <label htmlFor={switchId} className=\"font-medium leading-tight text-foreground\">\n {row.label}\n </label>\n ) : (\n <p className=\"font-medium leading-tight text-foreground\">{row.label}</p>\n )}\n <RowBadge badge={row.badge} />\n </div>\n {row.description ? <p className=\"mt-1 text-sm text-muted-foreground\">{row.description}</p> : null}\n {row.value ? (\n <p className={cx(\"mt-1 inline-flex items-center gap-1.5 text-sm\", row.valueTone === \"set\" ? \"font-medium text-foreground\" : \"text-muted-foreground\")}>\n {row.valueTone === \"set\" ? <Icon name=\"circle-check\" size={14} className=\"shrink-0 text-success\" /> : null}\n {row.value}\n </p>\n ) : null}\n <Meta meta={row.meta} />\n </div>\n {control || details ? (\n <div className={cx(\"flex shrink-0 items-center gap-2\", !inline && (row.icon || row.media) && \"max-sm:ml-13\")}>\n {busy && c?.type !== \"button\" && c?.type !== \"actions\" ? <Saving /> : null}\n {control}\n {details ? (\n <IconButton\n icon={open ? \"chevron-up\" : \"chevron-down\"}\n label={`${open ? \"Hide\" : \"Show\"} details for ${row.label}`}\n size=\"sm\"\n aria-expanded={open}\n aria-controls={detailsId}\n onClick={() => setOpen((v) => !v)}\n />\n ) : null}\n </div>\n ) : null}\n </div>\n <Failure failure={failure} className=\"mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-destructive\" />\n {details ? (\n <div id={detailsId} hidden={!open} className=\"mt-3 rounded-md border border-border bg-muted/40 p-3 text-sm\">\n {details}\n </div>\n ) : null}\n </li>\n );\n}\n\nexport function SettingsList({\n rows = [],\n variant = \"list\",\n channels = [],\n onChange,\n onAction,\n title,\n description,\n icon,\n badge,\n actions,\n footer,\n loading = false,\n error,\n onRetry,\n emptyTitle = \"Nothing here yet\",\n emptyBody,\n emptyAction,\n disabled = false,\n failedText = \"Not saved. The old value is back.\",\n className,\n}) {\n const saves = useSaves(failedText);\n const [asking, setAsking] = useState(null);\n const [confirming, setConfirming] = useState(false);\n\n async function confirmNow() {\n if (!asking) return;\n setConfirming(true);\n await asking.run();\n setConfirming(false);\n setAsking(null);\n }\n\n let body;\n if (error) {\n body = (\n <div className=\"border-t border-border p-4\">\n <Notice tone=\"danger\" title=\"These settings could not be loaded\" action={onRetry ? <Button size=\"sm\" onClick={onRetry}>Try again</Button> : null}>\n {error}\n </Notice>\n </div>\n );\n } else if (loading) {\n body = (\n <ul aria-busy=\"true\" aria-label=\"Loading\">\n {[0, 1, 2].map((i) => (\n <li key={i} className=\"flex items-center gap-3 border-t border-border px-4 py-4\">\n <Skeleton shape=\"rect\" className=\"size-10 shrink-0\" />\n <div className=\"grid min-w-0 flex-1 gap-2\">\n <Skeleton className=\"w-1/3\" />\n <Skeleton className=\"w-2/3\" />\n </div>\n <Skeleton shape=\"rect\" className=\"h-6 w-10 shrink-0 rounded-full\" />\n </li>\n ))}\n </ul>\n );\n } else if (rows.length === 0) {\n body = (\n <div className=\"border-t border-border p-4\">\n <Empty icon={icon ?? \"settings\"} title={emptyTitle} body={emptyBody} action={emptyAction} />\n </div>\n );\n } else if (variant === \"matrix\") {\n body = <SettingsMatrix rows={rows} channels={channels} onChange={onChange} saves={saves} disabled={disabled} />;\n } else {\n body = (\n <ul>\n {grouped(rows).map((b) => (\n <GroupRows key={b.group ?? \"_\"} bucket={b}>\n {b.rows.map((row) => (\n <Row key={row.key} row={row} saves={saves} onChange={onChange} onAction={onAction} ask={setAsking} disabled={disabled} />\n ))}\n </GroupRows>\n ))}\n </ul>\n );\n }\n\n const headed = Boolean(title || description || actions || badge);\n return (\n <Card padding=\"none\" className={cx(!headed && \"[&>ul>li:first-child]:border-t-0\", className)}>\n <Header title={title} description={description} icon={icon} badge={badge} actions={actions} />\n {body}\n {footer ? <div className=\"border-t border-border p-4\">{footer}</div> : null}\n <Confirm\n open={Boolean(asking)}\n onClose={() => {\n if (!confirming) setAsking(null);\n }}\n onConfirm={confirmNow}\n busy={confirming}\n tone={asking?.tone ?? \"neutral\"}\n title={asking?.title ?? \"Are you sure?\"}\n body={asking?.body}\n confirmLabel={asking?.confirmLabel ?? \"Confirm\"}\n consequences={asking?.consequences}\n requireText={asking?.requireText}\n acknowledge={asking?.acknowledge}\n />\n </Card>\n );\n}\n\nfunction GroupRows({ bucket, children }) {\n return (\n <>\n {bucket.group ? <GroupHeading>{bucket.group}</GroupHeading> : null}\n {children}\n </>\n );\n}\n"
23
+ },
24
+ {
25
+ "path": "registry/blocks/settings-list/settings-matrix.jsx",
26
+ "type": "registry:component",
27
+ "target": "@components/makersclaw/blocks/settings-list/settings-matrix.jsx",
28
+ "content": "\"use client\";\n\nimport { useId } from \"react\";\nimport { Icon, Kicker, Switch, cx } from \"../../ui\";\nimport { Failure, Saving } from \"./row-state\";\n\n// The notification grid of a SettingsList (variant=\"matrix\"): events down\n// the side, grouped; channels (email, push, SMS…) across the top; a Switch\n// in every cell that saves on its own. From 768px it is a grid with the\n// channels as column heads. On a phone each event's switches stack under\n// it, each one named by its channel, so nothing scrolls sideways.\n\nfunction groupsOf(rows) {\n const out = [];\n for (const r of rows) {\n const g = r.group ?? null;\n let b = out.find((x) => x.group === g);\n if (!b) out.push((b = { group: g, rows: [] }));\n b.rows.push(r);\n }\n return out;\n}\n\n// The grid's columns: the event, then one fixed column per channel. The\n// count comes in as a custom property so one class serves any number.\nconst GRID = \"md:grid md:items-center md:gap-x-2 md:[grid-template-columns:minmax(0,1fr)_repeat(var(--channels),5.5rem)]\";\n\nfunction Cell({ row, channel, saves, onChange, disabled }) {\n const id = useId();\n const key = `${row.key}:${channel.key}`;\n const stored = row.values?.[channel.key];\n if (stored === undefined || stored === null) {\n // This event cannot be sent on this channel.\n return (\n <div className=\"flex min-h-11 items-center gap-2 text-sm text-muted-foreground md:justify-center\">\n <span className=\"md:sr-only\">{channel.label}</span>\n <span aria-hidden=\"true\">—</span>\n <span className=\"sr-only\">not offered</span>\n </div>\n );\n }\n const locked = (row.locked ?? []).includes(channel.key);\n const busy = saves.isBusy(key);\n const on = saves.valueOf(key, stored) === true;\n return (\n <div className=\"flex min-h-11 items-center gap-2 md:justify-center\">\n <Switch\n id={id}\n checked={on}\n disabled={disabled || locked || busy || !onChange}\n onChange={(e) => {\n const value = e.target.checked;\n saves.run(key, () => onChange(row, value, channel.key), { value, stored });\n }}\n label={\n <span className=\"md:sr-only\">\n {channel.label}\n {locked ? <span className=\"sr-only\"> (always on)</span> : null}\n </span>\n }\n />\n {locked ? <Icon name=\"lock\" size={12} className=\"shrink-0 text-muted-foreground\" /> : null}\n {busy ? <Saving /> : null}\n </div>\n );\n}\n\nexport function SettingsMatrix({ rows, channels, onChange, saves, disabled }) {\n const base = useId();\n return (\n <div style={{ \"--channels\": channels.length }}>\n <div className={cx(\"hidden border-t border-border px-4 py-2\", GRID)} aria-hidden=\"true\">\n <Kicker>Event</Kicker>\n {channels.map((c) => (\n <span key={c.key} className=\"inline-flex items-center justify-center gap-1.5 text-xs font-medium text-muted-foreground\">\n {c.icon ? <Icon name={c.icon} size={14} /> : null}\n {c.label}\n </span>\n ))}\n </div>\n {groupsOf(rows).map((g) => (\n <section key={g.group ?? \"_\"} aria-label={g.group ?? undefined}>\n {g.group ? (\n <div className=\"border-t border-border bg-muted/50 px-4 py-2\">\n <Kicker>{g.group}</Kicker>\n </div>\n ) : null}\n <ul>\n {g.rows.map((row) => {\n const labelId = `${base}-${row.key}`;\n const failures = channels.map((c) => ({ c, f: saves.failure(`${row.key}:${c.key}`) })).filter((x) => x.f);\n return (\n <li key={row.key} role=\"group\" aria-labelledby={labelId} className=\"border-t border-border px-4 py-3\">\n <div className={GRID}>\n <div className=\"min-w-0 pb-1 md:pb-0\">\n <p id={labelId} className=\"text-sm font-medium text-foreground\">\n {row.label}\n </p>\n {row.description ? <p className=\"text-sm text-muted-foreground\">{row.description}</p> : null}\n </div>\n <div className=\"flex flex-wrap gap-x-5 md:contents\">\n {channels.map((c) => (\n <Cell key={c.key} row={row} channel={c} saves={saves} onChange={onChange} disabled={disabled} />\n ))}\n </div>\n </div>\n {failures.map(({ c, f }) => (\n <Failure key={c.key} failure={{ ...f, error: `${c.label}: ${f.error}` }} className=\"mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-destructive\" />\n ))}\n </li>\n );\n })}\n </ul>\n </section>\n ))}\n </div>\n );\n}\n"
29
+ },
30
+ {
31
+ "path": "registry/blocks/settings-list/row-state.jsx",
32
+ "type": "registry:component",
33
+ "target": "@components/makersclaw/blocks/settings-list/row-state.jsx",
34
+ "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { Button, Icon, Spinner } from \"../../ui\";\n\n// Each control in a SettingsList saves on its own. This keeps, per control\n// (a row, or a row × channel in the matrix), what it shows while its save\n// runs (the new value, at once), that it is saving, and why its last save\n// failed — with the attempt kept so \"Try again\" repeats it.\n\n/** Run `save()` → { ok } | { ok: false, error }; a throw counts as failed. */\nasync function attempt(save) {\n try {\n const res = await save();\n return res?.ok ? { ok: true } : { ok: false, error: res?.error };\n } catch (e) {\n return { ok: false, error: e?.message };\n }\n}\n\nfunction sameValue(a, b) {\n return a === b || (a == null && b == null);\n}\n\nexport function useSaves(failedText = \"Not saved.\") {\n const [shown, setShown] = useState({}); // key → { value, from }: the new value, over the stored `from`\n const [busy, setBusy] = useState({}); // key → true\n const [failed, setFailed] = useState({}); // key → { error, retry }\n const alive = useRef(true);\n useEffect(() => {\n alive.current = true;\n return () => {\n alive.current = false;\n };\n }, []);\n\n const drop = (setter, key) =>\n setter((m) => {\n if (!(key in m)) return m;\n const { [key]: _gone, ...rest } = m;\n return rest;\n });\n\n /** Save `key`: show `value` (optional; `stored` is what the rows hold\n * now) at once, run `save`, put the stored value back on failure.\n * `fallback` words a failure that came back without an error. */\n async function run(key, save, { value, stored, fallback } = {}) {\n const retry = () => run(key, save, { value, stored, fallback });\n if (value !== undefined) setShown((m) => ({ ...m, [key]: { value, from: stored } }));\n setBusy((m) => ({ ...m, [key]: true }));\n drop(setFailed, key);\n const res = await attempt(save);\n if (!alive.current) return res;\n drop(setBusy, key);\n // Good: the new value stays on screen until the rows catch up (see\n // valueOf). Bad: the stored one returns.\n if (!res.ok) {\n drop(setShown, key);\n setFailed((m) => ({ ...m, [key]: { error: res.error || fallback || failedText, retry } }));\n }\n return res;\n }\n\n return {\n run,\n // The new value while the rows still hold what they held before the\n // save; once they change (the save landed, or anything else moved\n // them) the rows are the truth again.\n valueOf: (key, stored) => (key in shown && sameValue(shown[key].from, stored) ? shown[key].value : stored),\n isBusy: (key) => Boolean(busy[key]),\n failure: (key) => failed[key] ?? null,\n dismiss: (key) => drop(setFailed, key),\n };\n}\n\n/** The line under a row whose save failed: why, and Try again. */\nexport function Failure({ failure, id, className }) {\n if (!failure) return null;\n return (\n <p id={id} role=\"alert\" className={className ?? \"flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-destructive\"}>\n <Icon name=\"warning\" size={14} className=\"shrink-0\" />\n <span className=\"min-w-0\">{failure.error}</span>\n <Button variant=\"link\" size=\"sm\" onClick={failure.retry}>\n Try again\n </Button>\n </p>\n );\n}\n\n/** A small spinner that says what is happening. */\nexport function Saving({ label = \"Saving\" }) {\n return <Spinner size=\"sm\" label={label} className=\"text-muted-foreground\" />;\n}\n"
35
+ },
36
+ {
37
+ "path": "registry/blocks/settings-list/README.md",
38
+ "type": "registry:file",
39
+ "target": "@components/makersclaw/blocks/settings-list/README.md",
40
+ "content": "# SettingsList\n\nA card of settings, one row each: an icon, a label, a description, a few\nmeta lines, a badge, and the row's control. Every change saves on its own —\nno Save button. A flipped switch or a new choice shows at once, a spinner\nsits beside it while it saves, and when the save fails the old value comes\nback with the reason and Try again, on that row only. `variant=\"matrix\"`\nis the notification grid: events down the side, channels across the top.\n\n```jsx\nimport { SettingsList } from \"../components/blocks/settings-list\"; // a Makersclaw app (\"./blocks/settings-list\" from components/)\nimport { SettingsList } from \"@/components/makersclaw/blocks/settings-list\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component, and\nload the settings in the server surface (README §4).\n\n## The data\n\nA row:\n\n- `key` — unique in the list (required).\n- `label` — what the setting is (required).\n- `description` — a muted line under it.\n- `icon` — a kit `Icon` name in a muted square; or `media`, any node (an\n Avatar, a logo).\n- `badge` — `\"Current\"` or `{ label, tone?, hue?, icon? }` beside the label.\n `tone` for a status (\"Recommended\", success), `hue` 1–5 for a kind\n (\"Development\").\n- `value` — a line saying what is set now (\"+44 7700 900 461\", \"Not set\n up\"); `valueTone: \"set\"` draws it in full weight with a check.\n- `meta` — small lines under it: strings, or `{ icon, text }` (\"globe\n Lisbon\", \"clock Active now\").\n- `group` — rows with the same group sit under one heading, in the order\n the groups first appear.\n- `control` — one of:\n - `{ type: \"switch\", value: true }` — saves through `onChange(row, bool)`.\n - `{ type: \"select\", value, options: [{ value, label }] }` — saves\n through `onChange(row, value)`.\n - `{ type: \"button\", label, icon?, variant?, tone?, confirm? }` — runs\n `onAction(row, \"button\")` (or `key` if given).\n - `{ type: \"actions\", items: [{ key, label, icon?, tone?, confirm? }],\n menu? }` — up to two buttons, or a \"⋯\" menu for three or more (or\n `menu: true`); runs `onAction(row, item.key)`.\n - `{ type: \"remove\", label?, confirm? }` — an × button (sign a device out,\n disconnect); runs `onAction(row, \"remove\")`.\n - none — a row that only says something.\n `confirm` asks first, with the kit Confirm: `{ title, body?,\n confirmLabel?, tone?, consequences?, requireText?, acknowledge? }`.\n- `details` — a node, or `(row) => node`, that opens under the row from a\n chevron button (the connected account, a copyable code). `defaultOpen`\n starts it open.\n- `disabled` — the control is shown and cannot be used.\n\nThe matrix (`variant=\"matrix\"`) rows are events:\n\n- `key`, `label`, `description`, `group` — as above.\n- `values` — `{ email: true, push: false, sms: null }`, one per channel\n key. `null` (or missing) means the event is not sent on that channel: a\n dash, not a switch.\n- `locked` — channel keys that cannot be turned off (`[\"email\"]` for a\n security alert): the switch shows, disabled, with a lock.\n\n## Props\n\n- `rows` — the rows above. Keep them as the saved truth: when a save\n succeeds, update the row (or let the server revalidate). Until the rows\n change, the row shows the new value it saved.\n- `onChange(row, value, channel?)` — save a switch or a select (in the\n matrix, `channel` is the channel key). Return `{ ok: true }` or `{ ok:\n false, error }`; a throw counts as failed. Without it, switches and\n selects are shown and cannot be changed. In a Makersclaw app it is one\n line over `saveSetting` (lib/actions.js):\n `onChange={(row, value) => saveSetting({ source: \"public.settings\", key: row.key, value })}`.\n- `onAction(row, actionKey)` — run a button, a menu item or a remove.\n Return `{ ok }` like `onChange`. Remove the row from `rows` on success\n when it is gone (a signed-out device).\n- `variant` — `\"list\"` (default) or `\"matrix\"`.\n- `channels` — the matrix's columns: `[{ key, label, icon? }]`.\n- `title`, `description`, `icon` — the card's heading; `icon` in a muted\n square before it.\n- `badge` — a state beside the heading (`{ label: \"On\", tone: \"success\",\n icon: \"circle-check\" }`).\n- `actions` — nodes at the heading's end (\"Add profile\").\n- `footer` — a node under the rows (a Notice with a tip).\n- `loading` — skeleton rows.\n- `error` — the settings could not be loaded: a Notice in place of the\n rows; `onRetry` adds \"Try again\".\n- `emptyTitle` (`\"Nothing here yet\"`) · `emptyBody` · `emptyAction` —\n shown when `rows` is empty.\n- `disabled` — every control shown and none usable.\n- `failedText` — the reason shown when a save fails without an `error`.\n- `className` — layout around it (width, margin). Never colour.\n\n## Example\n\n```jsx\n// components/Preferences.jsx — client\n\"use client\";\nimport { saveSetting } from \"../lib/actions\";\nimport { SettingsList } from \"./blocks/settings-list\";\n\nexport default function Preferences({ settings }) {\n const rows = [\n { key: \"digest\", icon: \"mail\", label: \"Weekly digest\", description: \"Every Monday at 8:00.\", control: { type: \"switch\", value: settings.digest === true } },\n {\n key: \"currency\",\n icon: \"dollar\",\n label: \"Currency\",\n control: { type: \"select\", value: settings.currency ?? \"usd\", options: [{ value: \"usd\", label: \"US dollar\" }, { value: \"eur\", label: \"Euro\" }] },\n },\n ];\n return (\n <SettingsList\n title=\"Preferences\"\n rows={rows}\n onChange={(row, value) => saveSetting({ source: \"public.settings\", key: row.key, value })}\n />\n );\n}\n```\n\nNotifications per channel:\n\n```jsx\n<SettingsList\n variant=\"matrix\"\n title=\"Notifications\"\n channels={[{ key: \"email\", label: \"Email\", icon: \"mail\" }, { key: \"push\", label: \"Push\", icon: \"smartphone\" }]}\n rows={[\n { key: \"mentions\", group: \"Conversations\", label: \"Mentions\", values: { email: true, push: true } },\n { key: \"signin\", group: \"Security\", label: \"New sign-in\", values: { email: true, push: false }, locked: [\"email\"] },\n ]}\n onChange={(row, on, channel) => saveSetting({ mode: \"json\", source: \"public.members\", column: \"notify\", id: me.id, key: `${row.key}.${channel}`, value: on })}\n/>\n```\n\nSigned-in devices, each with sign-out:\n\n```jsx\n<SettingsList\n icon=\"shield-check\"\n title=\"Signed-in devices\"\n rows={sessions.map((s) => ({\n key: s.id,\n icon: s.kind === \"phone\" ? \"smartphone\" : \"laptop\",\n label: s.device,\n badge: s.current ? { label: \"This device\", tone: \"info\" } : null,\n meta: [{ icon: \"globe\", text: s.place }, { icon: \"clock\", text: s.lastSeen }],\n control: s.current ? null : { type: \"remove\", label: `Sign out ${s.device}`, confirm: { title: \"Sign out this device?\", confirmLabel: \"Sign out\" } },\n }))}\n onAction={(row) => deleteRow({ source: \"public.sessions\", id: row.key })}\n/>\n```\n\n## What it will not do\n\n- Save several settings with one button: that is `record-form`.\n- Open a setup flow: a button's `onAction` is yours — open your `Sheet`\n from it and return `{ ok: true }`.\n- Scroll the matrix sideways on a phone: under 768px each event's switches\n stack under it, named by channel.\n\nA settings screen it cannot be is your own component in `components/`,\nbuilt from the kit — not an edit to this block. Its files are fixed: `npm\nrun check` refuses a change, and `makersclaw add settings-list` restores\nthem.\n\n## Credits\n\nShapes from Creative Tim UI (https://github.com/creativetimofficial/ui, MIT); this is a rewrite on the Makersclaw kit.\n"
41
+ }
42
+ ],
43
+ "categories": [
44
+ "settings",
45
+ "preferences",
46
+ "toggle",
47
+ "switch",
48
+ "notifications",
49
+ "channels",
50
+ "matrix",
51
+ "security",
52
+ "sessions",
53
+ "devices",
54
+ "integrations",
55
+ "two-factor",
56
+ "billing",
57
+ "account"
58
+ ],
59
+ "docs": "Installed to components/makersclaw/blocks/settings-list. Import: import { SettingsList } from \"@/components/makersclaw/blocks/settings-list\". Props and an example: components/makersclaw/blocks/settings-list/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
60
+ "meta": {
61
+ "makersclaw": {
62
+ "version": "0.3.1",
63
+ "useWhen": "An account or workspace settings screen where each setting applies the moment it is changed: preferences, two-step sign-in methods, signed-in devices, connected integrations, billing profiles, notification preferences per channel. Not for a form of fields saved together with one button (record-form)."
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "setup-checklist",
4
+ "type": "registry:block",
5
+ "title": "SetupChecklist",
6
+ "description": "The steps to get set up: which are done, how far along the whole is (a ring, a bar, or \"Step 2 of 4\"), and the button for the next one, sent out through an async onAction that can fail on the step. Four shapes: folding rows in a card with a dismiss menu, numbered step cards, a plain step list with a help panel, and bordered accordion rows that can be locked until the step before is done.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.1/r/core.json"
9
+ ],
10
+ "dependencies": [],
11
+ "files": [
12
+ {
13
+ "path": "registry/blocks/setup-checklist/index.js",
14
+ "type": "registry:component",
15
+ "target": "@components/makersclaw/blocks/setup-checklist/index.js",
16
+ "content": "// The SetupChecklist block — `makersclaw add setup-checklist`. Fixed:\n// compose it through its props (README.md beside this file); a checklist\n// it cannot be is your own component in components/.\nexport { SetupChecklist } from \"./setup-checklist\";\n"
17
+ },
18
+ {
19
+ "path": "registry/blocks/setup-checklist/setup-checklist.jsx",
20
+ "type": "registry:component",
21
+ "target": "@components/makersclaw/blocks/setup-checklist/setup-checklist.jsx",
22
+ "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { Accordion, Button, Card, Collapsible, Empty, Icon, Menu, Meter, Notice, Progress, Skeleton, cx } from \"../../ui\";\n\n// SetupChecklist — the steps to get set up: which are done, how far along\n// the whole is, and the button for the next one. The steps are the app's\n// (their `done` comes from its data); the block draws them and sends each\n// step's button out through `onAction`, showing a failure on the step.\n\n/* ------------------------------------------------------------------------\n Pieces\n ------------------------------------------------------------------------ */\n\nfunction Marker({ state, index, shape }) {\n if (state === \"done\") {\n return <Icon name=\"circle-check\" size={shape === \"number\" ? 24 : 20} className=\"shrink-0 text-success\" />;\n }\n if (shape === \"number\") {\n return (\n <span\n aria-hidden=\"true\"\n data-nums\n className={cx(\n \"flex size-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold\",\n state === \"current\" ? \"bg-primary text-primary-foreground\" : \"bg-muted text-muted-foreground\",\n )}\n >\n {index + 1}\n </span>\n );\n }\n if (shape === \"ring\") {\n return <span aria-hidden=\"true\" className=\"size-5 shrink-0 rounded-full border border-border\" />;\n }\n return <Icon name=\"circle-dashed\" size={20} className=\"shrink-0 text-muted-foreground\" />;\n}\n\n// What a reader hears after the title.\nconst SAID = { done: \"done\", current: \"next\", pending: \"not started\", locked: \"not started\" };\n\nfunction StepTitle({ step, state }) {\n return (\n <span className={cx(state === \"done\" ? \"text-muted-foreground line-through\" : state === \"locked\" ? \"text-muted-foreground\" : \"text-foreground\")}>\n {step.title}\n <span className=\"sr-only\">, {SAID[state]}</span>\n </span>\n );\n}\n\nfunction StepError({ message }) {\n if (!message) return null;\n return (\n <p role=\"alert\" className=\"mt-2 text-sm text-destructive\">\n {message}\n </p>\n );\n}\n\n/* ------------------------------------------------------------------------\n SetupChecklist\n ------------------------------------------------------------------------ */\n\nexport function SetupChecklist({\n steps,\n variant = \"collapsible\",\n title = \"Get set up\",\n description,\n onAction,\n onDismiss,\n menuItems,\n footer,\n sequential = false,\n doneLabel = \"All set\",\n loading = false,\n error,\n onRetry,\n className,\n}) {\n const list = (steps ?? []).filter(Boolean);\n const total = list.length;\n const doneCount = list.filter((s) => s.done).length;\n const current = list.findIndex((s) => !s.done);\n const allDone = total > 0 && current < 0;\n\n const uid = useId();\n const [busy, setBusy] = useState(null);\n const [errors, setErrors] = useState({});\n const [dismissError, setDismissError] = useState(null);\n const [open, setOpen] = useState(() => list[current]?.key ?? null);\n\n // When the open step gets done, the next one opens.\n const doneSig = list.map((s) => (s.done ? 1 : 0)).join(\"\");\n const lastSig = useRef(doneSig);\n useEffect(() => {\n if (lastSig.current === doneSig) return;\n lastSig.current = doneSig;\n const openStep = list.find((s) => s.key === open);\n if (!openStep || openStep.done) setOpen(list[current]?.key ?? null);\n // The steps' done flags are what moved.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [doneSig]);\n\n const stateOf = (i) => (list[i].done ? \"done\" : i === current ? \"current\" : sequential ? \"locked\" : \"pending\");\n\n async function run(step) {\n if (!onAction) return;\n setBusy(step.key);\n setErrors((e) => ({ ...e, [step.key]: null }));\n let res;\n try {\n res = await onAction(step);\n } catch (e) {\n res = { ok: false, error: e?.message };\n }\n setBusy(null);\n if (res && res.ok === false) setErrors((e) => ({ ...e, [step.key]: res.error || \"That did not work. Try again.\" }));\n }\n\n async function dismiss() {\n setDismissError(null);\n let res;\n try {\n res = await onDismiss();\n } catch (e) {\n res = { ok: false, error: e?.message };\n }\n if (res && res.ok === false) setDismissError(res.error || \"The checklist could not be hidden.\");\n }\n\n // A render helper, not a component: a component declared in here would\n // remount on every render and drop the focus from its button.\n function action(step, i, look) {\n const state = stateOf(i);\n const label = step.actionLabel ?? (state === \"done\" ? \"Open\" : \"Start\");\n const variant = state === \"done\" || look === \"secondary\" ? \"secondary\" : \"primary\";\n const disabled = state === \"locked\";\n if (!step.href && !onAction) return null;\n return (\n <Button\n size=\"sm\"\n variant={variant}\n icon={look === \"icon\" ? step.icon : undefined}\n href={disabled ? undefined : step.href}\n onClick={step.href ? () => onAction?.(step) : () => run(step)}\n loading={busy === step.key}\n disabled={disabled || (busy !== null && busy !== step.key)}\n aria-describedby={disabled ? `${uid}-${step.key}-locked` : undefined}\n >\n {label}\n </Button>\n );\n }\n\n const menu = [onDismiss && { label: \"Dismiss\", icon: \"archive\", onSelect: dismiss }, ...(menuItems ?? [])].filter(Boolean);\n\n /* ---- states that replace the steps -------------------------------- */\n\n if (loading) {\n return (\n <div aria-busy=\"true\" aria-label={title} className={cx(\"grid gap-3 rounded-lg border border-border bg-card p-4\", className)}>\n <Skeleton className=\"w-1/2\" />\n <Skeleton className=\"h-1.5 w-full\" shape=\"rect\" />\n {[0, 1, 2, 3].map((i) => (\n <div key={i} className=\"flex items-center gap-3 py-2\">\n <Skeleton shape=\"circle\" className=\"size-5\" />\n <Skeleton className=\"w-2/3\" />\n </div>\n ))}\n </div>\n );\n }\n if (error) {\n return (\n <Notice\n tone=\"danger\"\n title=\"The checklist did not load\"\n className={className}\n action={\n onRetry ? (\n <Button size=\"sm\" icon=\"refresh\" onClick={onRetry}>\n Retry\n </Button>\n ) : null\n }\n >\n {error}\n </Notice>\n );\n }\n if (total === 0) {\n return <Empty icon=\"circle-check\" title=\"Nothing to set up\" body=\"Every step is taken care of.\" className={className} />;\n }\n\n const bar = (\n <Progress\n value={doneCount}\n max={total}\n tone={allDone ? \"success\" : \"neutral\"}\n aria-label={`${title}: ${doneCount} of ${total} steps done`}\n valueText={`${doneCount} of ${total}`}\n />\n );\n const menuButton = menu.length ? <Menu items={menu} label={`${title} options`} /> : null;\n const dismissNotice = dismissError ? (\n <Notice tone=\"danger\" onDismiss={() => setDismissError(null)}>\n {dismissError}\n </Notice>\n ) : null;\n const lockedNote = (step) =>\n stateOf(list.indexOf(step)) === \"locked\" ? (\n <p id={`${uid}-${step.key}-locked`} className=\"mt-2 text-xs text-muted-foreground\">\n Finish the steps before this one first.\n </p>\n ) : null;\n\n /* ---- collapsible: a card of folding rows, a ring in the header ----- */\n\n if (variant === \"collapsible\") {\n return (\n <Card className={cx(\"grid gap-2\", className)}>\n <div className=\"flex flex-wrap items-center justify-between gap-x-4 gap-y-2\">\n <div className=\"min-w-0\">\n <h2 className=\"font-semibold leading-tight\">{title}</h2>\n {description ? <p className=\"mt-0.5 text-sm text-muted-foreground\">{description}</p> : null}\n </div>\n <div className=\"ml-auto flex items-center gap-1\">\n <Meter\n shape=\"ring\"\n size=\"sm\"\n value={doneCount}\n max={total}\n tone={allDone ? \"success\" : \"neutral\"}\n center={null}\n valueText={null}\n aria-label={`${doneCount} of ${total} steps done`}\n />\n <span data-nums className=\"ml-1.5 mr-1 text-sm text-muted-foreground\">\n {allDone ? (\n doneLabel\n ) : (\n <>\n <span className=\"font-medium text-foreground\">{doneCount}</span> of <span className=\"font-medium text-foreground\">{total}</span> done\n </>\n )}\n </span>\n {menuButton}\n </div>\n </div>\n {dismissNotice}\n <Accordion\n aria-label={title}\n chevron=\"right\"\n value={open}\n onChange={setOpen}\n items={list.map((step, i) => {\n const state = stateOf(i);\n return {\n key: step.key,\n title: (\n <span className=\"flex items-center gap-3\">\n <Marker state={state} />\n <StepTitle step={step} state={state} />\n </span>\n ),\n content: (\n <div className=\"pl-8\">\n {step.description ? <p className=\"max-w-prose text-muted-foreground\">{step.description}</p> : null}\n <div className=\"mt-3\">\n {action(step, i)}\n </div>\n {lockedNote(step)}\n <StepError message={errors[step.key]} />\n </div>\n ),\n };\n })}\n />\n {footer ? <div className=\"border-t border-border pt-3 text-sm\">{footer}</div> : null}\n </Card>\n );\n }\n\n const header = (right) => (\n <div className=\"grid gap-3\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <h2 className=\"text-lg font-semibold leading-tight\">{title}</h2>\n {description ? <p className=\"mt-1 text-sm text-muted-foreground\">{description}</p> : null}\n </div>\n {menuButton}\n </div>\n {right}\n {dismissNotice}\n </div>\n );\n\n /* ---- accordion: bordered folding rows, a panel in each ------------- */\n\n if (variant === \"accordion\") {\n return (\n <div className={cx(\"grid gap-4\", className)}>\n {header(null)}\n <div className=\"grid gap-2\" role=\"list\" aria-label={title}>\n {list.map((step, i) => {\n const state = stateOf(i);\n return (\n <div role=\"listitem\" key={step.key}>\n <Collapsible\n variant=\"outline\"\n open={open === step.key}\n onOpenChange={(next) => setOpen(next ? step.key : null)}\n title={\n <span className=\"flex items-center gap-3\">\n <Marker state={state} shape=\"ring\" />\n <StepTitle step={step} state={state} />\n </span>\n }\n >\n <div className=\"grid justify-items-center gap-1 rounded-md bg-muted px-4 py-5 text-center\">\n {step.icon ? <Icon name={step.icon} size={24} className=\"mb-2 text-muted-foreground\" /> : null}\n {step.summary ? <p className=\"font-medium text-foreground\">{step.summary}</p> : null}\n {step.description ? <p className=\"max-w-xs text-muted-foreground\">{step.description}</p> : null}\n <div className=\"mt-4\">\n {action(step, i)}\n </div>\n {lockedNote(step)}\n <StepError message={errors[step.key]} />\n </div>\n </Collapsible>\n </div>\n );\n })}\n </div>\n {footer ? <div className=\"text-sm\">{footer}</div> : null}\n </div>\n );\n }\n\n /* ---- cards: numbered step cards, a bar over them ------------------- */\n\n if (variant === \"cards\") {\n return (\n <div className={cx(\"grid gap-4\", className)}>\n {header(\n <div className=\"grid gap-2\">\n <p data-nums className=\"text-sm text-muted-foreground\">\n {allDone ? (\n <span className=\"font-medium text-success\">{doneLabel}</span>\n ) : (\n <>\n <span className=\"font-medium text-foreground\">{doneCount}</span> of {total} done\n </>\n )}\n </p>\n {bar}\n </div>,\n )}\n <ol className=\"grid gap-3\" aria-label={title}>\n {list.map((step, i) => {\n const state = stateOf(i);\n return (\n <li\n key={step.key}\n aria-current={state === \"current\" ? \"step\" : undefined}\n className={cx(\"rounded-lg border p-4\", state === \"current\" ? \"border-primary/40 bg-muted\" : \"border-border bg-card\")}\n >\n <div className=\"flex gap-3\">\n <Marker state={state} index={i} shape=\"number\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"font-medium\">\n <StepTitle step={step} state={state} />\n </p>\n {step.description ? (\n <p className={cx(\"mt-0.5 text-sm\", state === \"current\" ? \"text-muted-foreground\" : \"text-muted-foreground/80\")}>{step.description}</p>\n ) : null}\n {state === \"current\" ? (\n <div className=\"mt-3\">\n {action(step, i, \"icon\")}\n </div>\n ) : null}\n <StepError message={errors[step.key]} />\n </div>\n {step.icon ? (\n <Icon name={step.icon} size={20} className={cx(\"shrink-0\", state === \"current\" ? \"text-primary\" : \"text-muted-foreground/60\")} />\n ) : null}\n </div>\n </li>\n );\n })}\n </ol>\n {footer ? <div className=\"text-sm\">{footer}</div> : null}\n </div>\n );\n }\n\n /* ---- list: plain step rows, \"Step n of N\" and a short bar ---------- */\n\n return (\n <div className={cx(\"grid gap-4\", className)}>\n {header(\n <div className=\"flex items-center justify-end gap-3\">\n <span data-nums className=\"text-sm text-muted-foreground\">\n {allDone ? doneLabel : `Step ${current + 1} of ${total}`}\n </span>\n <div className=\"w-32\">{bar}</div>\n </div>,\n )}\n <ol className=\"grid gap-3\" aria-label={title}>\n {list.map((step, i) => {\n const state = stateOf(i);\n return (\n <li\n key={step.key}\n aria-current={state === \"current\" ? \"step\" : undefined}\n className={cx(\"rounded-lg border bg-card p-4\", state === \"current\" ? \"border-foreground/25\" : \"border-border\")}\n >\n <div className=\"flex items-start gap-3\">\n {state === \"done\" ? (\n <Marker state=\"done\" />\n ) : (\n <span aria-hidden=\"true\" data-nums className=\"flex size-5 shrink-0 items-center justify-center text-sm font-medium text-muted-foreground\">\n {i + 1}.\n </span>\n )}\n <div className=\"min-w-0 flex-1\">\n <p className=\"font-medium\">\n <StepTitle step={step} state={state} />\n </p>\n {step.description ? <p className=\"mt-1 text-sm text-muted-foreground\">{step.description}</p> : null}\n {state === \"current\" ? (\n <div className=\"mt-3\">\n {action(step, i)}\n </div>\n ) : null}\n <StepError message={errors[step.key]} />\n </div>\n </div>\n </li>\n );\n })}\n </ol>\n {footer ? <div className=\"rounded-lg bg-muted p-4 text-sm\">{footer}</div> : null}\n </div>\n );\n}\n"
23
+ },
24
+ {
25
+ "path": "registry/blocks/setup-checklist/README.md",
26
+ "type": "registry:file",
27
+ "target": "@components/makersclaw/blocks/setup-checklist/README.md",
28
+ "content": "# SetupChecklist\n\nThe steps to get set up, which of them are done, how far along the whole\nis, and the button for the next one. The steps are yours: their `done`\ncomes from the app's data (the profile has a name, a teammate was\ninvited), so the list is never a second record of the truth. A step's\nbutton goes out through `onAction`; while it runs the button spins, and\nwhen it fails the step says why.\n\n```jsx\nimport { SetupChecklist } from \"../components/blocks/setup-checklist\"; // a Makersclaw app (\"./blocks/setup-checklist\" from components/)\nimport { SetupChecklist } from \"@/components/makersclaw/blocks/setup-checklist\"; // a shadcn project\n```\n\nA client component. Work out `done` for each step in the server surface\n(README §4) and pass the steps in.\n\n## The data\n\n`steps` — in order:\n`[{ key, title, description?, actionLabel?, href?, icon?, summary?, done }]`.\n\n- `key` — unique, stable.\n- `actionLabel` — the step's button (\"Send invites\"); default \"Start\"\n (\"Open\" once done).\n- `href` — the button is a link to the page where the step is done;\n `onAction` still hears about the click.\n- `icon` — a kit `Icon` name: on the right of a card and in its button\n (`cards`), above the text (`accordion`).\n- `summary` — `accordion`: the bold line in the step's panel.\n\nThe next step is the first one not done.\n\n## Props\n\n- `steps` — as above. Required.\n- `variant` — default `\"collapsible\"`:\n - `collapsible` — a card: the title, a ring with \"2 of 6 done\", a \"⋯\"\n menu (Dismiss and `menuItems`), then each step a folding row with its\n description and button. The next step starts open, and when the open\n step gets done the next one opens.\n - `cards` — the title, \"1 of 4 done\" and a bar, then a numbered card\n per step. Only the next step shows its button.\n - `list` — the title, \"Step 2 of 4\" and a short bar, then a row per\n step, numbered \"1.\", \"2.\"; the next one is outlined and shows its\n button. `footer` sits in a filled panel below (\"Need a hand?\").\n - `accordion` — each step a bordered folding row; its panel is centred:\n icon, `summary`, description, button. Pair it with `sequential`.\n- `onAction(step)` — the step's button (or a click on its link). Do the\n thing, or open your `Sheet` for it; return `{ ok: true }` or\n `{ ok: false, error }` (a throw counts as failed) and the step shows the\n error under its button. Mark it done by passing the step back with\n `done: true`; the block never marks a step itself. Without `onAction`\n only steps with an `href` have a button.\n- `sequential` — a step's button stays disabled, with \"Finish the steps\n before this one first\", until every step before it is done. Default\n `false`: any step can be done in any order.\n- `title` — default \"Get set up\". `description` — a line under it.\n- `onDismiss()` — adds Dismiss to the \"⋯\" menu (`collapsible` shows the\n menu in its header, the others beside the title). Hide the checklist\n and remember that you did; return `{ ok: false, error }` to show why it\n could not.\n- `menuItems` — more \"⋯\" items, kit `Menu` shape: `[{ label, onSelect,\n icon? }]` (\"Send feedback\").\n- `footer` — a node under the steps: help, a link to the docs.\n- `doneLabel` — what the header says when every step is done, default\n \"All set\" (the bar turns to the success tone).\n- `loading` — skeleton rows while the steps load.\n- `error` — a string: the steps did not load. Shown in a notice with\n Retry when `onRetry` is given.\n- `className` — layout around it (width, margin). Never colour.\n\nWith no steps it says \"Nothing to set up\".\n\n## Example\n\n```jsx\n// surfaces/home.jsx — server: work out what is done\nimport { loadRows } from \"../lib/data\";\nimport GettingStarted from \"../components/GettingStarted\";\n\nexport default async function Home() {\n const [{ rows: members }, { rows: deals }] = await Promise.all([\n loadRows({ source: \"public.members\" }),\n loadRows({ source: \"public.deals\", limit: 1 }),\n ]);\n return <GettingStarted invited={members.length > 1} hasDeal={deals.length > 0} />;\n}\n```\n\n```jsx\n// components/GettingStarted.jsx — client\n\"use client\";\nimport { SetupChecklist } from \"./blocks/setup-checklist\";\n\nexport default function GettingStarted({ invited, hasDeal, onHide }) {\n return (\n <SetupChecklist\n title=\"Get started\"\n steps={[\n { key: \"invite\", title: \"Invite your team\", description: \"Bring in the people who own accounts.\", actionLabel: \"Invite\", href: \"/team\", done: invited },\n { key: \"deal\", title: \"Add your first deal\", description: \"One deal is enough to see the pipeline work.\", actionLabel: \"Add a deal\", href: \"/deals?new=1\", done: hasDeal },\n ]}\n onDismiss={onHide}\n />\n );\n}\n```\n\nIn order, with buttons that do the work in place:\n\n```jsx\n<SetupChecklist\n variant=\"accordion\"\n sequential\n title=\"Quick start\"\n steps={steps}\n onAction={async (step) => {\n const res = await runSetupStep(step.key); // your server action, { ok, error }\n if (res.ok) router.refresh(); // the step comes back done\n return res;\n }}\n/>\n```\n\n## What it will not do\n\n- Decide a step is done: `done` is yours, from your data.\n- Remember that it was dismissed: `onDismiss` is where you save that.\n- Nest steps inside steps, or show a running job's stages: that is the kit\n `Steps` or the `job-panel` block.\n- Page through a wizard of forms: each step's button goes where the work\n is.\n\nA checklist it cannot be is your own component in `components/`, built\nfrom the kit — not an edit to this block. Its files are fixed: `npm run\ncheck` refuses a change, and `makersclaw add setup-checklist` restores\nthem.\n\n## Credits\n\nPorted from blocks.so (https://blocks.so, onboarding 01–04, MIT), rewritten on the Makersclaw kit.\n"
29
+ }
30
+ ],
31
+ "categories": [
32
+ "onboarding",
33
+ "checklist",
34
+ "setup",
35
+ "getting started",
36
+ "steps",
37
+ "progress",
38
+ "tasks",
39
+ "welcome",
40
+ "quick start",
41
+ "wizard",
42
+ "dismiss"
43
+ ],
44
+ "docs": "Installed to components/makersclaw/blocks/setup-checklist. Import: import { SetupChecklist } from \"@/components/makersclaw/blocks/setup-checklist\". Props and an example: components/makersclaw/blocks/setup-checklist/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
45
+ "meta": {
46
+ "makersclaw": {
47
+ "version": "0.3.1",
48
+ "useWhen": "A new account, workspace or shop has a few things to do before it is useful (complete the profile, invite the team, connect a tool, launch) and the founder should see what is left. Not for a running job's stages (kit Steps or job-panel), and not for a multi-page form wizard."
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,52 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "share-dialog",
4
+ "type": "registry:block",
5
+ "title": "ShareDialog",
6
+ "description": "Who can see a record and how to let more people in: invite by email (comma separated, with an access level), each person with their access in a menu (can view, can edit, remove), sharing switches, and a link with a copy button that turns to a check. A dialog, or the same body inline (SharePanel). Every change is an async callback; a failure shows and the old value comes back.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.1/r/core.json"
9
+ ],
10
+ "dependencies": [],
11
+ "files": [
12
+ {
13
+ "path": "registry/blocks/share-dialog/index.js",
14
+ "type": "registry:component",
15
+ "target": "@components/makersclaw/blocks/share-dialog/index.js",
16
+ "content": "// The ShareDialog block — `makersclaw add share-dialog`. Fixed: compose\n// it through its props (README.md beside this file).\nexport { ShareDialog, SharePanel } from \"./share-dialog\";\n"
17
+ },
18
+ {
19
+ "path": "registry/blocks/share-dialog/share-dialog.jsx",
20
+ "type": "registry:component",
21
+ "target": "@components/makersclaw/blocks/share-dialog/share-dialog.jsx",
22
+ "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { Avatar, Badge, Button, Icon, InputGroup, Item, Menu, Notice, Select, Sheet, Switch, cx } from \"../../ui\";\n\n// ShareDialog — who can see a thing, and how to let more people in:\n// invite by email, each person's access, a link to copy, a few sharing\n// switches. SharePanel is the same body without the dialog, for a\n// settings page. Every change goes out through an async callback that\n// returns { ok } or { ok: false, error }; the dialog shows the failure\n// and puts the old value back. See README.md beside this file.\n\nconst DEFAULT_ROLES = [\n { value: \"view\", label: \"Can view\" },\n { value: \"edit\", label: \"Can edit\" },\n];\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\nfunction failure(result) {\n return result && result.ok === false ? result.error || \"That did not save. Try again.\" : null;\n}\n\nasync function attempt(fn) {\n try {\n return failure(await fn());\n } catch (e) {\n return e?.message || String(e);\n }\n}\n\n/** \"a@x.com, b@y.com; c@z.com\" → { good: [...], bad: [...] }, deduped. */\nfunction parseEmails(text) {\n const all = String(text)\n .split(/[\\s,;]+/)\n .map((s) => s.trim())\n .filter(Boolean);\n const seen = new Set();\n const good = [];\n const bad = [];\n for (const e of all) {\n const k = e.toLowerCase();\n if (seen.has(k)) continue;\n seen.add(k);\n (EMAIL.test(e) ? good : bad).push(e);\n }\n return { good, bad };\n}\n\nfunction InviteRow({ onInvite, roles, inviteRole, placeholder, people, label }) {\n const id = useId();\n const [text, setText] = useState(\"\");\n const [role, setRole] = useState(inviteRole ?? roles[0]?.value);\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState(null);\n const [note, setNote] = useState(null);\n\n async function submit(e) {\n e.preventDefault();\n if (busy) return;\n setNote(null);\n const { good, bad } = parseEmails(text);\n if (!good.length && !bad.length) {\n setError(\"Type at least one email address.\");\n return;\n }\n if (bad.length) {\n setError(`${bad.join(\", \")} ${bad.length === 1 ? \"is not an email address\" : \"are not email addresses\"}.`);\n return;\n }\n const have = new Set((people ?? []).map((p) => String(p.email ?? \"\").toLowerCase()));\n const fresh = good.filter((e) => !have.has(e.toLowerCase()));\n if (!fresh.length) {\n setError(good.length === 1 ? `${good[0]} already has access.` : \"Everyone listed already has access.\");\n return;\n }\n setError(null);\n setBusy(true);\n const failed = await attempt(() => onInvite(fresh, role));\n setBusy(false);\n if (failed) {\n setError(failed);\n return;\n }\n setText(\"\");\n setNote(fresh.length === 1 ? `Invited ${fresh[0]}.` : `Invited ${fresh.length} people.`);\n }\n\n const picker =\n roles.length > 1 && !inviteRole ? (\n <Select\n aria-label=\"Access for the people you invite\"\n value={role}\n onChange={(e) => setRole(e.target.value)}\n options={roles}\n disabled={busy}\n className=\"sm:w-36\"\n />\n ) : null;\n\n return (\n <form onSubmit={submit} className=\"grid gap-2\" noValidate>\n <label htmlFor={id} className=\"text-sm font-medium text-foreground\">\n {label}\n </label>\n <div className=\"flex flex-col gap-2 sm:flex-row\">\n <div className=\"min-w-0 flex-1\">\n <InputGroup\n id={id}\n icon=\"user-plus\"\n type=\"text\"\n inputMode=\"email\"\n autoComplete=\"off\"\n placeholder={placeholder}\n value={text}\n onChange={(e) => {\n setText(e.target.value);\n if (error) setError(null);\n }}\n invalid={Boolean(error)}\n aria-describedby={error ? `${id}-error` : note ? `${id}-note` : undefined}\n disabled={busy}\n />\n </div>\n {picker}\n <Button type=\"submit\" variant=\"primary\" loading={busy}>\n Invite\n </Button>\n </div>\n {error ? (\n <p id={`${id}-error`} role=\"alert\" className=\"text-xs text-destructive\">\n {error}\n </p>\n ) : note ? (\n <p id={`${id}-note`} role=\"status\" className=\"text-xs text-muted-foreground\">\n {note}\n </p>\n ) : null}\n </form>\n );\n}\n\nfunction PersonRow({ person, roles, onRoleChange, onRemove, onError, you }) {\n const [role, setRole] = useState(person.role);\n const [busy, setBusy] = useState(false);\n useEffect(() => setRole(person.role), [person.role]);\n\n const known = roles.find((r) => r.value === role);\n const editable = !person.locked && known && (onRoleChange || onRemove);\n\n async function change(next) {\n if (next === role || busy) return;\n const before = role;\n setRole(next); // shows at once; goes back if the save fails\n setBusy(true);\n const failed = await attempt(() => onRoleChange(person, next));\n setBusy(false);\n if (failed) {\n setRole(before);\n onError(`${person.name ?? person.email}: ${failed}`);\n }\n }\n\n async function remove() {\n setBusy(true);\n const failed = await attempt(() => onRemove(person));\n setBusy(false);\n if (failed) onError(`${person.name ?? person.email} still has access: ${failed}`);\n }\n\n const roleLabel = known?.label ?? person.roleLabel ?? String(role ?? \"\");\n\n const items = editable\n ? [\n onRoleChange && { type: \"radio\", label: \"Access\", value: role, onValueChange: change, options: roles },\n onRoleChange && onRemove && { type: \"separator\" },\n onRemove && { label: \"Remove access\", icon: \"trash\", tone: \"danger\", onSelect: remove },\n ]\n : [];\n\n const meta = editable ? (\n <Menu\n label={`${roleLabel}, access for ${person.name ?? person.email}`}\n trigger={\n <span className=\"inline-flex items-center gap-1 text-sm\">\n {busy ? <span className=\"text-muted-foreground\">Saving…</span> : roleLabel}\n <Icon name=\"chevron-down\" size={14} className=\"text-muted-foreground\" />\n </span>\n }\n items={items}\n />\n ) : (\n <Badge variant=\"outline\">{roleLabel}</Badge>\n );\n\n return (\n <li>\n <Item\n size=\"sm\"\n media={<Avatar name={person.name ?? person.email} src={person.src} size=\"sm\" />}\n title={\n <>\n {person.name ?? person.email}\n {you ? <span className=\"font-normal text-muted-foreground\"> (you)</span> : null}\n </>\n }\n description={person.name ? person.email : person.pending ? \"Invite sent\" : undefined}\n meta={person.pending && person.name ? <Badge variant=\"soft\">Invited</Badge> : undefined}\n actions={meta}\n className={cx(busy && \"opacity-70\")}\n />\n </li>\n );\n}\n\nfunction LinkRow({ link, onCopy, onPreview, previewHref, label }) {\n const id = useId();\n const [copied, setCopied] = useState(false);\n const [error, setError] = useState(null);\n const timer = useRef(null);\n useEffect(() => () => clearTimeout(timer.current), []);\n\n async function copy() {\n setError(null);\n let failed = null;\n try {\n if (!navigator.clipboard?.writeText) throw new Error(\"This browser blocks copying. Select the link and copy it by hand.\");\n await navigator.clipboard.writeText(link);\n failed = await attempt(() => onCopy?.(link));\n } catch (e) {\n failed = e?.message || \"Copying failed. Select the link and copy it by hand.\";\n }\n if (failed) {\n setError(failed);\n return;\n }\n setCopied(true);\n clearTimeout(timer.current);\n timer.current = setTimeout(() => setCopied(false), 1600);\n }\n\n return (\n <div className=\"grid gap-2\">\n <label htmlFor={id} className=\"text-sm font-medium text-foreground\">\n {label}\n </label>\n <InputGroup\n id={id}\n icon=\"link\"\n readOnly\n value={link}\n onFocus={(e) => e.target.select()}\n action={{ label: copied ? \"Copied\" : \"Copy link\", icon: copied ? \"check\" : \"copy\", onClick: copy }}\n />\n <p role=\"status\" className={cx(\"text-xs\", error ? \"text-destructive\" : \"text-muted-foreground\", !error && !copied && \"sr-only\")}>\n {error ?? (copied ? \"Link copied.\" : \"\")}\n </p>\n <div className=\"flex flex-col gap-2 sm:flex-row\">\n <Button variant=\"secondary\" icon={copied ? \"check\" : \"copy\"} onClick={copy} className=\"sm:flex-1\">\n {copied ? \"Copied\" : \"Copy link\"}\n </Button>\n {previewHref || onPreview ? (\n <Button variant=\"secondary\" icon=\"external\" href={previewHref} onClick={onPreview} target={previewHref ? \"_blank\" : undefined} rel={previewHref ? \"noreferrer\" : undefined} className=\"sm:flex-1\">\n Preview\n </Button>\n ) : null}\n </div>\n </div>\n );\n}\n\nfunction SettingRow({ setting, onSettingChange, onError }) {\n const id = useId();\n const [on, setOn] = useState(Boolean(setting.checked));\n const [busy, setBusy] = useState(false);\n useEffect(() => setOn(Boolean(setting.checked)), [setting.checked]);\n\n async function toggle(e) {\n const next = e.target.checked;\n setOn(next);\n setBusy(true);\n const failed = await attempt(() => onSettingChange?.(setting.id, next));\n setBusy(false);\n if (failed) {\n setOn(!next);\n onError(`${setting.label}: ${failed}`);\n }\n }\n\n return (\n <div className=\"flex items-center justify-between gap-4 rounded-md border border-border px-3\">\n <div className=\"min-w-0 py-2\">\n <label htmlFor={id} className=\"flex items-center gap-2 text-sm font-medium\">\n {setting.icon ? <Icon name={setting.icon} size={16} className=\"shrink-0 text-muted-foreground\" /> : null}\n {setting.label}\n </label>\n {setting.description ? <p className=\"text-xs text-muted-foreground\">{setting.description}</p> : null}\n </div>\n <Switch id={id} checked={on} onChange={toggle} disabled={busy || setting.disabled} />\n </div>\n );\n}\n\n/** The body: invite, people, settings, link — each shows when its data or\n * callback is given. */\nexport function SharePanel({\n people,\n roles = DEFAULT_ROLES,\n onInvite,\n inviteRole,\n onRoleChange,\n onRemove,\n link,\n onCopy,\n onPreview,\n previewHref,\n settings,\n onSettingChange,\n youId,\n inviteLabel = \"Invite by email\",\n invitePlaceholder = \"Emails, separated by commas\",\n peopleLabel = \"People with access\",\n linkLabel = \"Share link\",\n empty = \"Only you have access so far.\",\n className,\n}) {\n const [error, setError] = useState(null);\n const list = people ?? null;\n\n return (\n <div className={cx(\"grid gap-5\", className)}>\n {onInvite ? <InviteRow onInvite={onInvite} roles={roles} inviteRole={inviteRole} placeholder={invitePlaceholder} people={list} label={inviteLabel} /> : null}\n\n {settings?.length ? (\n <div className=\"grid gap-2\">\n {settings.map((s) => (\n <SettingRow key={s.id} setting={s} onSettingChange={onSettingChange} onError={setError} />\n ))}\n </div>\n ) : null}\n\n {list ? (\n <section className=\"grid gap-1\" aria-label={peopleLabel}>\n <h3 className=\"text-sm font-medium\">{peopleLabel}</h3>\n {list.length ? (\n <ul className=\"grid divide-y divide-border\">\n {list.map((p) => (\n <PersonRow\n key={p.id ?? p.email}\n person={p}\n roles={roles}\n onRoleChange={onRoleChange}\n onRemove={onRemove}\n onError={setError}\n you={youId != null && (p.id ?? p.email) === youId}\n />\n ))}\n </ul>\n ) : (\n <p className=\"py-3 text-sm text-muted-foreground\">{empty}</p>\n )}\n </section>\n ) : null}\n\n {error ? (\n <Notice tone=\"danger\" onDismiss={() => setError(null)}>\n {error}\n </Notice>\n ) : null}\n\n {link ? <LinkRow link={link} onCopy={onCopy} onPreview={onPreview} previewHref={previewHref} label={linkLabel} /> : null}\n </div>\n );\n}\n\n/** SharePanel in the kit's Sheet: a dialog on a wide window, a bottom\n * sheet on a phone. Controlled: `open` + `onClose`. */\nexport function ShareDialog({ open, onClose, title = \"Share\", description, footer, className, ...panel }) {\n return (\n <Sheet open={open} onClose={onClose} side=\"center\" title={title} description={description} footer={footer} className={className}>\n {open ? <SharePanel {...panel} /> : null}\n </Sheet>\n );\n}\n"
23
+ },
24
+ {
25
+ "path": "registry/blocks/share-dialog/README.md",
26
+ "type": "registry:file",
27
+ "target": "@components/makersclaw/blocks/share-dialog/README.md",
28
+ "content": "# ShareDialog\n\nWho can see a record, and how to let more people in. An invite field\nthat takes several emails at once (with an access level), the people who\nalready have access (each with a menu: can view, can edit, remove),\nsharing switches (\"Allow comments\"), and a link with a copy button that\nturns to a check. Each section shows when you give its data or callback.\n`ShareDialog` puts it in the kit's `Sheet` (a dialog; a bottom sheet on a\nphone); `SharePanel` is the same body inline, for a settings page.\n\nEvery change goes out through an async callback that returns `{ ok: true }`\nor `{ ok: false, error }`. A role change or a switch shows at once and goes\nback when the save fails, with the reason on screen.\n\n```jsx\nimport { ShareDialog, SharePanel } from \"../components/blocks/share-dialog\"; // a Makersclaw app (\"./blocks/share-dialog\" from components/)\nimport { ShareDialog, SharePanel } from \"@/components/makersclaw/blocks/share-dialog\"; // a shadcn project\n```\n\nA client component: render it from a `\"use client\"` component.\n\n## The data\n\n`people` is who has access now: `[{ id?, name?, email, src?, role, roleLabel?, locked?, pending? }]`.\n\n- `role` is one of `roles`' values. A role outside them (an owner) shows\n as a badge, with `roleLabel` as its words.\n- `locked: true` — no menu for this person (the owner, yourself).\n- `pending: true` — invited, not joined yet: \"Invite sent\" or an\n \"Invited\" badge.\n- `src` — a photo; the kit's `Avatar` shows initials without one.\n\n## Props\n\nBoth components take these. Everything is optional; a section shows only\nwhen its data or callback is there.\n\n- `people` — the list above. `[]` shows `empty`; leave it out to hide the\n section.\n- `roles` — the access levels, in order: `[{ value, label }]`. Default\n `[{ value: \"view\", label: \"Can view\" }, { value: \"edit\", label: \"Can edit\" }]`.\n- `onInvite(emails, role)` — shows the invite field. Async, `{ ok, error }`.\n The field splits on commas, spaces and semicolons, drops duplicates and\n people who already have access, and refuses anything that is not an\n email before it calls you. Add the invited people to `people` yourself\n (with `pending: true`). With two or more `roles`, a picker beside the\n field chooses the role.\n- `inviteRole` — every invite gets this role, and no picker shows.\n- `onRoleChange(person, role)` — each person's access becomes a menu.\n Async, `{ ok, error }`. Update `people` on success.\n- `onRemove(person)` — \"Remove access\" in that menu. Async. Take the\n person out of `people` on success.\n- `link` — a URL to share: a read-only field with a copy button, then a\n \"Copy link\" button. Copying uses the browser's clipboard. A blocked\n clipboard shows a message.\n- `onCopy(link)` — called after a copy (analytics, \"link created\"). Async\n and optional; a failure shows.\n- `previewHref` or `onPreview` — a \"Preview\" button beside \"Copy link\".\n- `settings` — switches: `[{ id, label, description?, icon?, checked, disabled? }]`.\n- `onSettingChange(id, next)` — async, `{ ok, error }`. The switch goes\n back when it fails.\n- `youId` — the `id` (or email) of the person looking: \"(you)\" after their name.\n- `inviteLabel` · `invitePlaceholder` · `peopleLabel` · `linkLabel` ·\n `empty` — the words. Defaults: \"Invite by email\", \"Emails, separated by\n commas\", \"People with access\", \"Share link\", \"Only you have access so far.\"\n- `className` — on the panel.\n\n`ShareDialog` also takes:\n\n- `open` · `onClose()` — controlled. Esc, a press outside and the ✕ close it.\n- `title` — default \"Share\". `description` — a line under it.\n- `footer` — buttons under the body (a \"Done\").\n\n## Example\n\n```jsx\n\"use client\";\nimport { useState } from \"react\";\nimport { Button } from \"../components/ui\";\nimport { ShareDialog } from \"../components/blocks/share-dialog\";\n\nexport function ShareProject({ project, members, invite, setRole, remove }) {\n const [open, setOpen] = useState(false);\n return (\n <>\n <Button icon=\"share\" onClick={() => setOpen(true)}>Share</Button>\n <ShareDialog\n open={open}\n onClose={() => setOpen(false)}\n title={`Share “${project.name}”`}\n people={members}\n onInvite={(emails, role) => invite(project.id, emails, role)} // server actions returning { ok, error }\n onRoleChange={(person, role) => setRole(project.id, person.id, role)}\n onRemove={(person) => remove(project.id, person.id)}\n link={`https://app.example.com/p/${project.id}`}\n />\n </>\n );\n}\n```\n\n## What it will not do\n\n- Send the invite email or create the access row: `onInvite` does.\n- Search your directory as you type. The field takes addresses; for a\n people picker, use the kit's `Combobox`.\n- Make the link public or private by itself. Put that switch in `settings`\n and change `link` from your callback.\n- Show a person's access per page or field. It has one role per person.\n\n## Credits\n\nPorted from blocks.so (https://blocks.so, dialogs 08 and 09, MIT) and Creative Tim UI (https://github.com/creativetimofficial/ui, modals-03, MIT), rewritten on the Makersclaw kit.\n"
29
+ }
30
+ ],
31
+ "categories": [
32
+ "share",
33
+ "invite",
34
+ "members",
35
+ "collaborators",
36
+ "access",
37
+ "permissions",
38
+ "roles",
39
+ "team",
40
+ "link",
41
+ "copy link",
42
+ "dialog",
43
+ "modal"
44
+ ],
45
+ "docs": "Installed to components/makersclaw/blocks/share-dialog. Import: import { ShareDialog, SharePanel } from \"@/components/makersclaw/blocks/share-dialog\". Props and an example: components/makersclaw/blocks/share-dialog/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
46
+ "meta": {
47
+ "makersclaw": {
48
+ "version": "0.3.1",
49
+ "useWhen": "A record, project, document or workspace can be shared: an \"Invite\" or \"Share\" button, a members list with roles, a public link to copy."
50
+ }
51
+ }
52
+ }