makersclaw 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (478) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -1
  3. package/THIRD-PARTY-NOTICES.md +180 -0
  4. package/package.json +15 -8
  5. package/r/annotate.json +62 -0
  6. package/r/approval-request.json +52 -0
  7. package/r/audio-player.json +74 -0
  8. package/r/auth-form.json +55 -0
  9. package/r/board.json +55 -0
  10. package/r/calendar-view.json +105 -0
  11. package/r/cart.json +62 -0
  12. package/r/chart.json +83 -0
  13. package/r/chat.json +95 -0
  14. package/r/code-editor.json +94 -0
  15. package/r/comment-box.json +64 -0
  16. package/r/contact-section.json +53 -0
  17. package/r/core.json +341 -0
  18. package/r/csv-import.json +63 -0
  19. package/r/data-table.json +69 -0
  20. package/r/diagram.json +74 -0
  21. package/r/doc-editor.json +82 -0
  22. package/r/doc-page.json +61 -0
  23. package/r/email-render.json +73 -0
  24. package/r/faq.json +50 -0
  25. package/r/file-upload.json +54 -0
  26. package/r/flow-canvas.json +93 -0
  27. package/r/footer.json +52 -0
  28. package/r/gantt.json +70 -0
  29. package/r/job-panel.json +56 -0
  30. package/r/json-editor.json +66 -0
  31. package/r/map.json +89 -0
  32. package/r/menubar.json +52 -0
  33. package/r/mind-map.json +67 -0
  34. package/r/navigation-menu.json +52 -0
  35. package/r/order-detail.json +54 -0
  36. package/r/org-chart.json +66 -0
  37. package/r/page-builder.json +105 -0
  38. package/r/payment-methods.json +51 -0
  39. package/r/pdf-document.json +79 -0
  40. package/r/pdf-viewer.json +88 -0
  41. package/r/product-detail.json +76 -0
  42. package/r/product-grid.json +65 -0
  43. package/r/record-form.json +67 -0
  44. package/r/record-notes.json +55 -0
  45. package/r/registry.json +3612 -0
  46. package/r/search-dialog.json +51 -0
  47. package/r/settings-list.json +66 -0
  48. package/r/setup-checklist.json +51 -0
  49. package/r/share-dialog.json +52 -0
  50. package/r/sidebar.json +69 -0
  51. package/r/signature-pad.json +72 -0
  52. package/r/sketch-canvas.json +99 -0
  53. package/r/spreadsheet.json +90 -0
  54. package/r/stat-cards.json +66 -0
  55. package/r/task-list.json +63 -0
  56. package/r/task-model.json +98 -0
  57. package/r/testimonials.json +52 -0
  58. package/r/transaction-list.json +53 -0
  59. package/r/whiteboard.json +66 -0
  60. package/registry/blocks/annotate/README.md +179 -0
  61. package/registry/blocks/annotate/annotate-data.js +250 -0
  62. package/registry/blocks/annotate/annotate.jsx +646 -0
  63. package/registry/blocks/annotate/block.json +10 -0
  64. package/registry/blocks/annotate/demo.jsx +135 -0
  65. package/registry/blocks/annotate/index.js +15 -0
  66. package/registry/blocks/approval-request/README.md +116 -0
  67. package/registry/blocks/approval-request/approval-request.jsx +386 -0
  68. package/registry/blocks/approval-request/block.json +10 -0
  69. package/registry/blocks/approval-request/demo.jsx +127 -0
  70. package/registry/blocks/approval-request/index.js +4 -0
  71. package/registry/blocks/audio-player/README.md +167 -0
  72. package/registry/blocks/audio-player/audio-player.jsx +623 -0
  73. package/registry/blocks/audio-player/block.json +14 -0
  74. package/registry/blocks/audio-player/colors.js +109 -0
  75. package/registry/blocks/audio-player/demo.jsx +209 -0
  76. package/registry/blocks/audio-player/index.js +6 -0
  77. package/registry/blocks/audio-player/time.js +67 -0
  78. package/registry/blocks/audio-player/wavesurfer.js +9 -0
  79. package/registry/blocks/auth-form/README.md +126 -0
  80. package/registry/blocks/auth-form/auth-form.jsx +568 -0
  81. package/registry/blocks/auth-form/block.json +10 -0
  82. package/registry/blocks/auth-form/demo.jsx +131 -0
  83. package/registry/blocks/auth-form/index.js +3 -0
  84. package/registry/blocks/board/README.md +64 -13
  85. package/registry/blocks/board/block.json +2 -2
  86. package/registry/blocks/board/board.jsx +237 -94
  87. package/registry/blocks/board/demo.jsx +103 -24
  88. package/registry/blocks/calendar-view/README.md +180 -0
  89. package/registry/blocks/calendar-view/agenda.jsx +147 -0
  90. package/registry/blocks/calendar-view/block.json +76 -0
  91. package/registry/blocks/calendar-view/calendar-model.js +442 -0
  92. package/registry/blocks/calendar-view/calendar.jsx +627 -0
  93. package/registry/blocks/calendar-view/demo.jsx +195 -0
  94. package/registry/blocks/calendar-view/draft.jsx +49 -0
  95. package/registry/blocks/calendar-view/index.js +39 -0
  96. package/registry/blocks/calendar-view/month.jsx +346 -0
  97. package/registry/blocks/calendar-view/paint.js +69 -0
  98. package/registry/blocks/calendar-view/time-grid.jsx +527 -0
  99. package/registry/blocks/calendar-view/tray.jsx +87 -0
  100. package/registry/blocks/cart/README.md +146 -0
  101. package/registry/blocks/cart/block.json +10 -0
  102. package/registry/blocks/cart/cart.jsx +597 -0
  103. package/registry/blocks/cart/demo.jsx +98 -0
  104. package/registry/blocks/cart/index.js +4 -0
  105. package/registry/blocks/cart/money.js +28 -0
  106. package/registry/blocks/chart/README.md +316 -0
  107. package/registry/blocks/chart/block.json +10 -0
  108. package/registry/blocks/chart/cartesian.jsx +642 -0
  109. package/registry/blocks/chart/core.jsx +262 -0
  110. package/registry/blocks/chart/demo.jsx +357 -0
  111. package/registry/blocks/chart/heatmap.jsx +336 -0
  112. package/registry/blocks/chart/index.js +8 -0
  113. package/registry/blocks/chart/pie.jsx +245 -0
  114. package/registry/blocks/chart/sparkline.jsx +88 -0
  115. package/registry/blocks/chat/README.md +273 -0
  116. package/registry/blocks/chat/attachment.jsx +95 -0
  117. package/registry/blocks/chat/block.json +10 -0
  118. package/registry/blocks/chat/chat.jsx +132 -0
  119. package/registry/blocks/chat/composer.jsx +364 -0
  120. package/registry/blocks/chat/demo.jsx +578 -0
  121. package/registry/blocks/chat/header.jsx +34 -0
  122. package/registry/blocks/chat/index.js +9 -0
  123. package/registry/blocks/chat/message.jsx +450 -0
  124. package/registry/blocks/chat/scroller.jsx +247 -0
  125. package/registry/blocks/chat/text.jsx +104 -0
  126. package/registry/blocks/code-editor/README.md +210 -0
  127. package/registry/blocks/code-editor/block.json +22 -0
  128. package/registry/blocks/code-editor/code-editor.jsx +534 -0
  129. package/registry/blocks/code-editor/codemirror.js +38 -0
  130. package/registry/blocks/code-editor/demo.jsx +205 -0
  131. package/registry/blocks/code-editor/index.js +8 -0
  132. package/registry/blocks/code-editor/json.js +317 -0
  133. package/registry/blocks/code-editor/sql.js +136 -0
  134. package/registry/blocks/code-editor/text.js +101 -0
  135. package/registry/blocks/code-editor/theme.js +186 -0
  136. package/registry/blocks/comment-box/README.md +123 -0
  137. package/registry/blocks/comment-box/block.json +10 -0
  138. package/registry/blocks/comment-box/comment-body.jsx +217 -0
  139. package/registry/blocks/comment-box/comment-box.jsx +198 -0
  140. package/registry/blocks/comment-box/comment-time.jsx +77 -0
  141. package/registry/blocks/comment-box/demo.jsx +141 -0
  142. package/registry/blocks/comment-box/index.js +8 -0
  143. package/registry/blocks/contact-section/README.md +152 -0
  144. package/registry/blocks/contact-section/block.json +11 -0
  145. package/registry/blocks/contact-section/contact-section.jsx +509 -0
  146. package/registry/blocks/contact-section/demo.jsx +93 -0
  147. package/registry/blocks/contact-section/index.js +4 -0
  148. package/registry/blocks/csv-import/README.md +133 -0
  149. package/registry/blocks/csv-import/block.json +13 -0
  150. package/registry/blocks/csv-import/csv-import.jsx +479 -0
  151. package/registry/blocks/csv-import/demo.jsx +106 -0
  152. package/registry/blocks/csv-import/import.js +200 -0
  153. package/registry/blocks/csv-import/index.js +7 -0
  154. package/registry/blocks/data-table/README.md +386 -0
  155. package/registry/blocks/data-table/block.json +10 -0
  156. package/registry/blocks/data-table/data-table.jsx +1164 -0
  157. package/registry/blocks/data-table/demo.jsx +588 -0
  158. package/registry/blocks/data-table/index.js +5 -0
  159. package/registry/blocks/data-table/model.js +169 -0
  160. package/registry/blocks/diagram/README.md +108 -0
  161. package/registry/blocks/diagram/block.json +13 -0
  162. package/registry/blocks/diagram/demo.jsx +113 -0
  163. package/registry/blocks/diagram/diagram.jsx +481 -0
  164. package/registry/blocks/diagram/index.js +7 -0
  165. package/registry/blocks/diagram/mermaid.js +52 -0
  166. package/registry/blocks/diagram/palette.js +313 -0
  167. package/registry/blocks/diagram/source.js +98 -0
  168. package/registry/blocks/doc-editor/README.md +215 -0
  169. package/registry/blocks/doc-editor/block.json +15 -0
  170. package/registry/blocks/doc-editor/demo.jsx +225 -0
  171. package/registry/blocks/doc-editor/doc-editor.jsx +291 -0
  172. package/registry/blocks/doc-editor/doc.js +309 -0
  173. package/registry/blocks/doc-editor/index.js +7 -0
  174. package/registry/blocks/doc-editor/schema.jsx +56 -0
  175. package/registry/blocks/doc-editor/shadcn-classes.js +110 -0
  176. package/registry/blocks/doc-editor/theme.js +82 -0
  177. package/registry/blocks/doc-page/README.md +119 -0
  178. package/registry/blocks/doc-page/autosave.js +104 -0
  179. package/registry/blocks/doc-page/block.json +10 -0
  180. package/registry/blocks/doc-page/demo.jsx +137 -0
  181. package/registry/blocks/doc-page/doc-page.jsx +276 -0
  182. package/registry/blocks/doc-page/index.js +4 -0
  183. package/registry/blocks/email-render/README.md +151 -0
  184. package/registry/blocks/email-render/block.json +14 -0
  185. package/registry/blocks/email-render/demo.jsx +153 -0
  186. package/registry/blocks/email-render/doc-email.jsx +210 -0
  187. package/registry/blocks/email-render/email.jsx +221 -0
  188. package/registry/blocks/email-render/index.js +10 -0
  189. package/registry/blocks/email-render/render.js +50 -0
  190. package/registry/blocks/email-render/theme.js +215 -0
  191. package/registry/blocks/faq/README.md +101 -0
  192. package/registry/blocks/faq/block.json +11 -0
  193. package/registry/blocks/faq/demo.jsx +89 -0
  194. package/registry/blocks/faq/faq.jsx +220 -0
  195. package/registry/blocks/faq/index.js +4 -0
  196. package/registry/blocks/file-upload/README.md +138 -0
  197. package/registry/blocks/file-upload/block.json +10 -0
  198. package/registry/blocks/file-upload/demo.jsx +204 -0
  199. package/registry/blocks/file-upload/file-upload.jsx +635 -0
  200. package/registry/blocks/file-upload/index.js +4 -0
  201. package/registry/blocks/flow-canvas/README.md +420 -0
  202. package/registry/blocks/flow-canvas/block.json +13 -0
  203. package/registry/blocks/flow-canvas/board.js +819 -0
  204. package/registry/blocks/flow-canvas/chrome.jsx +649 -0
  205. package/registry/blocks/flow-canvas/context.jsx +35 -0
  206. package/registry/blocks/flow-canvas/demo.jsx +249 -0
  207. package/registry/blocks/flow-canvas/edges.jsx +107 -0
  208. package/registry/blocks/flow-canvas/flow-canvas.jsx +1329 -0
  209. package/registry/blocks/flow-canvas/index.js +41 -0
  210. package/registry/blocks/flow-canvas/nodes.jsx +392 -0
  211. package/registry/blocks/flow-canvas/theme.js +174 -0
  212. package/registry/blocks/footer/README.md +133 -0
  213. package/registry/blocks/footer/block.json +11 -0
  214. package/registry/blocks/footer/demo.jsx +119 -0
  215. package/registry/blocks/footer/footer.jsx +406 -0
  216. package/registry/blocks/footer/index.js +4 -0
  217. package/registry/blocks/gantt/README.md +220 -0
  218. package/registry/blocks/gantt/block.json +10 -0
  219. package/registry/blocks/gantt/demo.jsx +148 -0
  220. package/registry/blocks/gantt/gantt.jsx +923 -0
  221. package/registry/blocks/gantt/index.js +7 -0
  222. package/registry/blocks/gantt/rows.jsx +468 -0
  223. package/registry/blocks/gantt/schedule.js +278 -0
  224. package/registry/blocks/job-panel/README.md +86 -2
  225. package/registry/blocks/job-panel/block.json +3 -3
  226. package/registry/blocks/job-panel/demo.jsx +230 -3
  227. package/registry/blocks/job-panel/job-panel.jsx +169 -3
  228. package/registry/blocks/json-editor/README.md +133 -0
  229. package/registry/blocks/json-editor/block.json +10 -0
  230. package/registry/blocks/json-editor/demo.jsx +92 -0
  231. package/registry/blocks/json-editor/index.js +7 -0
  232. package/registry/blocks/json-editor/json-editor.jsx +305 -0
  233. package/registry/blocks/json-editor/json-tree.jsx +393 -0
  234. package/registry/blocks/json-editor/tree.js +225 -0
  235. package/registry/blocks/map/README.md +221 -0
  236. package/registry/blocks/map/block.json +14 -0
  237. package/registry/blocks/map/demo.jsx +201 -0
  238. package/registry/blocks/map/geo.js +198 -0
  239. package/registry/blocks/map/index.js +8 -0
  240. package/registry/blocks/map/list.jsx +128 -0
  241. package/registry/blocks/map/map-view.jsx +715 -0
  242. package/registry/blocks/map/maplibre.js +30 -0
  243. package/registry/blocks/map/marks.jsx +146 -0
  244. package/registry/blocks/map/theme.js +142 -0
  245. package/registry/blocks/menubar/README.md +109 -0
  246. package/registry/blocks/menubar/block.json +10 -0
  247. package/registry/blocks/menubar/demo.jsx +128 -0
  248. package/registry/blocks/menubar/index.js +4 -0
  249. package/registry/blocks/menubar/menubar.jsx +199 -0
  250. package/registry/blocks/mind-map/README.md +206 -0
  251. package/registry/blocks/mind-map/block.json +10 -0
  252. package/registry/blocks/mind-map/demo.jsx +103 -0
  253. package/registry/blocks/mind-map/index.js +31 -0
  254. package/registry/blocks/mind-map/mind-map.jsx +1125 -0
  255. package/registry/blocks/mind-map/topic.jsx +143 -0
  256. package/registry/blocks/mind-map/tree.js +645 -0
  257. package/registry/blocks/navigation-menu/README.md +124 -0
  258. package/registry/blocks/navigation-menu/block.json +10 -0
  259. package/registry/blocks/navigation-menu/demo.jsx +84 -0
  260. package/registry/blocks/navigation-menu/index.js +4 -0
  261. package/registry/blocks/navigation-menu/navigation-menu.jsx +448 -0
  262. package/registry/blocks/order-detail/README.md +109 -0
  263. package/registry/blocks/order-detail/block.json +10 -0
  264. package/registry/blocks/order-detail/demo.jsx +88 -0
  265. package/registry/blocks/order-detail/index.js +4 -0
  266. package/registry/blocks/order-detail/order-detail.jsx +354 -0
  267. package/registry/blocks/org-chart/README.md +145 -0
  268. package/registry/blocks/org-chart/block.json +10 -0
  269. package/registry/blocks/org-chart/demo.jsx +94 -0
  270. package/registry/blocks/org-chart/index.js +7 -0
  271. package/registry/blocks/org-chart/org-chart.jsx +631 -0
  272. package/registry/blocks/org-chart/org.js +245 -0
  273. package/registry/blocks/org-chart/person.jsx +96 -0
  274. package/registry/blocks/page-builder/README.md +257 -0
  275. package/registry/blocks/page-builder/block.json +14 -0
  276. package/registry/blocks/page-builder/demo.jsx +145 -0
  277. package/registry/blocks/page-builder/grapes.js +18 -0
  278. package/registry/blocks/page-builder/index.js +11 -0
  279. package/registry/blocks/page-builder/page-builder.jsx +704 -0
  280. package/registry/blocks/page-builder/page-view.jsx +33 -0
  281. package/registry/blocks/page-builder/page.js +118 -0
  282. package/registry/blocks/page-builder/panels.jsx +588 -0
  283. package/registry/blocks/page-builder/sections.js +209 -0
  284. package/registry/blocks/page-builder/theme.js +87 -0
  285. package/registry/blocks/page-builder/thumbs.jsx +189 -0
  286. package/registry/blocks/page-builder/tokens.js +329 -0
  287. package/registry/blocks/payment-methods/README.md +104 -0
  288. package/registry/blocks/payment-methods/block.json +10 -0
  289. package/registry/blocks/payment-methods/demo.jsx +71 -0
  290. package/registry/blocks/payment-methods/index.js +4 -0
  291. package/registry/blocks/payment-methods/payment-methods.jsx +302 -0
  292. package/registry/blocks/pdf-document/README.md +213 -0
  293. package/registry/blocks/pdf-document/block.json +13 -0
  294. package/registry/blocks/pdf-document/demo.jsx +136 -0
  295. package/registry/blocks/pdf-document/index.js +33 -0
  296. package/registry/blocks/pdf-document/money.js +83 -0
  297. package/registry/blocks/pdf-document/pdf-document.jsx +144 -0
  298. package/registry/blocks/pdf-document/primitives.jsx +271 -0
  299. package/registry/blocks/pdf-document/quote.jsx +179 -0
  300. package/registry/blocks/pdf-document/theme.js +193 -0
  301. package/registry/blocks/pdf-viewer/README.md +191 -0
  302. package/registry/blocks/pdf-viewer/block.json +14 -0
  303. package/registry/blocks/pdf-viewer/demo.jsx +252 -0
  304. package/registry/blocks/pdf-viewer/helpers.js +270 -0
  305. package/registry/blocks/pdf-viewer/index.js +8 -0
  306. package/registry/blocks/pdf-viewer/notes.jsx +195 -0
  307. package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1368 -0
  308. package/registry/blocks/pdf-viewer/pdfjs.js +37 -0
  309. package/registry/blocks/pdf-viewer/ssr-shim-end.js +6 -0
  310. package/registry/blocks/pdf-viewer/ssr-shim.js +44 -0
  311. package/registry/blocks/product-detail/README.md +217 -0
  312. package/registry/blocks/product-detail/block.json +10 -0
  313. package/registry/blocks/product-detail/demo.jsx +233 -0
  314. package/registry/blocks/product-detail/index.js +7 -0
  315. package/registry/blocks/product-detail/price.jsx +115 -0
  316. package/registry/blocks/product-detail/product-detail.jsx +348 -0
  317. package/registry/blocks/product-detail/product-media.jsx +102 -0
  318. package/registry/blocks/product-detail/product-options.jsx +144 -0
  319. package/registry/blocks/product-grid/README.md +162 -0
  320. package/registry/blocks/product-grid/block.json +10 -0
  321. package/registry/blocks/product-grid/demo.jsx +255 -0
  322. package/registry/blocks/product-grid/index.js +5 -0
  323. package/registry/blocks/product-grid/product-card.jsx +232 -0
  324. package/registry/blocks/product-grid/product-grid.jsx +235 -0
  325. package/registry/blocks/record-form/README.md +196 -0
  326. package/registry/blocks/record-form/block.json +10 -0
  327. package/registry/blocks/record-form/demo.jsx +525 -0
  328. package/registry/blocks/record-form/index.js +4 -0
  329. package/registry/blocks/record-form/record-fields.jsx +457 -0
  330. package/registry/blocks/record-form/record-form.jsx +415 -0
  331. package/registry/blocks/record-form/rules.js +81 -0
  332. package/registry/blocks/record-notes/README.md +139 -0
  333. package/registry/blocks/record-notes/block.json +10 -0
  334. package/registry/blocks/record-notes/demo.jsx +179 -0
  335. package/registry/blocks/record-notes/index.js +4 -0
  336. package/registry/blocks/record-notes/record-notes.jsx +266 -0
  337. package/registry/blocks/search-dialog/README.md +108 -0
  338. package/registry/blocks/search-dialog/block.json +10 -0
  339. package/registry/blocks/search-dialog/demo.jsx +116 -0
  340. package/registry/blocks/search-dialog/index.js +3 -0
  341. package/registry/blocks/search-dialog/search-dialog.jsx +235 -0
  342. package/registry/blocks/settings-list/README.md +171 -0
  343. package/registry/blocks/settings-list/block.json +10 -0
  344. package/registry/blocks/settings-list/demo.jsx +333 -0
  345. package/registry/blocks/settings-list/index.js +4 -0
  346. package/registry/blocks/settings-list/row-state.jsx +93 -0
  347. package/registry/blocks/settings-list/settings-list.jsx +349 -0
  348. package/registry/blocks/settings-list/settings-matrix.jsx +118 -0
  349. package/registry/blocks/setup-checklist/README.md +142 -0
  350. package/registry/blocks/setup-checklist/block.json +10 -0
  351. package/registry/blocks/setup-checklist/demo.jsx +159 -0
  352. package/registry/blocks/setup-checklist/index.js +4 -0
  353. package/registry/blocks/setup-checklist/setup-checklist.jsx +446 -0
  354. package/registry/blocks/share-dialog/README.md +113 -0
  355. package/registry/blocks/share-dialog/block.json +10 -0
  356. package/registry/blocks/share-dialog/demo.jsx +140 -0
  357. package/registry/blocks/share-dialog/index.js +3 -0
  358. package/registry/blocks/share-dialog/share-dialog.jsx +384 -0
  359. package/registry/blocks/sidebar/README.md +190 -0
  360. package/registry/blocks/sidebar/block.json +43 -0
  361. package/registry/blocks/sidebar/demo.jsx +577 -0
  362. package/registry/blocks/sidebar/index.js +4 -0
  363. package/registry/blocks/sidebar/sidebar-panel.jsx +149 -0
  364. package/registry/blocks/sidebar/sidebar.jsx +864 -0
  365. package/registry/blocks/signature-pad/README.md +137 -0
  366. package/registry/blocks/signature-pad/block.json +13 -0
  367. package/registry/blocks/signature-pad/colors.js +115 -0
  368. package/registry/blocks/signature-pad/demo.jsx +94 -0
  369. package/registry/blocks/signature-pad/index.js +6 -0
  370. package/registry/blocks/signature-pad/pad.js +7 -0
  371. package/registry/blocks/signature-pad/signature-pad.jsx +434 -0
  372. package/registry/blocks/signature-pad/signature.js +68 -0
  373. package/registry/blocks/sketch-canvas/README.md +280 -0
  374. package/registry/blocks/sketch-canvas/block.json +13 -0
  375. package/registry/blocks/sketch-canvas/demo.jsx +159 -0
  376. package/registry/blocks/sketch-canvas/excalidraw.js +40 -0
  377. package/registry/blocks/sketch-canvas/index.js +9 -0
  378. package/registry/blocks/sketch-canvas/palette.js +226 -0
  379. package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1188 -0
  380. package/registry/blocks/sketch-canvas/sketch.js +179 -0
  381. package/registry/blocks/sketch-canvas/ssr-shim-end.js +6 -0
  382. package/registry/blocks/sketch-canvas/ssr-shim.js +45 -0
  383. package/registry/blocks/sketch-canvas/stamps.js +104 -0
  384. package/registry/blocks/sketch-canvas/theme.js +131 -0
  385. package/registry/blocks/spreadsheet/README.md +307 -0
  386. package/registry/blocks/spreadsheet/block.json +13 -0
  387. package/registry/blocks/spreadsheet/cells.jsx +455 -0
  388. package/registry/blocks/spreadsheet/chrome.jsx +325 -0
  389. package/registry/blocks/spreadsheet/demo.jsx +177 -0
  390. package/registry/blocks/spreadsheet/grid.js +387 -0
  391. package/registry/blocks/spreadsheet/index.js +34 -0
  392. package/registry/blocks/spreadsheet/spreadsheet.jsx +1732 -0
  393. package/registry/blocks/spreadsheet/types.js +473 -0
  394. package/registry/blocks/stat-cards/README.md +173 -0
  395. package/registry/blocks/stat-cards/block.json +10 -0
  396. package/registry/blocks/stat-cards/demo.jsx +257 -0
  397. package/registry/blocks/stat-cards/index.js +4 -0
  398. package/registry/blocks/stat-cards/stat-cards.jsx +307 -0
  399. package/registry/blocks/stat-cards/usage.jsx +243 -0
  400. package/registry/blocks/task-list/README.md +176 -0
  401. package/registry/blocks/task-list/block.json +10 -0
  402. package/registry/blocks/task-list/demo.jsx +134 -0
  403. package/registry/blocks/task-list/index.js +5 -0
  404. package/registry/blocks/task-list/task-list.jsx +907 -0
  405. package/registry/blocks/task-model/README.md +455 -0
  406. package/registry/blocks/task-model/block.json +167 -0
  407. package/registry/blocks/task-model/changes.js +89 -0
  408. package/registry/blocks/task-model/dates.js +371 -0
  409. package/registry/blocks/task-model/demo.jsx +215 -0
  410. package/registry/blocks/task-model/dnd.jsx +240 -0
  411. package/registry/blocks/task-model/index.js +97 -0
  412. package/registry/blocks/task-model/model.js +919 -0
  413. package/registry/blocks/task-model/parts.jsx +449 -0
  414. package/registry/blocks/task-model/toolbar.jsx +271 -0
  415. package/registry/blocks/testimonials/README.md +139 -0
  416. package/registry/blocks/testimonials/block.json +11 -0
  417. package/registry/blocks/testimonials/demo.jsx +137 -0
  418. package/registry/blocks/testimonials/index.js +4 -0
  419. package/registry/blocks/testimonials/testimonials.jsx +430 -0
  420. package/registry/blocks/transaction-list/README.md +126 -0
  421. package/registry/blocks/transaction-list/block.json +10 -0
  422. package/registry/blocks/transaction-list/demo.jsx +79 -0
  423. package/registry/blocks/transaction-list/index.js +4 -0
  424. package/registry/blocks/transaction-list/transaction-list.jsx +383 -0
  425. package/registry/blocks/whiteboard/README.md +179 -0
  426. package/registry/blocks/whiteboard/block.json +10 -0
  427. package/registry/blocks/whiteboard/demo.jsx +169 -0
  428. package/registry/blocks/whiteboard/index.js +6 -0
  429. package/registry/blocks/whiteboard/templates.js +150 -0
  430. package/registry/blocks/whiteboard/whiteboard.jsx +721 -0
  431. package/src/install.mjs +18 -0
  432. package/src/shadcn.mjs +171 -0
  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/parts/fixture.jsx +1 -1
  437. package/templates/next/files/components/parts/sample.js +15 -0
  438. package/templates/next/files/components/ui/avatar.jsx +141 -0
  439. package/templates/next/files/components/ui/breadcrumb.jsx +149 -0
  440. package/templates/next/files/components/ui/button-group.jsx +70 -0
  441. package/templates/next/files/components/ui/button.jsx +132 -12
  442. package/templates/next/files/components/ui/calendar.jsx +609 -0
  443. package/templates/next/files/components/ui/carousel.jsx +281 -0
  444. package/templates/next/files/components/ui/collapsible.jsx +208 -0
  445. package/templates/next/files/components/ui/combobox.jsx +608 -0
  446. package/templates/next/files/components/ui/command.jsx +577 -0
  447. package/templates/next/files/components/ui/context-menu.jsx +191 -0
  448. package/templates/next/files/components/ui/date-picker.jsx +360 -0
  449. package/templates/next/files/components/ui/distribution.jsx +117 -0
  450. package/templates/next/files/components/ui/feedback.jsx +143 -0
  451. package/templates/next/files/components/ui/field.jsx +115 -3
  452. package/templates/next/files/components/ui/file-input.jsx +234 -0
  453. package/templates/next/files/components/ui/icon.jsx +130 -8
  454. package/templates/next/files/components/ui/image.jsx +143 -0
  455. package/templates/next/files/components/ui/index.js +47 -2
  456. package/templates/next/files/components/ui/input-group.jsx +213 -0
  457. package/templates/next/files/components/ui/input-otp.jsx +170 -0
  458. package/templates/next/files/components/ui/item.jsx +206 -0
  459. package/templates/next/files/components/ui/menu.jsx +454 -82
  460. package/templates/next/files/components/ui/notice.jsx +126 -14
  461. package/templates/next/files/components/ui/pagination.jsx +156 -0
  462. package/templates/next/files/components/ui/popover.jsx +284 -0
  463. package/templates/next/files/components/ui/radio.jsx +193 -0
  464. package/templates/next/files/components/ui/rating.jsx +131 -0
  465. package/templates/next/files/components/ui/resizable.jsx +338 -0
  466. package/templates/next/files/components/ui/scroll-area.jsx +123 -0
  467. package/templates/next/files/components/ui/separator.jsx +119 -0
  468. package/templates/next/files/components/ui/sheet.jsx +96 -11
  469. package/templates/next/files/components/ui/slider.jsx +174 -0
  470. package/templates/next/files/components/ui/stat.jsx +574 -0
  471. package/templates/next/files/components/ui/steps.jsx +338 -0
  472. package/templates/next/files/components/ui/swatch.jsx +33 -0
  473. package/templates/next/files/components/ui/table.jsx +130 -0
  474. package/templates/next/files/components/ui/toast.jsx +440 -0
  475. package/templates/next/files/components/ui/toggle.jsx +195 -0
  476. package/templates/next/files/components/ui/tooltip.jsx +270 -0
  477. package/templates/next/files/lib/sample.js +4 -12
  478. package/templates/next/template.json +264 -8
package/r/footer.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "footer",
4
+ "type": "registry:block",
5
+ "title": "Footer",
6
+ "description": "A public site's footer: brand and tagline, link columns or one row of links, social buttons in the kit's brand marks (X, LinkedIn, GitHub, YouTube, Instagram, Facebook), and the legal line with its links; optionally a newsletter sign-up with consent (validates, saves through an async onSubscribe returning { ok }, shows the failure), a call to action, a large brand statement, language and currency selects, and a version badge. Simple, columns or centred.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/core.json"
9
+ ],
10
+ "dependencies": [],
11
+ "files": [
12
+ {
13
+ "path": "registry/blocks/footer/index.js",
14
+ "type": "registry:component",
15
+ "target": "@components/makersclaw/blocks/footer/index.js",
16
+ "content": "// The Footer block — `makersclaw add footer`. Fixed: compose it through\n// its props (README.md beside this file); a footer it cannot be is your\n// own component in components/.\nexport { Footer } from \"./footer\";\n"
17
+ },
18
+ {
19
+ "path": "registry/blocks/footer/footer.jsx",
20
+ "type": "registry:component",
21
+ "target": "@components/makersclaw/blocks/footer/footer.jsx",
22
+ "content": "\"use client\";\n\nimport { useId, useState } from \"react\";\nimport { Badge, Button, Checkbox, Field, IconButton, Input, Notice, Select, cx } from \"../../ui\";\n\n// Footer — a public site's footer: the brand and a line about it, link\n// columns (or one row of links), social buttons drawn with the kit's\n// brand marks, and the legal line; optionally a newsletter sign-up (with\n// consent), a call to action, a large brand statement, language or\n// currency pickers, and a version badge. Three layouts: simple (one\n// row), columns, and centred. The sign-up validates the email and saves\n// through `newsletter.onSubscribe(email, extra)` — async, returning\n// { ok } — showing the failure or a subscribed state.\n//\n// <Footer brand={<Logo />} columns={COLUMNS} socials={SOCIALS}\n// legal=\"© 2026 Fieldline Ltd.\" />\n//\n// README.md beside this file lists every prop.\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n// A network's name and its brand glyph.\nconst NETWORKS = {\n x: { label: \"X\", icon: \"x-logo\" },\n linkedin: { label: \"LinkedIn\", icon: \"linkedin\" },\n instagram: { label: \"Instagram\", icon: \"instagram\" },\n youtube: { label: \"YouTube\", icon: \"youtube\" },\n facebook: { label: \"Facebook\", icon: \"facebook\" },\n github: { label: \"GitHub\", icon: \"github\" },\n google: { label: \"Google\", icon: \"google\" },\n apple: { label: \"Apple\", icon: \"apple\" },\n microsoft: { label: \"Microsoft\", icon: \"microsoft\" },\n email: { label: \"Email\", icon: \"mail\" },\n rss: { label: \"RSS\", icon: \"globe\" },\n};\n\nconst COLUMNS = {\n 1: \"grid-cols-1\",\n 2: \"grid-cols-2\",\n 3: \"grid-cols-2 sm:grid-cols-3\",\n 4: \"grid-cols-2 sm:grid-cols-4\",\n 5: \"grid-cols-2 sm:grid-cols-3 lg:grid-cols-5\",\n};\n\nfunction FooterLink({ link, onNavigate }) {\n return (\n <Button\n variant=\"link\"\n tone=\"muted\"\n href={link.href}\n onClick={(e) => onNavigate?.(link, e)}\n target={link.external ? \"_blank\" : undefined}\n rel={link.external ? \"noreferrer\" : undefined}\n >\n {link.label}\n </Button>\n );\n}\n\nfunction LinkRow({ links, onNavigate, center, label }) {\n if (!links?.length) return null;\n return (\n <nav aria-label={label}>\n <ul className={cx(\"flex flex-wrap gap-x-6 gap-y-3\", center && \"justify-center\")}>\n {links.map((l, i) => (\n <li key={l.href ?? l.label ?? i}>\n <FooterLink link={l} onNavigate={onNavigate} />\n </li>\n ))}\n </ul>\n </nav>\n );\n}\n\nfunction Columns({ columns, onNavigate }) {\n if (!columns?.length) return null;\n const n = Math.min(columns.length, 5);\n return (\n <div className={cx(\"grid min-w-0 gap-x-6 gap-y-10\", COLUMNS[n])}>\n {columns.map((col, i) => (\n <nav key={col.title ?? i} aria-label={col.title} className=\"min-w-0\">\n {col.title ? <h3 className=\"mb-4 text-sm font-semibold\">{col.title}</h3> : null}\n <ul className=\"grid gap-3\">\n {(col.links ?? []).map((l, k) => (\n <li key={l.href ?? l.label ?? k} className=\"min-w-0\">\n <FooterLink link={l} onNavigate={onNavigate} />\n </li>\n ))}\n </ul>\n </nav>\n ))}\n </div>\n );\n}\n\nfunction Socials({ socials, center }) {\n if (!socials?.length) return null;\n return (\n <ul aria-label=\"Social links\" className={cx(\"flex flex-wrap items-center gap-1\", center && \"justify-center\")}>\n {socials.map((s, i) => {\n const net = NETWORKS[s.network] ?? { label: s.label ?? s.network, icon: s.icon ?? \"link\" };\n return (\n <li key={s.href ?? i}>\n <IconButton\n icon={s.icon ?? net.icon}\n label={s.label ?? net.label}\n href={s.href}\n target=\"_blank\"\n rel=\"noreferrer\"\n />\n </li>\n );\n })}\n </ul>\n );\n}\n\nfunction Brand({ brand, tagline, center }) {\n if (!brand && !tagline) return null;\n return (\n <div className={cx(\"grid min-w-0 content-start gap-3\", center && \"justify-items-center text-center\")}>\n {brand ? <div className=\"text-lg font-semibold\">{brand}</div> : null}\n {tagline ? <p className=\"max-w-sm text-pretty text-sm text-muted-foreground\">{tagline}</p> : null}\n </div>\n );\n}\n\nfunction Locales({ locales }) {\n const [own, setOwn] = useState(() => Object.fromEntries((locales ?? []).map((l) => [l.id, l.defaultValue ?? l.options?.[0]?.value ?? \"\"])));\n const uid = useId();\n if (!locales?.length) return null;\n return (\n <div className=\"flex flex-wrap gap-3\">\n {locales.map((l) => {\n const value = l.value ?? own[l.id];\n const fid = `${uid}-${l.id}`;\n return (\n <Field key={l.id} id={fid} className=\"w-full min-w-0 sm:w-44\">\n <label htmlFor={fid} className=\"sr-only\">\n {l.label}\n </label>\n <Select\n id={fid}\n value={value}\n options={l.options}\n onChange={(e) => {\n const v = e.target.value;\n if (l.value === undefined) setOwn((prev) => ({ ...prev, [l.id]: v }));\n l.onChange?.(v);\n }}\n />\n </Field>\n );\n })}\n </div>\n );\n}\n\nfunction Newsletter({ newsletter, center }) {\n const uid = useId();\n const [email, setEmail] = useState(\"\");\n const [agreed, setAgreed] = useState(false);\n const [error, setError] = useState(null);\n const [status, setStatus] = useState(\"idle\");\n const [failure, setFailure] = useState(null);\n const card = newsletter.look === \"card\";\n\n async function submit(ev) {\n ev.preventDefault();\n if (status === \"sending\") return;\n const value = email.trim();\n let problem = null;\n if (!value) problem = \"Enter your email.\";\n else if (!EMAIL.test(value)) problem = \"That email does not look right.\";\n else if (newsletter.consent && !agreed) problem = \"Please agree before subscribing.\";\n setError(problem);\n if (problem) {\n document.getElementById(problem.startsWith(\"Please\") ? `${uid}-consent` : `${uid}-email`)?.focus();\n return;\n }\n setStatus(\"sending\");\n setFailure(null);\n let res;\n try {\n res = await newsletter.onSubscribe(value, { consent: newsletter.consent ? agreed : undefined });\n } catch (err) {\n res = { ok: false, error: err?.message };\n }\n if (res?.ok) setStatus(\"done\");\n else {\n setStatus(\"idle\");\n setFailure(res?.error || \"You were not subscribed. Try again in a minute.\");\n }\n }\n\n return (\n <div\n className={cx(\n \"grid min-w-0 content-start gap-3\",\n card && \"rounded-xl border border-border bg-card p-6 text-card-foreground sm:p-8 md:grid-cols-2 md:items-center md:gap-8\",\n center && !card && \"justify-items-center text-center\",\n )}\n >\n <div className=\"grid gap-1\">\n <h3 className={cx(\"font-semibold\", card ? \"text-lg\" : \"text-sm\")}>{newsletter.title ?? \"Subscribe\"}</h3>\n {newsletter.body ? <p className=\"max-w-sm text-pretty text-sm text-muted-foreground\">{newsletter.body}</p> : null}\n </div>\n {status === \"done\" ? (\n <Notice tone=\"success\" title={newsletter.doneTitle ?? \"You're subscribed\"}>\n {newsletter.doneBody ?? `We'll write to ${email.trim()}.`}\n </Notice>\n ) : (\n <form noValidate onSubmit={submit} className=\"grid w-full max-w-md gap-2\" aria-busy={status === \"sending\" || undefined}>\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-start\">\n <Field id={`${uid}-email`} className=\"min-w-0 flex-1\" error={error && !error.startsWith(\"Please\") ? error : undefined}>\n <label htmlFor={`${uid}-email`} className=\"sr-only\">\n Email\n </label>\n <Input\n id={`${uid}-email`}\n type=\"email\"\n inputMode=\"email\"\n autoComplete=\"email\"\n placeholder={newsletter.placeholder ?? \"you@company.com\"}\n value={email}\n onChange={(e) => {\n setEmail(e.target.value);\n if (error) setError(null);\n }}\n invalid={!!error && !error.startsWith(\"Please\")}\n aria-describedby={error && !error.startsWith(\"Please\") ? `${uid}-email-error` : undefined}\n />\n </Field>\n <Button type=\"submit\" variant=\"primary\" loading={status === \"sending\"} className=\"shrink-0\">\n {newsletter.label ?? \"Subscribe\"}\n </Button>\n </div>\n {newsletter.consent ? (\n <div className=\"grid gap-1 text-left\">\n <Checkbox\n id={`${uid}-consent`}\n checked={agreed}\n onChange={(e) => {\n setAgreed(e.target.checked);\n if (error) setError(null);\n }}\n aria-invalid={error?.startsWith(\"Please\") ? true : undefined}\n label={newsletter.consent}\n className=\"text-muted-foreground\"\n />\n {error?.startsWith(\"Please\") ? (\n <p role=\"alert\" className=\"text-xs text-destructive\">\n {error}\n </p>\n ) : null}\n </div>\n ) : null}\n {failure ? (\n <Notice tone=\"danger\" title=\"Not subscribed\">\n {failure}\n </Notice>\n ) : null}\n </form>\n )}\n </div>\n );\n}\n\nfunction Cta({ cta, center, onNavigate }) {\n if (!cta) return null;\n return (\n <div className={cx(\"flex flex-col gap-4 md:flex-row md:items-center md:justify-between\", center && \"items-center text-center md:flex-col\")}>\n <div className=\"grid min-w-0 gap-1\">\n {cta.title ? <h3 className=\"text-balance text-2xl font-semibold tracking-tight\">{cta.title}</h3> : null}\n {cta.body ? <p className=\"max-w-xl text-pretty text-muted-foreground\">{cta.body}</p> : null}\n </div>\n {cta.actions?.length ? (\n <div className={cx(\"flex flex-wrap gap-3\", center && \"justify-center\")}>\n {cta.actions.map((a, i) => (\n <Button\n key={a.label ?? i}\n variant={a.variant ?? (i === 0 ? \"primary\" : \"secondary\")}\n icon={a.icon}\n href={a.href}\n onClick={(e) => {\n a.onClick?.(e);\n if (a.href) onNavigate?.(a, e);\n }}\n >\n {a.label}\n </Button>\n ))}\n </div>\n ) : null}\n </div>\n );\n}\n\nfunction Statement({ statement, center }) {\n if (!statement) return null;\n const text = typeof statement === \"string\" ? statement : statement.text;\n const by = typeof statement === \"string\" ? null : statement.by;\n return (\n <figure className={cx(\"grid gap-3 border-b border-border pb-10\", center && \"justify-items-center text-center\")}>\n <blockquote className=\"max-w-4xl text-balance text-3xl font-semibold tracking-tight sm:text-5xl\">{text}</blockquote>\n {by ? <figcaption className=\"text-muted-foreground\">{by}</figcaption> : null}\n </figure>\n );\n}\n\nfunction Bottom({ legal, legalLinks, version, locales, onNavigate, center }) {\n if (!legal && !legalLinks?.length && !version && !locales?.length) return null;\n return (\n <div\n className={cx(\n \"flex flex-col gap-4 border-t border-border pt-8 text-sm md:flex-row md:items-center md:justify-between\",\n center && \"items-center text-center md:flex-col\",\n )}\n >\n <div className={cx(\"flex min-w-0 flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:gap-x-6\", center && \"items-center sm:justify-center\")}>\n {legal ? <p className=\"text-muted-foreground\">{legal}</p> : null}\n <LinkRow links={legalLinks} onNavigate={onNavigate} center={center} label=\"Legal\" />\n </div>\n <div className={cx(\"flex flex-wrap items-center gap-3\", center && \"justify-center\")}>\n {version ? <Badge variant=\"code\">{version}</Badge> : null}\n <Locales locales={locales} />\n </div>\n </div>\n );\n}\n\nexport function Footer({\n layout = \"columns\",\n brand,\n tagline,\n links,\n columns,\n socials,\n newsletter,\n cta,\n statement,\n locales,\n version,\n legal,\n legalLinks,\n onNavigate,\n label = \"Site\",\n className,\n}) {\n const centered = layout === \"centered\";\n const news = newsletter?.onSubscribe ? newsletter : null;\n const newsCard = news?.look === \"card\";\n\n let main;\n if (centered) {\n main = (\n <div className=\"grid justify-items-center gap-6 text-center\">\n <Brand brand={brand} tagline={tagline} center />\n <LinkRow links={links} onNavigate={onNavigate} center label={label} />\n <Socials socials={socials} center />\n {news && !newsCard ? <Newsletter newsletter={news} center /> : null}\n </div>\n );\n } else if (layout === \"simple\") {\n main = (\n <div className=\"flex flex-col gap-6 md:flex-row md:items-center md:justify-between\">\n <div className=\"flex min-w-0 flex-col gap-6 md:flex-row md:items-center md:gap-10\">\n {brand ? <div className=\"shrink-0 text-lg font-semibold\">{brand}</div> : null}\n <LinkRow links={links} onNavigate={onNavigate} label={label} />\n </div>\n <Socials socials={socials} />\n </div>\n );\n } else {\n main = (\n <div className={cx(\"grid gap-12\", news && !newsCard ? \"lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)_minmax(0,2fr)]\" : \"lg:grid-cols-[minmax(0,2fr)_minmax(0,5fr)]\")}>\n <div className=\"grid content-start gap-5\">\n <Brand brand={brand} tagline={tagline} />\n <Socials socials={socials} />\n <LinkRow links={links} onNavigate={onNavigate} label={label} />\n </div>\n <Columns columns={columns} onNavigate={onNavigate} />\n {news && !newsCard ? <Newsletter newsletter={news} /> : null}\n </div>\n );\n }\n\n return (\n <footer aria-label={label} className={cx(\"border-t border-border px-4 pt-14 pb-8 sm:px-6 sm:pt-16\", className)}>\n <div className=\"mx-auto grid w-full max-w-6xl gap-10\">\n <Statement statement={statement} center={centered} />\n {main}\n {newsCard ? <Newsletter newsletter={news} /> : null}\n <Cta cta={cta} center={centered} onNavigate={onNavigate} />\n <Bottom\n legal={legal}\n legalLinks={legalLinks}\n version={version}\n locales={locales}\n onNavigate={onNavigate}\n center={centered}\n />\n </div>\n </footer>\n );\n}\n"
23
+ },
24
+ {
25
+ "path": "registry/blocks/footer/README.md",
26
+ "type": "registry:file",
27
+ "target": "@components/makersclaw/blocks/footer/README.md",
28
+ "content": "# Footer\n\nA public site's footer. It holds the brand and a line about it, link\ncolumns (or one row of links), social buttons drawn with the kit's brand\nmarks, and the legal line with its links. Optional parts: a newsletter\nsign-up (with a consent box), a call to action, a large brand statement\nor quote, language and currency selects, and a version badge.\n\nThree layouts:\n- **columns** (default) — the brand, tagline and socials beside the link\n columns (and an inline sign-up), then the legal row;\n- **simple** — one row: brand, links, socials; then the legal row;\n- **centred** — everything stacked in the middle.\n\nThe sign-up checks the email (and the consent, when asked) in place,\nthen calls `newsletter.onSubscribe(email, { consent })`, which is async\nand returns `{ ok }`. While it runs the button spins. `{ ok: false,\nerror }` (or a throw) shows the error and keeps the email. `{ ok: true }`\nshows \"You're subscribed\". It is a full-width section: it pads itself\nand centres its content.\n\n```jsx\nimport { Footer } from \"../components/blocks/footer\"; // a Makersclaw app (\"./blocks/footer\" from components/)\nimport { Footer } from \"@/components/makersclaw/blocks/footer\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component (a\nlayout is the usual place).\n\n## The data\n\n```js\n// a link, anywhere: columns, links, legalLinks\n{ label: \"Pricing\", href: \"/pricing\", external?: true } // external opens a new tab\n\n// columns\n[{ title: \"Product\", links: [link, …] }, …] // up to 5 across; 2 on a phone\n\n// socials\n[{ network: \"x\", href: \"https://x.com/fieldline\", label? }, …]\n```\n\n`network` is one of `x`, `linkedin`, `github`, `youtube`, `instagram`,\n`facebook`, `google`, `apple`, `microsoft`, `email`, `rss`. Anything\nelse needs an `icon` (a kit Icon name) and a `label`. Each is an icon\nbutton, named to a screen reader (\"X\", \"LinkedIn\"), opening a new tab.\n\n## Props\n\n- `layout` — `columns` (default) · `simple` · `centered`.\n- `brand` — a node: your logo, linked home if you like.\n- `tagline` — a line under the brand (columns and centred).\n- `columns` — link columns (columns layout).\n- `links` — one row of links (simple and centred; under the socials in\n columns).\n- `socials` — as above.\n- `newsletter` — `{ onSubscribe, title?, body?, label?, placeholder?,\n consent?, look?, doneTitle?, doneBody? }`. `onSubscribe(email, {\n consent })` is async and returns `{ ok: true }` or `{ ok: false,\n error? }`; without it there is no sign-up. `consent` is the checkbox's\n label (a string or a node with a link), and must be ticked. `look`:\n `inline` (default: a column beside the links) · `card` (a full-width\n card under them). `label` is the button, default \"Subscribe\".\n- `cta` — `{ title?, body?, actions: [{ label, href?, onClick?, icon?,\n variant? }] }`: \"Sign in\", \"Get the app\". A band above the legal row;\n the first action is primary unless it says otherwise.\n- `statement` — a large line at the top: a string, or `{ text, by }` for\n a quote.\n- `locales` — `[{ id, label, options: [{ value, label }], value?,\n defaultValue?, onChange? }]`: language, currency, country selects in\n the legal row. Uncontrolled unless you pass `value`.\n- `version` — \"v4.2.0\": a code badge in the legal row.\n- `legal` — the copyright line: \"© 2026 Fieldline Ltd.\". Pass the year;\n the block does not read the clock.\n- `legalLinks` — privacy, terms, cookies.\n- `onNavigate(link, event)` — called when any link or a `cta` action with\n an `href` is followed, before navigation; `event.preventDefault()`\n stops it.\n- `label` — what a screen reader calls the footer and its link row.\n Default \"Site\".\n- `className` — the footer's spacing. Never colour.\n\n## Example\n\n```jsx\n// components/SiteFooter.jsx\n\"use client\";\nimport { Icon } from \"./ui\";\nimport { Footer } from \"./blocks/footer\";\nimport { subscribe } from \"../lib/actions\"; // your server action\n\nexport default function SiteFooter() {\n return (\n <Footer\n brand={<span className=\"inline-flex items-center gap-2\"><Icon name=\"zap\" /> Fieldline</span>}\n tagline=\"The pipeline small sales teams actually keep up to date.\"\n columns={[\n { title: \"Product\", links: [{ label: \"Pricing\", href: \"/pricing\" }, { label: \"Changelog\", href: \"/changelog\" }] },\n { title: \"Company\", links: [{ label: \"About\", href: \"/about\" }, { label: \"Careers\", href: \"/careers\" }] },\n ]}\n socials={[{ network: \"x\", href: \"https://x.com/fieldline\" }, { network: \"linkedin\", href: \"https://linkedin.com/company/fieldline\" }]}\n newsletter={{\n body: \"One email a month.\",\n consent: \"I agree to the privacy policy.\",\n onSubscribe: async (email) => subscribe(email),\n }}\n legal=\"© 2026 Fieldline Ltd. All rights reserved.\"\n legalLinks={[{ label: \"Privacy\", href: \"/privacy\" }, { label: \"Terms\", href: \"/terms\" }]}\n />\n );\n}\n```\n\n## What it will not do\n\n- Change the language or currency by itself: `locales[].onChange` is\n yours.\n- Stick to the bottom of a short page: that is your layout's\n `min-h-dvh` flex column.\n- Paint itself in the primary colour: the look is the workspace\n theme's.\n- Draw store badges: \"Get the app\" is two buttons with the Apple and\n Google marks.\n\nA footer 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 footer` restores them.\n\n## Credits\n\nLayouts ported from Creative Tim UI's footer blocks\n(https://github.com/creativetimofficial/ui, MIT), rewritten on the\nMakersclaw kit.\n"
29
+ }
30
+ ],
31
+ "categories": [
32
+ "footer",
33
+ "site footer",
34
+ "links",
35
+ "social",
36
+ "newsletter",
37
+ "subscribe",
38
+ "legal",
39
+ "copyright",
40
+ "language",
41
+ "currency",
42
+ "marketing",
43
+ "landing page"
44
+ ],
45
+ "docs": "Installed to components/makersclaw/blocks/footer. Import: import { Footer } from \"@/components/makersclaw/blocks/footer\". Props and an example: components/makersclaw/blocks/footer/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
46
+ "meta": {
47
+ "makersclaw": {
48
+ "version": "0.3.0",
49
+ "useWhen": "The bottom of every page of a public site: a marketing site, a docs hub, a shop, a customer portal. Not for an app's own status bar or a toolbar pinned to the bottom of a screen."
50
+ }
51
+ }
52
+ }
package/r/gantt.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "gantt",
4
+ "type": "registry:block",
5
+ "title": "Gantt",
6
+ "description": "The Gantt view of work, like Linear's timeline or TeamGantt: a task list on the left (groups that fold, subtasks indented under a summary bar) beside a timeline with a two-tier date header, bars coloured by project or status with their progress, milestones as diamonds, dependency arrows (red when the dates disagree), a today line and weekend shading. Drag a bar to move it, an edge to change its start or end, the dot past its end onto another bar to link them; tasks waiting on a moved one slide along while you drag and the finish date shows what the move cost. Zoom day, week, month or quarter; highlight the critical path; click an empty row to schedule an undated task; quick-add per group; arrow keys move a focused bar; search, filters, group and sort from the shared toolbar. One async onChange saves every change; failures roll back with a notice.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/core.json",
9
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/task-model.json"
10
+ ],
11
+ "dependencies": [],
12
+ "files": [
13
+ {
14
+ "path": "registry/blocks/gantt/index.js",
15
+ "type": "registry:component",
16
+ "target": "@components/makersclaw/blocks/gantt/index.js",
17
+ "content": "// The Gantt block — `makersclaw add gantt` (it brings task-model). Tasks\n// on a timeline: drag, resize, link, zoom, the critical path. Fixed:\n// compose it through its props (README.md beside this file); a timeline\n// it cannot be is your own component in components/, built from\n// task-model's helpers and these schedule helpers.\nexport { Gantt } from \"./gantt\";\nexport { ZOOMS, ganttRange, headerTiers, planMove, afterBlockersPatch, isMilestone, summarySpan, arrowPath } from \"./schedule\";\n"
18
+ },
19
+ {
20
+ "path": "registry/blocks/gantt/gantt.jsx",
21
+ "type": "registry:component",
22
+ "target": "@components/makersclaw/blocks/gantt/gantt.jsx",
23
+ "content": "\"use client\";\n\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { Button, Empty, Icon, Input, Notice, Skeleton, Toggle, ToggleGroup, cx } from \"../../ui\";\nimport {\n DEFAULT_VIEW,\n NONE,\n TaskDragProvider,\n ViewToolbar,\n addDays,\n applyView,\n createTaskContext,\n criticalPath,\n dateConflicts,\n deleteChange,\n dependencyEdges,\n descendantIds,\n diffDays,\n draftTask,\n flattenTree,\n formatDate,\n isDone,\n movePatch,\n progressOf,\n spanOf,\n spanPatch,\n toggleDoneChange,\n useTaskChanges,\n wouldCycle,\n} from \"../task-model\";\nimport { ADD_ROW, Bar, GroupRow, ListRow, QuickAdd, ROW, TINTS, TimelineRow, Unscheduled, spanWords } from \"./rows\";\nimport {\n ZOOMS,\n afterBlockersPatch,\n arrowPath,\n ganttRange,\n headerTiers,\n isMilestone,\n lengthOf,\n planMove,\n summarySpan,\n weekendBands,\n xOf,\n zoomOf,\n} from \"./schedule\";\n\n// Gantt — tasks on a timeline, like Linear's timeline or TeamGantt: the\n// task list on the left (groups that fold, subtasks under their parent),\n// the bars on the right under a two-tier date header. Drag a bar to move\n// it, an edge to change its start or end, the dot past its end onto\n// another bar to make that one wait on it; every task that waits on a\n// moved one slides along while you drag, and the finish date at the top\n// says what the move cost. Milestones are diamonds, parents are summary\n// brackets, dependencies are arrows (red when the dates disagree), and the\n// critical path lights up on demand. Zoom day · week · month · quarter;\n// a today line; the toolbar's search, filters, group and sort.\n//\n// Everything saves through one async `onChange(change)` answering { ok }\n// (task-model's change shapes); the timeline shows each change at once\n// and takes it back, with a notice, when the save fails.\n//\n// README.md beside this file lists every prop.\n\nconst HEADER = 52;\nconst ZOOM_ITEMS = Object.values(ZOOMS).map((z) => ({ value: z.key, label: z.label }));\nconst GROUP_FIELDS = [\"project\", \"status\", \"assignees\", \"priority\"];\n\nfunction useControlled(value, fallback, onChange) {\n const [own, setOwn] = useState(fallback);\n const controlled = value !== undefined;\n const set = useCallback(\n (next) => {\n if (!controlled) setOwn(next);\n onChange?.(next);\n },\n [controlled, onChange],\n );\n return [controlled ? value : own, set];\n}\n\nexport function Gantt({\n tasks,\n statuses,\n people,\n projects,\n fields,\n today,\n weekStartsOn,\n weekend,\n holidays,\n onChange,\n onOpen,\n view: viewProp,\n defaultView,\n onViewChange,\n zoom: zoomProp,\n defaultZoom = \"week\",\n onZoomChange,\n colorBy = \"project\",\n pushDependents = true,\n defaultCritical = false,\n newTaskLength = 3,\n toolbar = true,\n toolbarExtra,\n addLabel = \"Add task\",\n starters,\n emptyTitle = \"Nothing on the timeline yet\",\n emptyBody = \"Add the first task and give it dates, then drag it into place. Link tasks to see what waits on what.\",\n height = 560,\n loading = false,\n error: readError,\n label = \"Timeline\",\n locale,\n className,\n}) {\n const uid = useId();\n const ctx = useMemo(\n () => createTaskContext({ statuses, people, projects, fields, today, weekStartsOn, weekend, holidays }),\n [statuses, people, projects, fields, today, weekStartsOn, weekend, holidays],\n );\n const editable = typeof onChange === \"function\";\n const { tasks: all, commit, error, clearError } = useTaskChanges(tasks, onChange);\n const [viewRaw, setViewRaw] = useControlled(viewProp, { ...DEFAULT_VIEW, groupBy: \"project\", sort: [{ key: \"start\", dir: \"asc\" }], ...defaultView }, onViewChange);\n const view = useMemo(() => ({ ...DEFAULT_VIEW, ...viewRaw }), [viewRaw]);\n const [zoom, setZoomRaw] = useControlled(zoomProp, ZOOMS[defaultZoom] ? defaultZoom : \"week\", onZoomChange);\n const z = zoomOf(zoom);\n const dw = z.dayWidth;\n\n const [critical, setCritical] = useState(defaultCritical);\n const [listOpen, setListOpen] = useState(null); // null: open from 640px, closed on a phone\n const [narrow, setNarrow] = useState(false);\n const [folded, setFolded] = useState(() => new Set());\n const [collapsed, setCollapsed] = useState(() => new Set());\n const [hot, setHot] = useState(null);\n const [adding, setAdding] = useState(null);\n const [drag, setDrag] = useState(null); // { id, kind, days, keys? }\n const [linking, setLinking] = useState(false);\n const [notice, setNotice] = useState(null);\n const [announce, setAnnounce] = useState(\"\");\n const scrollRef = useRef(null);\n const listRef = useRef(null);\n const dragEndedAt = useRef(0);\n const nudgeTimer = useRef(null);\n const dragRef = useRef(null);\n dragRef.current = drag;\n\n useEffect(() => {\n const mq = window.matchMedia(\"(max-width: 639px)\");\n const on = () => setNarrow(mq.matches);\n on();\n mq.addEventListener(\"change\", on);\n return () => mq.removeEventListener(\"change\", on);\n }, []);\n const showList = listOpen ?? !narrow;\n\n const say = (text) => setAnnounce(text);\n const name = (t) => `“${t?.title || \"Untitled\"}”`;\n const byIdAll = useMemo(() => new Map(all.map((t) => [t.id, t])), [all]);\n\n // ---- what a drag shows before it saves -------------------------------\n const plan = useMemo(() => (drag ? planMove(all, drag.id, drag.kind, drag.days, { push: pushDependents }) : []), [all, drag, pushDependents]);\n const display = useMemo(() => {\n if (!plan.length) return all;\n const patches = new Map(plan.map((p) => [p.id, p.patch]));\n return all.map((t) => (patches.has(t.id) ? { ...t, ...patches.get(t.id) } : t));\n }, [all, plan]);\n const byId = useMemo(() => new Map(display.map((t) => [t.id, t])), [display]);\n const moved = useMemo(() => new Set(plan.map((p) => p.id)), [plan]);\n\n // ---- the view ----------------------------------------------------------\n const shown = useMemo(() => applyView(all, view, ctx), [all, view, ctx]);\n const by = view.groupBy && view.groupBy !== \"none\" ? view.groupBy : null;\n const range = useMemo(() => ganttRange(all, zoom, ctx.today, { weekStartsOn: ctx.weekStartsOn }), [all, zoom, ctx]);\n const from = range.from;\n const width = range.days * dw;\n const tiers = useMemo(() => (from ? headerTiers(from, range.to, zoom, { today: ctx.today, weekStartsOn: ctx.weekStartsOn, locale }) : { top: [], bottom: [] }), [from, range.to, zoom, ctx, locale]);\n const bands = useMemo(() => (from ? weekendBands(from, range.to, zoom, ctx.weekend) : []), [from, range.to, zoom, ctx]);\n\n // Rows in reading order, each with its height and top: the list pane and\n // the timeline draw the same list, so they always line up.\n const rows = useMemo(() => {\n const out = [];\n let y = 0;\n const push = (r, h) => {\n out.push({ ...r, y, height: h });\n y += h;\n };\n const single = !by;\n for (const g of shown.groups) {\n const shut = folded.has(g.key);\n if (!single) push({ kind: \"group\", key: `g:${g.key}`, group: g }, ROW);\n if (!shut || single) {\n for (const r of flattenTree(g.tasks, { collapsed })) {\n const task = byId.get(r.task.id) ?? r.task;\n const milestone = isMilestone(task);\n const span = r.hasChildren ? summarySpan(task, display) : spanOf(task);\n push(\n {\n kind: \"task\",\n key: `${g.key}::${task.id}`,\n group: g.key,\n task,\n depth: r.depth,\n hasChildren: r.hasChildren,\n collapsed: r.collapsed,\n milestone,\n span,\n done: isDone(task, ctx),\n progress: progressOf(task, display, ctx),\n },\n ROW,\n );\n }\n if (editable) push({ kind: \"add\", key: `add:${g.key}`, group: g }, ADD_ROW);\n }\n }\n return { list: out, height: y };\n }, [shown.groups, by, folded, collapsed, byId, display, ctx, editable]);\n\n // Where each task's bar is (its first row, when it shows twice).\n const boxes = useMemo(() => {\n const m = new Map();\n if (!from) return m;\n for (const r of rows.list) {\n if (r.kind !== \"task\" || !r.span || m.has(r.task.id)) continue;\n const cy = r.y + ROW / 2;\n if (r.milestone && !r.hasChildren) {\n const cx = xOf(r.span.end, from, dw) + dw / 2;\n m.set(r.task.id, { x0: cx - 8, x1: cx + 8, y: cy });\n } else {\n const x0 = xOf(r.span.start, from, dw);\n m.set(r.task.id, { x0, x1: x0 + Math.max(lengthOf(r.span) * dw, 6), y: cy });\n }\n }\n return m;\n }, [rows, from, dw]);\n\n const path = useMemo(() => (critical ? criticalPath(display) : new Set()), [critical, display]);\n const conflicts = useMemo(() => new Set(dateConflicts(display).map((e) => `${e.from}>${e.to}`)), [display]);\n const conflicted = useMemo(() => {\n const m = new Map();\n for (const k of conflicts) {\n const [a, b] = k.split(\">\");\n m.set(b, [...(m.get(b) ?? []), a]);\n }\n return m;\n }, [conflicts]);\n const edges = useMemo(() => dependencyEdges(display).filter((e) => boxes.has(e.from) && boxes.has(e.to)), [display, boxes]);\n\n // The finish: the last day any shown task ends, and how a drag moves it.\n const finish = useMemo(() => {\n let base = null;\n let now = null;\n for (const t of shown.tasks) {\n const a = spanOf(t);\n const b = spanOf(byId.get(t.id));\n if (a && (!base || a.end > base)) base = a.end;\n if (b && (!now || b.end > now)) now = b.end;\n }\n return { day: now, delta: base && now ? diffDays(base, now) : 0 };\n }, [shown.tasks, byId]);\n const pathDays = useMemo(() => {\n let lo = null;\n let hi = null;\n for (const id of path) {\n const s = spanOf(byId.get(id));\n if (!s) continue;\n if (!lo || s.start < lo) lo = s.start;\n if (!hi || s.end > hi) hi = s.end;\n }\n return lo ? diffDays(lo, hi) + 1 : 0;\n }, [path, byId]);\n\n // ---- scrolling ---------------------------------------------------------\n const listWidth = () => (showList ? listRef.current?.offsetWidth ?? 0 : 0);\n const scrollToDay = useCallback(\n (day, { smooth = false, at = 1 / 3 } = {}) => {\n const el = scrollRef.current;\n if (!el || !from || !day) return;\n const lw = showList ? listRef.current?.offsetWidth ?? 0 : 0;\n const left = xOf(day, from, dw) - (el.clientWidth - lw) * at;\n const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n el.scrollTo({ left: Math.max(0, left), behavior: smooth && !reduced ? \"smooth\" : \"auto\" });\n },\n [from, dw, showList],\n );\n // Open on today, once there is a timeline.\n const opened = useRef(false);\n useEffect(() => {\n if (opened.current || !from) return;\n opened.current = true;\n scrollToDay(ctx.today);\n }, [from, ctx.today, scrollToDay]);\n // A new zoom keeps the day in the middle where it was.\n const anchor = useRef(null);\n function setZoom(next) {\n const el = scrollRef.current;\n if (el && from) {\n const lw = listWidth();\n const mid = el.scrollLeft + (el.clientWidth - lw) / 2;\n anchor.current = addDays(from, Math.round(mid / dw));\n }\n setZoomRaw(next);\n }\n useEffect(() => {\n if (!anchor.current) return;\n scrollToDay(anchor.current, { at: 1 / 2 });\n anchor.current = null;\n }, [zoom, scrollToDay]);\n\n // ---- changes -----------------------------------------------------------\n const commitPlan = useCallback(\n (id, kind, days) => {\n const steps = planMove(all, id, kind, days, { push: pushDependents });\n if (!steps.length) return;\n const task = byIdAll.get(id);\n const family = new Set([id, ...descendantIds(all, id)]);\n const pushed = steps.filter((s) => !family.has(s.id)).length;\n const change = steps.length === 1 ? { type: \"update\", id: steps[0].id, patch: steps[0].patch } : { type: \"batch\", changes: steps.map((s) => ({ type: \"update\", id: s.id, patch: s.patch })) };\n const after = spanOf({ ...task, ...steps.find((s) => s.id === id)?.patch }) ?? summarySpan({ ...task }, all);\n commit(change, {\n label: name(task),\n undo: pushed ? `Moved ${name(task)} and ${pushed} task${pushed === 1 ? \"\" : \"s\"} waiting on it` : undefined,\n }).then((r) => {\n if (r?.ok !== false) say(`${name(task)} is now ${spanWords(after, locale)}${pushed ? `; ${pushed} waiting task${pushed === 1 ? \"\" : \"s\"} moved too` : \"\"}.`);\n });\n },\n [all, byIdAll, commit, pushDependents, locale],\n );\n\n const onPreview = useCallback((id, kind, days) => {\n setDrag((d) => (d && d.id === id && d.kind === kind && d.days === days ? d : { id, kind, days }));\n }, []);\n\n // Keyboard: each arrow adds a day to a pending move; it saves when the\n // keys rest (or on Enter), Escape drops it.\n const onNudge = useCallback(\n (id, kind, step, action) => {\n const d = dragRef.current;\n clearTimeout(nudgeTimer.current);\n const flush = (x) => {\n setDrag(null);\n if (x?.days) commitPlan(x.id, x.kind, x.days);\n };\n if (action === \"flush\") return flush(d);\n if (action === \"cancel\") {\n setDrag(null);\n say(\"Move cancelled.\");\n return;\n }\n let next;\n if (d && d.keys && d.id === id && d.kind === kind) next = { ...d, days: d.days + step };\n else {\n if (d?.keys) flush(d);\n next = { id, kind, days: step, keys: true };\n }\n setDrag(next);\n nudgeTimer.current = setTimeout(() => flush(dragRef.current), 800);\n },\n [commitPlan],\n );\n useEffect(() => () => clearTimeout(nudgeTimer.current), []);\n\n function onDragStart({ active }) {\n if (active?.data?.kind === \"link\") setLinking(true);\n }\n\n function onDragEnd({ active, over, delta }) {\n dragEndedAt.current = Date.now();\n setLinking(false);\n const a = active?.data ?? {};\n if (a.kind === \"link\") {\n setDrag(null);\n const to = over?.data?.task;\n if (to && to.id !== a.task.id) link(a.task.id, to.id);\n return;\n }\n const days = Math.round((delta?.x ?? 0) / dw);\n setDrag(null);\n if (a.task && days) commitPlan(a.task.id, a.kind, days);\n }\n\n function link(fromId, toId) {\n const target = byIdAll.get(toId);\n const source = byIdAll.get(fromId);\n if (!target || !source) return;\n if ((target.dependsOn ?? []).includes(fromId)) return;\n if (wouldCycle(all, fromId, toId)) {\n setNotice(`${name(source)} already waits on ${name(target)}, so ${name(target)} cannot wait on it.`);\n return;\n }\n setNotice(null);\n commit({ type: \"update\", id: toId, patch: { dependsOn: [...(target.dependsOn ?? []), fromId] } }, { label: name(target) }).then((r) => {\n if (r?.ok !== false) say(`${name(target)} now waits on ${name(source)}.`);\n });\n }\n\n function unlink(fromId, toId) {\n const target = byIdAll.get(toId);\n if (!target) return;\n commit(\n { type: \"update\", id: toId, patch: { dependsOn: (target.dependsOn ?? []).filter((d) => d !== fromId) } },\n { label: name(target), undo: `${name(target)} no longer waits on ${name(byIdAll.get(fromId))}` },\n );\n }\n\n function groupPatch(key) {\n if (!by) return {};\n return movePatch({ assignees: [], tags: [] }, by, NONE, key, ctx);\n }\n\n async function create(title, groupKey) {\n const patch = groupPatch(groupKey);\n if (patch === null) return { ok: false };\n const start = ctx.today;\n const task = draftTask(title, ctx, { ...patch, start, due: addDays(start, Math.max(1, newTaskLength) - 1) });\n const r = await commit({ type: \"create\", task }, { label: name(task) });\n if (r?.ok !== false) say(`Added ${name(task)}, starting today.`);\n return r;\n }\n\n function schedule(task, dayIndex) {\n const start = addDays(from, dayIndex);\n const end = addDays(start, Math.max(1, isMilestone(task) ? 1 : newTaskLength) - 1);\n commit({ type: \"update\", id: task.id, patch: spanPatch(task, start, end) }, { label: name(task) }).then((r) => {\n if (r?.ok !== false) say(`${name(task)} is scheduled for ${spanWords({ start, end }, locale)}.`);\n });\n }\n\n function remove(task) {\n commit(deleteChange(all, [task.id]), { label: name(task), undo: `Deleted ${name(task)}` });\n }\n\n async function startFrom(s) {\n let cursor = ctx.today;\n let prev = null;\n const made = [];\n for (const item of s.tasks ?? []) {\n const len = Math.max(1, item.milestone ? 1 : item.days ?? newTaskLength);\n const start = item.parallel && prev ? prev.start : cursor;\n const end = addDays(start, len - 1);\n const t = draftTask(item.title, ctx, {\n start: item.milestone ? undefined : start,\n due: end,\n ...(item.milestone ? { milestone: true } : {}),\n dependsOn: prev && !item.parallel ? [prev.id] : [],\n });\n made.push(t);\n if (!item.parallel) cursor = addDays(end, 1);\n prev = { id: t.id, start };\n }\n await commit({ type: \"create\", tasks: made }, { label: s.label });\n }\n\n function clickGuard() {\n return Date.now() - dragEndedAt.current < 250;\n }\n\n function menuFor(row) {\n const t = row.task;\n const items = [];\n if (onOpen) items.push({ label: \"Open\", icon: \"external\", onSelect: () => onOpen(t) });\n if (!editable) return items;\n if (!row.span) items.push({ label: \"Schedule from today\", icon: \"calendar\", onSelect: () => schedule(t, diffDays(from, ctx.today)) });\n const fix = conflicted.has(t.id) ? afterBlockersPatch(t, all) : null;\n if (fix) items.push({ label: \"Start after what it waits on\", icon: \"arrow-right\", onSelect: () => commit({ type: \"update\", id: t.id, patch: fix }, { label: name(t) }) });\n // \"Waits on\": what it waits on now, then the tasks nearest its start\n // (a dozen; the dot on a bar links anything).\n const mine = new Set(t.dependsOn ?? []);\n const at = row.span?.start ?? ctx.today;\n const near = (o) => Math.abs(diffDays(at, spanOf(o)?.start ?? at) ?? 0);\n const others = shown.tasks\n .filter((o) => o.id !== t.id && o.id !== t.parentId)\n .sort((a, b) => (mine.has(b.id) ? 1 : 0) - (mine.has(a.id) ? 1 : 0) || near(a) - near(b))\n .slice(0, Math.max(12, mine.size));\n if (others.length) {\n items.push({\n label: \"Waits on\",\n icon: \"link\",\n items: others.map((o) => {\n const on = (t.dependsOn ?? []).includes(o.id);\n return {\n type: \"checkbox\",\n label: o.title || \"Untitled\",\n checked: on,\n disabled: !on && wouldCycle(all, o.id, t.id),\n onCheckedChange: (next) => (next ? link(o.id, t.id) : unlink(o.id, t.id)),\n };\n }),\n });\n }\n items.push({ label: row.done ? \"Mark not done\" : \"Mark done\", icon: \"check\", onSelect: () => commit(toggleDoneChange(t, ctx), { label: name(t) }) });\n items.push({ type: \"separator\" });\n items.push({ label: \"Delete\", icon: \"trash\", tone: \"danger\", onSelect: () => remove(t) });\n return items;\n }\n\n function tintOf(task) {\n if (colorBy === \"status\") {\n const s = ctx.statusById.get(String(task.status));\n return TINTS[s?.hue] ?? TINTS[s?.tone] ?? TINTS.neutral;\n }\n if (colorBy === \"project\") return TINTS[ctx.projectById.get(String(task.project))?.hue] ?? TINTS.primary;\n return TINTS.primary;\n }\n\n const clearFilters = () => setViewRaw({ ...view, search: \"\", filters: {}, hideDone: false });\n const firstGroup = shown.groups[0]?.key ?? \"all\";\n\n // ---- render ------------------------------------------------------------\n const addButton = editable ? (\n <Button\n variant=\"primary\"\n size=\"sm\"\n icon=\"plus\"\n onClick={() => {\n setListOpen(true);\n setFolded((s) => (s.has(firstGroup) ? new Set([...s].filter((k) => k !== firstGroup)) : s));\n setAdding(firstGroup);\n }}\n >\n <span className=\"sr-only sm:not-sr-only\">{addLabel}</span>\n </Button>\n ) : null;\n\n const bar = (\n <div className=\"flex flex-wrap items-center gap-2 border-b border-border px-2 py-2\">\n <Toggle\n size=\"sm\"\n variant=\"quiet\"\n icon=\"panel-left\"\n label={showList ? \"Hide the task list\" : \"Show the task list\"}\n pressed={showList}\n onPressedChange={(on) => setListOpen(on)}\n />\n <Button size=\"sm\" variant=\"quiet\" onClick={() => scrollToDay(ctx.today, { smooth: true })}>\n Today\n </Button>\n <ToggleGroup aria-label=\"Zoom\" size=\"sm\" items={ZOOM_ITEMS} value={zoom} onChange={setZoom} className=\"order-last w-full sm:order-none sm:w-auto\" />\n <div className=\"ms-auto flex min-w-0 items-center gap-2\">\n {finish.day ? (\n <p className=\"hidden items-center gap-1.5 text-sm text-muted-foreground md:flex\">\n Finishes <span className=\"font-medium text-foreground\">{formatDate(finish.day, { weekday: \"short\", month: \"short\", day: \"numeric\" }, { locale })}</span>\n {drag && finish.delta ? (\n <span data-nums className={cx(\"rounded-full px-1.5 text-xs font-medium tabular-nums\", finish.delta > 0 ? \"bg-warning/15 text-warning\" : \"bg-success/15 text-success\")}>\n {finish.delta > 0 ? \"+\" : \"−\"}\n {Math.abs(finish.delta)} {Math.abs(finish.delta) === 1 ? \"day\" : \"days\"}\n </span>\n ) : null}\n </p>\n ) : null}\n <Toggle size=\"sm\" variant=\"quiet\" icon=\"zap\" pressed={critical} onPressedChange={setCritical}>\n <span className=\"sr-only sm:not-sr-only\">Critical path</span>\n </Toggle>\n </div>\n </div>\n );\n\n let content;\n if (loading) {\n content = (\n <div aria-busy=\"true\" className=\"grid gap-3 p-4\" style={{ height: Math.min(height, 320) }}>\n {[64, 48, 72, 40, 56].map((w, i) => (\n <div key={i} className=\"flex items-center gap-4\">\n <Skeleton className=\"w-28 shrink-0 sm:w-56\" />\n <div className=\"min-w-0 flex-1\" style={{ paddingInlineStart: `${i * 6}%`, paddingInlineEnd: `${100 - w - i * 6}%` }}>\n <Skeleton shape=\"rect\" className=\"h-6 w-full\" />\n </div>\n </div>\n ))}\n <span className=\"sr-only\">Loading the timeline</span>\n </div>\n );\n } else if (!all.length) {\n content = (\n <div className=\"p-4\">\n <Empty\n icon=\"calendar\"\n title={emptyTitle}\n body={emptyBody}\n action={editable ? <FirstTask onCreate={(title) => create(title, firstGroup)} starters={starters} onStarter={startFrom} addLabel={addLabel} uid={uid} /> : null}\n />\n </div>\n );\n } else if (!shown.tasks.length) {\n content = (\n <div className=\"p-4\">\n <Empty\n icon=\"filter\"\n title=\"No tasks match\"\n body=\"Nothing on this timeline fits the search and filters.\"\n action={\n <Button size=\"sm\" variant=\"secondary\" onClick={clearFilters}>\n Clear filters\n </Button>\n }\n />\n </div>\n );\n } else {\n const tx = from ? xOf(ctx.today, from, dw) : null;\n const todayIn = tx !== null && tx >= 0 && tx < width;\n content = (\n <TaskDragProvider collision=\"pointer\" onDragStart={onDragStart} onDragEnd={onDragEnd} onDragCancel={() => (setDrag(null), setLinking(false))} disabled={!editable}>\n <div\n ref={scrollRef}\n className={cx(\"relative overflow-auto overscroll-x-contain\", showList ? \"[--gantt-list:11rem] sm:[--gantt-list:18rem]\" : \"[--gantt-list:0px]\")}\n style={{ maxHeight: `min(${height}px, 75vh)` }}\n onPointerLeave={() => setHot(null)}\n >\n <div className=\"w-max min-w-full\">\n {/* The header: the list's heading, then months over days (or\n years over months). Sticky to the top. */}\n <div className=\"sticky top-0 z-40 flex border-b border-border bg-card\" style={{ height: HEADER }}>\n {showList ? (\n <div className=\"sticky left-0 z-10 flex w-44 shrink-0 items-end border-e border-border bg-card px-3 pb-1.5 sm:w-72\">\n <span className=\"text-xs font-medium text-muted-foreground\">\n Task{\" \"}\n <span data-nums className=\"tabular-nums\">\n · {shown.tasks.length}\n </span>\n </span>\n </div>\n ) : null}\n <div className=\"relative shrink-0\" style={{ width }} aria-hidden=\"true\">\n {tiers.top.map((c) => (\n <div key={c.key} className=\"absolute top-0 flex h-6 items-center border-s border-border px-2 first:border-s-0\" style={{ left: c.x, width: c.w }}>\n <span className=\"sticky left-[calc(var(--gantt-list)+8px)] truncate text-xs font-medium text-foreground\">\n {c.label}\n </span>\n </div>\n ))}\n {tiers.bottom.map((c) => (\n <div\n key={c.key}\n className={cx(\"absolute bottom-0 flex h-7 items-center justify-center border-s border-border/60 first:border-s-0\", c.weekend ? \"text-muted-foreground/60\" : \"text-muted-foreground\")}\n style={{ left: c.x, width: c.w }}\n >\n {c.today && z.unit === \"day\" ? (\n <span data-nums className=\"rounded-full bg-primary px-1.5 text-2xs font-semibold tabular-nums text-primary-foreground\">\n {c.label}\n </span>\n ) : c.w >= 22 ? (\n <span data-nums className={cx(\"truncate text-2xs tabular-nums\", c.today && \"font-semibold text-foreground\")}>\n {c.label}\n </span>\n ) : null}\n </div>\n ))}\n {todayIn && z.unit !== \"day\" ? <span className=\"absolute bottom-0 size-1.5 -translate-x-1/2 translate-y-1/2 rounded-full bg-primary\" style={{ left: tx + dw / 2 }} /> : null}\n </div>\n </div>\n\n <div className=\"relative flex\">\n {/* The list pane: sticky to the left while the timeline scrolls. */}\n {showList ? (\n <div ref={listRef} className=\"sticky left-0 z-30 w-44 shrink-0 border-e border-border bg-card sm:w-72\">\n {rows.list.map((r) =>\n r.kind === \"group\" ? (\n <GroupRow\n key={r.key}\n group={r.group}\n by={by}\n ctx={ctx}\n count={r.group.tasks.length}\n folded={folded.has(r.group.key)}\n hot={hot === r.key}\n onHot={() => setHot(r.key)}\n onFold={() =>\n setFolded((s) => {\n const n = new Set(s);\n if (n.has(r.group.key)) n.delete(r.group.key);\n else n.add(r.group.key);\n return n;\n })\n }\n />\n ) : r.kind === \"task\" ? (\n <ListRow\n key={r.key}\n row={r}\n ctx={ctx}\n hot={hot === r.key}\n onHot={() => setHot(r.key)}\n onOpen={onOpen}\n menu={menuFor(r)}\n conflict={conflicted.has(r.task.id)}\n blockers={(conflicted.get(r.task.id) ?? []).map((id) => name(byIdAll.get(id))).join(\" and \")}\n onFold={() =>\n setCollapsed((s) => {\n const n = new Set(s);\n if (n.has(r.task.id)) n.delete(r.task.id);\n else n.add(r.task.id);\n return n;\n })\n }\n />\n ) : (\n <QuickAdd\n key={r.key}\n uid={uid}\n group={r.group}\n addLabel={addLabel}\n open={adding === r.group.key}\n onOpen={() => setAdding(r.group.key)}\n onClose={() => setAdding((a) => (a === r.group.key ? null : a))}\n onCreate={create}\n />\n ),\n )}\n </div>\n ) : null}\n\n {/* The timeline. */}\n <div className=\"relative shrink-0\" style={{ width, height: rows.height }}>\n <div aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0\">\n {bands.map((b) => (\n <span key={b.start} className=\"absolute inset-y-0 bg-muted/50\" style={{ left: b.x, width: b.w }} />\n ))}\n {tiers.bottom.slice(1).map((c) => (\n <span key={c.key} className=\"absolute inset-y-0 w-px bg-border/50\" style={{ left: c.x }} />\n ))}\n {rows.list\n .filter((r) => r.kind === \"group\")\n .map((r) => (\n <span key={r.key} className={cx(\"absolute inset-x-0 bg-muted/40\", r.y > 0 && \"border-t border-border\")} style={{ top: r.y, height: ROW }} />\n ))}\n </div>\n\n {rows.list.map((r) => {\n if (r.kind === \"add\") return <div key={r.key} style={{ height: ADD_ROW }} />;\n if (r.kind === \"group\") {\n const s = groupSpan(r.group.tasks, byId);\n return (\n <div key={r.key} className={cx(\"relative\", hot === r.key && \"bg-accent/40\")} style={{ height: ROW }} onPointerEnter={() => setHot(r.key)}>\n {s ? (\n <span\n aria-hidden=\"true\"\n className=\"absolute top-1/2 h-1.5 -translate-y-1/2 rounded-full bg-muted-foreground/25\"\n style={{ left: xOf(s.start, from, dw), width: Math.max(lengthOf(s) * dw, 6) }}\n />\n ) : null}\n </div>\n );\n }\n const box = boxes.get(r.task.id);\n const first = box && Math.abs(box.y - (r.y + ROW / 2)) < 1;\n return (\n <TimelineRow key={r.key} row={r} hot={hot === r.key} onHot={() => setHot(r.key)} linking={linking && editable}>\n {(over) =>\n r.span ? (\n <Bar\n row={r}\n x={r.milestone && !r.hasChildren ? xOf(r.span.end, from, dw) : xOf(r.span.start, from, dw)}\n w={r.milestone && !r.hasChildren ? dw : Math.max(lengthOf(r.span) * dw, 6)}\n dayWidth={dw}\n tint={tintOf(r.task)}\n shape={r.hasChildren ? \"summary\" : r.milestone ? \"milestone\" : \"bar\"}\n editable={editable && first !== false}\n onOpen={onOpen}\n onPreview={onPreview}\n onNudge={onNudge}\n clickGuard={clickGuard}\n dim={critical && path.size > 0 && !path.has(r.task.id)}\n critical={critical && path.has(r.task.id)}\n over={over}\n moving={Boolean(drag && moved.has(r.task.id))}\n hintId={`${uid}-hint`}\n locale={locale}\n />\n ) : editable ? (\n <Unscheduled row={r} dayWidth={dw} length={r.milestone ? 1 : newTaskLength} onSchedule={schedule} />\n ) : null\n }\n </TimelineRow>\n );\n })}\n\n {/* raw: an SVG drawing — the dependency arrows over the\n bars; no kit part draws them. */}\n <svg aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0 z-[5] overflow-visible\" width={width} height={rows.height}>\n {edges.map((e) => {\n const a = boxes.get(e.from);\n const b = boxes.get(e.to);\n const { d, head } = arrowPath(a, b, { rowHeight: ROW });\n const bad = conflicts.has(`${e.from}>${e.to}`);\n const hotPath = critical && path.has(e.from) && path.has(e.to);\n const faded = critical && path.size > 0 && !hotPath;\n const stroke = bad ? \"stroke-destructive\" : hotPath ? \"stroke-primary\" : \"stroke-muted-foreground/70\";\n const fill = bad ? \"fill-destructive\" : hotPath ? \"fill-primary\" : \"fill-muted-foreground/70\";\n return (\n <g key={`${e.from}>${e.to}`} className={cx(faded && \"opacity-30\", \"motion-safe:transition-opacity motion-safe:duration-150\")}>\n <path d={d} fill=\"none\" className={stroke} strokeWidth={hotPath ? 2 : 1.25} strokeDasharray={bad ? \"4 3\" : undefined} strokeLinecap=\"round\" />\n <path d={head} className={fill} />\n </g>\n );\n })}\n </svg>\n\n {todayIn ? (\n <span aria-hidden=\"true\" className=\"pointer-events-none absolute inset-y-0 z-[4] w-px bg-primary/70\" style={{ left: tx + dw / 2 }} />\n ) : null}\n </div>\n </div>\n </div>\n </div>\n </TaskDragProvider>\n );\n }\n\n return (\n <section aria-label={label} className={cx(\"grid min-w-0 gap-3\", className)}>\n {toolbar && all.length ? (\n <ViewToolbar\n view={view}\n onViewChange={setViewRaw}\n ctx={ctx}\n tasks={all}\n controls={[\"search\", \"filter\", \"group\", \"sort\"]}\n groupFields={GROUP_FIELDS}\n count={shown.tasks.length}\n extra={\n toolbarExtra || addButton ? (\n <>\n {toolbarExtra}\n {addButton}\n </>\n ) : null\n }\n />\n ) : null}\n {readError ? <Notice tone=\"danger\">{readError}</Notice> : null}\n {error ? (\n <Notice tone=\"danger\" onDismiss={clearError}>\n {error}\n </Notice>\n ) : null}\n {notice ? (\n <Notice tone=\"warning\" onDismiss={() => setNotice(null)}>\n {notice}\n </Notice>\n ) : null}\n <div className=\"min-w-0 overflow-hidden rounded-lg border border-border bg-card text-card-foreground\">\n {all.length && !loading ? bar : null}\n {content}\n {critical && all.length && !loading ? (\n <p className=\"border-t border-border px-3 py-2 text-xs text-muted-foreground\" aria-live=\"polite\">\n {path.size > 1 ? (\n <>\n <Icon name=\"zap\" size={12} className=\"me-1 inline-block align-[-2px] text-primary\" />\n The critical path: {path.size} linked tasks, {pathDays} days end to end. Any day they slip, the finish slips.\n </>\n ) : (\n \"No critical path yet: link tasks (drag the dot past a bar's end onto another bar) to see the chain that sets the finish.\"\n )}\n </p>\n ) : null}\n </div>\n <p id={`${uid}-hint`} className=\"sr-only\">\n Left and right arrows move it a day; with Shift they change its end, with Alt its start. Enter saves at once or opens it; Escape cancels a move.\n </p>\n <p className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {announce}\n </p>\n </section>\n );\n}\n\nfunction groupSpan(tasks, byId) {\n let s = null;\n for (const t of tasks) {\n const x = spanOf(byId.get(t.id) ?? t);\n if (!x) continue;\n s = s ? { start: x.start < s.start ? x.start : s.start, end: x.end > s.end ? x.end : s.end } : x;\n }\n return s;\n}\n\n// The empty state's action: name the first task (it starts today), or\n// start from a template.\nfunction FirstTask({ onCreate, starters, onStarter, addLabel, uid }) {\n const [draft, setDraft] = useState(\"\");\n const [busy, setBusy] = useState(false);\n async function submit() {\n const title = draft.trim();\n if (!title || busy) return;\n setBusy(true);\n await onCreate(title);\n setBusy(false);\n setDraft(\"\");\n }\n return (\n <div className=\"grid w-full max-w-sm justify-items-center gap-3\">\n <form\n className=\"flex w-full items-center gap-2\"\n onSubmit={(e) => {\n e.preventDefault();\n submit();\n }}\n >\n <label htmlFor={`${uid}-first`} className=\"sr-only\">\n The first task\n </label>\n <Input id={`${uid}-first`} value={draft} placeholder=\"What needs doing first?\" onChange={(e) => setDraft(e.target.value)} />\n <Button type=\"submit\" variant=\"primary\" icon=\"plus\" loading={busy} disabled={!draft.trim()}>\n {addLabel}\n </Button>\n </form>\n {starters?.length ? (\n <div className=\"grid justify-items-center gap-1.5\">\n <span className=\"text-xs text-muted-foreground\">Or start from a plan</span>\n <div className=\"flex flex-wrap justify-center gap-2\">\n {starters.map((s) => (\n <Button key={s.label} size=\"sm\" variant=\"secondary\" icon={s.icon ?? \"sparkles\"} onClick={() => onStarter(s)}>\n {s.label}\n </Button>\n ))}\n </div>\n </div>\n ) : null}\n </div>\n );\n}\n"
24
+ },
25
+ {
26
+ "path": "registry/blocks/gantt/rows.jsx",
27
+ "type": "registry:component",
28
+ "target": "@components/makersclaw/blocks/gantt/rows.jsx",
29
+ "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { Avatar, Button, Icon, Input, Menu, StatusDot, cx } from \"../../ui\";\nimport { GroupLabel, formatDate, useDraggableTask, useDropTarget } from \"../task-model\";\nimport { daysFromPixels, lengthOf } from \"./schedule\";\n\n// The gantt's rows: the list pane's row (a task, a group heading, a\n// quick-add) and the timeline's row (a bar, a milestone diamond, a\n// summary bracket, or — for a task without dates — a ghost bar that\n// schedules it where it is clicked). Geometry comes in from gantt.jsx;\n// what a drag did goes back out through `on`.\n\nexport const ROW = 36;\nexport const ADD_ROW = 48;\nconst BAR_H = 24;\n\n// Bar colours, written out whole so Tailwind finds every class: a\n// project's hue (chart-1…5) or a status tone. `fill` is the progress.\nexport const TINTS = {\n 1: { bar: \"bg-chart-1/25 border-chart-1/70\", fill: \"bg-chart-1/45\", solid: \"bg-chart-1\" },\n 2: { bar: \"bg-chart-2/25 border-chart-2/70\", fill: \"bg-chart-2/45\", solid: \"bg-chart-2\" },\n 3: { bar: \"bg-chart-3/25 border-chart-3/70\", fill: \"bg-chart-3/45\", solid: \"bg-chart-3\" },\n 4: { bar: \"bg-chart-4/25 border-chart-4/70\", fill: \"bg-chart-4/45\", solid: \"bg-chart-4\" },\n 5: { bar: \"bg-chart-5/25 border-chart-5/70\", fill: \"bg-chart-5/45\", solid: \"bg-chart-5\" },\n primary: { bar: \"bg-primary/25 border-primary/70\", fill: \"bg-primary/45\", solid: \"bg-primary\" },\n info: { bar: \"bg-info/25 border-info/70\", fill: \"bg-info/45\", solid: \"bg-info\" },\n success: { bar: \"bg-success/25 border-success/70\", fill: \"bg-success/45\", solid: \"bg-success\" },\n warning: { bar: \"bg-warning/25 border-warning/70\", fill: \"bg-warning/45\", solid: \"bg-warning\" },\n danger: { bar: \"bg-destructive/25 border-destructive/70\", fill: \"bg-destructive/45\", solid: \"bg-destructive\" },\n neutral: { bar: \"bg-muted-foreground/15 border-muted-foreground/55\", fill: \"bg-muted-foreground/30\", solid: \"bg-muted-foreground\" },\n};\n\n/** \"Sep 22 – 26\" · \"Sep 29 – Oct 3\" · \"Sep 22\" */\nexport function spanWords(span, locale) {\n if (!span) return \"No dates\";\n const a = formatDate(span.start, { month: \"short\", day: \"numeric\" }, { locale });\n if (span.start === span.end) return a;\n const sameMonth = span.start.slice(0, 7) === span.end.slice(0, 7);\n const b = formatDate(span.end, sameMonth ? { day: \"numeric\" } : { month: \"short\", day: \"numeric\" }, { locale });\n return `${a} – ${b}`;\n}\n\n// ---- the list pane -------------------------------------------------------\n\n/** A group's heading in the list pane: fold, face, count. */\nexport function GroupRow({ group, by, ctx, folded, onFold, hot, onHot, count }) {\n return (\n <div\n className={cx(\"flex items-center gap-1 border-t border-border bg-muted/40 pe-2 ps-1 first:border-t-0\", hot && \"bg-accent/60\")}\n style={{ height: ROW }}\n onPointerEnter={onHot}\n >\n {/* raw: the heading folds its group; a kit Button would centre it\n and weigh it as an action. */}\n <button\n type=\"button\"\n aria-expanded={!folded}\n onClick={onFold}\n className=\"flex h-8 min-w-0 flex-1 items-center gap-1.5 rounded-md px-1 text-left outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <Icon name={folded ? \"chevron-right\" : \"chevron-down\"} size={14} className=\"shrink-0 text-muted-foreground\" />\n <GroupLabel group={group} by={by} ctx={ctx} className=\"min-w-0\" />\n <span data-nums className=\"shrink-0 text-xs font-normal tabular-nums text-muted-foreground\">\n {count}\n </span>\n </button>\n </div>\n );\n}\n\n/** A task's row in the list pane: fold (a parent), status, title, the\n * first face and its length; a menu on hover. */\nexport function ListRow({ row, ctx, hot, onHot, onFold, onOpen, menu, conflict, blockers }) {\n const t = row.task;\n const status = ctx.statusById.get(String(t.status));\n const person = t.assignees?.[0] ? ctx.personById.get(t.assignees[0]) : null;\n const len = lengthOf(row.span);\n const title = t.title || \"Untitled\";\n return (\n <div\n className={cx(\"group/row flex items-center gap-1.5 pe-1\", hot && \"bg-accent/60\")}\n style={{ height: ROW, paddingInlineStart: 4 + row.depth * 16 }}\n onPointerEnter={onHot}\n >\n {row.hasChildren ? (\n // raw: a 24px fold inside a 36px row; the kit's IconButton is a\n // 36/44px target that would push the title out of line.\n <button\n type=\"button\"\n aria-expanded={!row.collapsed}\n aria-label={`${row.collapsed ? \"Show\" : \"Hide\"} the subtasks of ${title}`}\n onClick={onFold}\n className=\"inline-flex size-6 shrink-0 items-center justify-center rounded-sm text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <Icon name={row.collapsed ? \"chevron-right\" : \"chevron-down\"} size={14} />\n </button>\n ) : (\n <span aria-hidden=\"true\" className=\"size-6 shrink-0\" />\n )}\n {row.milestone ? (\n <Icon name=\"diamond\" size={12} className=\"shrink-0 text-muted-foreground\" />\n ) : (\n <StatusDot tone={status ? status.tone ?? (status.hue ? undefined : \"neutral\") : \"neutral\"} hue={status?.hue} size=\"sm\" />\n )}\n {onOpen ? (\n // raw: a title that opens the task; a kit Button would centre and\n // pad it as an action.\n <button\n type=\"button\"\n title={title}\n onClick={() => onOpen(t)}\n className={cx(\n \"min-w-0 flex-1 truncate rounded-sm text-left text-sm outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring\",\n row.done && \"text-muted-foreground line-through\",\n row.hasChildren && \"font-medium\",\n )}\n >\n {title}\n </button>\n ) : (\n <span title={title} className={cx(\"min-w-0 flex-1 truncate text-sm\", row.done && \"text-muted-foreground line-through\", row.hasChildren && \"font-medium\")}>\n {title}\n </span>\n )}\n {conflict ? (\n <span className=\"shrink-0 text-destructive\" title={`Starts before ${blockers} ends`}>\n <Icon name=\"warning\" size={14} />\n <span className=\"sr-only\">Starts before {blockers} ends</span>\n </span>\n ) : null}\n {person ? <Avatar name={person.name} src={person.src} size=\"xs\" className=\"hidden shrink-0 sm:inline-flex\" /> : null}\n <span data-nums className=\"hidden w-8 shrink-0 text-end text-xs tabular-nums text-muted-foreground sm:inline\">\n {len ? `${len}d` : \"–\"}\n <span className=\"sr-only\">{len ? (len === 1 ? \" day\" : \" days\") : \"no dates\"}</span>\n </span>\n {menu?.length ? (\n <Menu\n label={`More for ${title}`}\n items={menu}\n className=\"shrink-0 opacity-0 transition-opacity duration-150 focus-visible:opacity-100 group-hover/row:opacity-100 group-focus-within/row:opacity-100 aria-expanded:opacity-100 [@media(hover:none)]:opacity-100\"\n />\n ) : null}\n </div>\n );\n}\n\n/** The last row of a group: \"Add task\", then a field — Enter adds and\n * stays open for the next one, Escape closes. */\nexport function QuickAdd({ group, open, onOpen, onClose, onCreate, addLabel, uid }) {\n const [draft, setDraft] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const id = `${uid}-add-${group.key}`;\n async function submit() {\n const title = draft.trim();\n if (!title || busy) return;\n setBusy(true);\n const r = await onCreate(title, group.key);\n setBusy(false);\n if (r?.ok !== false) setDraft(\"\");\n }\n return (\n <div className=\"flex items-center px-1.5\" style={{ height: ADD_ROW }}>\n {open ? (\n <>\n <label htmlFor={id} className=\"sr-only\">\n New task in {group.label}\n </label>\n <Input\n id={id}\n autoFocus\n value={draft}\n disabled={busy}\n placeholder=\"Task name, then Enter\"\n onChange={(e) => setDraft(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && !e.nativeEvent.isComposing) {\n e.preventDefault();\n submit();\n } else if (e.key === \"Escape\") {\n setDraft(\"\");\n onClose();\n }\n }}\n onBlur={() => {\n if (!draft.trim()) onClose();\n }}\n />\n </>\n ) : (\n <Button variant=\"quiet\" size=\"sm\" icon=\"plus\" align=\"start\" onClick={onOpen} className=\"text-muted-foreground\">\n {addLabel}\n </Button>\n )}\n </div>\n );\n}\n\n// ---- the timeline --------------------------------------------------------\n\n/** A task's row on the timeline: the drop target a link lands on, and\n * whatever the task draws. */\nexport function TimelineRow({ row, hot, onHot, linking, children }) {\n const drop = useDropTarget(`drop:${row.key}`, { task: row.task, label: row.task.title || \"Untitled\" }, { disabled: !linking });\n const over = drop.isOver && drop.active?.data?.kind === \"link\" && drop.active.data.task.id !== row.task.id;\n return (\n <div ref={drop.setNodeRef} className={cx(\"relative\", hot && \"bg-accent/40\", over && \"z-[6]\")} style={{ height: ROW }} onPointerEnter={onHot} data-over={over || undefined}>\n {typeof children === \"function\" ? children(over) : children}\n </div>\n );\n}\n\n/** A task's bar: drag the body to move it, an edge to change its start or\n * end, the dot past its end onto another bar to make that one wait on\n * it. Arrow keys move it a day; Shift changes its end, Alt its start.\n * A parent's bar is a summary bracket (moves with its subtasks); a\n * milestone is a diamond on its day. */\nexport function Bar({\n row,\n x,\n w,\n dayWidth,\n tint,\n shape = \"bar\",\n editable,\n onOpen,\n onPreview,\n onNudge,\n clickGuard,\n dim,\n critical,\n over,\n moving,\n hintId,\n locale,\n}) {\n const t = row.task;\n const title = t.title || \"Untitled\";\n const bar = shape === \"bar\";\n const move = useDraggableTask(`bar:${row.key}`, { task: t, kind: \"move\", label: title }, { disabled: !editable });\n const start = useDraggableTask(`start:${row.key}`, { task: t, kind: \"start\", label: `the start of ${title}` }, { disabled: !editable || !bar });\n const end = useDraggableTask(`end:${row.key}`, { task: t, kind: \"end\", label: `the end of ${title}` }, { disabled: !editable || !bar });\n const link = useDraggableTask(`link:${row.key}`, { task: t, kind: \"link\", label: `a link from ${title}` }, { disabled: !editable });\n\n const active = move.isDragging ? [\"move\", move] : start.isDragging ? [\"start\", start] : end.isDragging ? [\"end\", end] : null;\n const days = active ? daysFromPixels(active[1].transform?.x, dayWidth) : 0;\n const kind = active?.[0];\n useEffect(() => {\n if (kind) onPreview?.(t.id, kind, days);\n }, [kind, days, t.id, onPreview]);\n\n const words = spanWords(row.span, locale);\n const len = lengthOf(row.span);\n const progress = typeof row.progress === \"number\" ? Math.max(0, Math.min(100, row.progress)) : null;\n const label = `${title}${shape === \"milestone\" ? \", milestone\" : \"\"}, ${words}${len > 1 ? `, ${len} days` : \"\"}${progress !== null && bar ? `, ${Math.round(progress)}% done` : \"\"}${row.done ? \", done\" : \"\"}`;\n const inside = bar && w >= 64;\n const dragging = Boolean(kind) || moving;\n\n function onKeyDown(e) {\n if (!editable) {\n if ((e.key === \"Enter\" || e.key === \" \") && onOpen) {\n e.preventDefault();\n onOpen(t);\n }\n return;\n }\n if (e.key === \"ArrowLeft\" || e.key === \"ArrowRight\") {\n e.preventDefault();\n const step = e.key === \"ArrowRight\" ? 1 : -1;\n onNudge(t.id, bar && e.shiftKey ? \"end\" : bar && e.altKey ? \"start\" : \"move\", step);\n } else if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n if (moving) onNudge(t.id, null, 0, \"flush\");\n else onOpen?.(t);\n } else if (e.key === \"Escape\" && moving) {\n e.preventDefault();\n onNudge(t.id, null, 0, \"cancel\");\n }\n }\n\n const body = cx(\n \"absolute outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-card\",\n editable ? \"cursor-grab touch-manipulation active:cursor-grabbing\" : onOpen ? \"cursor-pointer\" : \"cursor-default\",\n over && \"ring-2 ring-primary ring-offset-1 ring-offset-card\",\n critical && !over && \"ring-2 ring-primary/70\",\n );\n const handle = editable ? { ...move.handle } : {};\n // Our keys, not dnd-kit's keyboard drag: a bar moves by whole days.\n const common = {\n ...handle,\n ref: move.setNodeRef,\n role: onOpen || editable ? \"button\" : \"img\",\n tabIndex: onOpen || editable ? 0 : undefined,\n \"aria-roledescription\": editable ? \"timeline bar\" : undefined,\n \"aria-describedby\": editable ? hintId : undefined,\n \"aria-label\": label,\n onKeyDown,\n onClick: () => {\n if (clickGuard?.()) return;\n onOpen?.(t);\n },\n };\n\n return (\n <div\n className={cx(\n \"group/bar absolute\",\n !dragging && \"motion-safe:transition-[left,width,opacity] motion-safe:duration-150\",\n dim && !dragging && \"opacity-40\",\n dragging && \"z-20\",\n )}\n style={{ left: x, width: w, top: shape === \"bar\" ? (ROW - BAR_H) / 2 : 0, height: shape === \"bar\" ? BAR_H : ROW }}\n >\n {shape === \"milestone\" ? (\n // raw: a draggable diamond on the timeline; no kit part is a\n // shape that moves.\n <div {...common} className={cx(body, \"left-1/2 top-1/2 size-3.5 -translate-x-1/2 -translate-y-1/2 rotate-45 rounded-[3px] shadow-sm\", row.done ? \"bg-success\" : tint.solid)} />\n ) : shape === \"summary\" ? (\n // raw: the summary bracket over a parent's subtasks; it drags the\n // whole family.\n <div {...common} className={cx(body, \"inset-x-0 top-1/2 h-3 -translate-y-1/2 rounded-sm\")}>\n <span aria-hidden=\"true\" className=\"absolute inset-x-0 top-0 h-1.5 rounded-sm bg-foreground/70\" />\n <span aria-hidden=\"true\" className=\"absolute start-0 top-0 h-3 w-1.5 rounded-b-sm bg-foreground/70\" />\n <span aria-hidden=\"true\" className=\"absolute end-0 top-0 h-3 w-1.5 rounded-b-sm bg-foreground/70\" />\n </div>\n ) : (\n // raw: the bar is a draggable, resizable shape; a kit Button would\n // centre and pad it as an action.\n <div\n {...common}\n className={cx(\n body,\n \"inset-0 overflow-hidden rounded-md border text-xs text-foreground shadow-xs\",\n tint.bar,\n row.done && \"opacity-70\",\n dragging && \"shadow-md\",\n )}\n >\n {progress ? <span aria-hidden=\"true\" className={cx(\"absolute inset-y-0 start-0\", tint.fill)} style={{ width: `${progress}%` }} /> : null}\n {inside ? (\n <span className=\"relative flex h-full items-center gap-1 px-2\">\n {row.done ? <Icon name=\"check\" size={12} className=\"shrink-0 text-success\" /> : null}\n <span className={cx(\"truncate font-medium\", row.done && \"line-through decoration-muted-foreground\")}>{title}</span>\n </span>\n ) : null}\n </div>\n )}\n\n {editable && bar ? (\n <>\n {/* raw: pointer-only edge grips (the keyboard uses Shift/Alt +\n arrows); dnd-kit's listeners go on the element that drags. */}\n <span\n ref={start.setNodeRef}\n {...start.handle}\n role={undefined}\n tabIndex={-1}\n aria-hidden=\"true\"\n className=\"absolute inset-y-0 -start-1 z-10 flex w-3 cursor-ew-resize touch-none items-center justify-center opacity-0 transition-opacity duration-150 group-hover/bar:opacity-100 [@media(hover:none)]:hidden\"\n >\n <span className=\"h-3 w-1 rounded-full bg-foreground/50\" />\n </span>\n <span\n ref={end.setNodeRef}\n {...end.handle}\n role={undefined}\n tabIndex={-1}\n aria-hidden=\"true\"\n className=\"absolute inset-y-0 -end-1 z-10 flex w-3 cursor-ew-resize touch-none items-center justify-center opacity-0 transition-opacity duration-150 group-hover/bar:opacity-100 [@media(hover:none)]:hidden\"\n >\n <span className=\"h-3 w-1 rounded-full bg-foreground/50\" />\n </span>\n </>\n ) : null}\n {editable ? (\n <LinkDot link={link} title={title} />\n ) : null}\n\n {/* The words beside the bar: its title when it does not fit inside,\n its dates on hover, focus and while it moves. */}\n <span\n className={cx(\n \"pointer-events-none absolute top-1/2 flex -translate-y-1/2 items-center gap-2 whitespace-nowrap ps-2 text-xs\",\n editable ? \"start-[calc(100%+14px)]\" : \"start-full\",\n )}\n >\n {!inside ? <span className={cx(\"font-medium text-foreground\", row.done && \"text-muted-foreground line-through\")}>{title}</span> : null}\n <span\n data-nums\n className={cx(\n \"tabular-nums text-muted-foreground transition-opacity duration-150\",\n dragging ? \"opacity-100\" : \"opacity-0 group-hover/bar:opacity-100 group-focus-within/bar:opacity-100\",\n )}\n >\n {words}\n {len > 1 ? ` · ${len}d` : \"\"}\n </span>\n </span>\n </div>\n );\n}\n\n// The dot past a bar's end: drag it onto another bar to link them. While\n// it is dragged, a dashed line follows the pointer.\nfunction LinkDot({ link, title }) {\n const tx = link.transform?.x ?? 0;\n const ty = link.transform?.y ?? 0;\n return (\n <>\n {/* raw: a pointer-only link grip (the keyboard links from the row's\n menu, \"Waits on\"). */}\n <span\n ref={link.setNodeRef}\n {...link.handle}\n role={undefined}\n tabIndex={-1}\n aria-hidden=\"true\"\n title={`Drag onto another task to make it wait on ${title}`}\n className={cx(\n \"absolute -end-3.5 top-1/2 z-10 flex size-4 -translate-y-1/2 cursor-crosshair touch-none items-center justify-center\",\n link.isDragging ? \"opacity-100\" : \"opacity-0 transition-opacity duration-150 group-hover/bar:opacity-100 [@media(hover:none)]:hidden\",\n )}\n >\n <span className=\"size-2.5 rounded-full border-2 border-primary bg-card\" />\n </span>\n {link.isDragging ? (\n // raw: an SVG drawing — the link being drawn; no kit part draws one.\n <svg aria-hidden=\"true\" className=\"pointer-events-none absolute -end-1.5 top-1/2 z-20 overflow-visible\" width=\"1\" height=\"1\">\n <path d={`M0 0 L${tx} ${ty}`} className=\"stroke-primary\" strokeWidth={1.5} strokeDasharray=\"4 3\" fill=\"none\" />\n <circle cx={tx} cy={ty} r={3.5} className=\"fill-primary\" />\n </svg>\n ) : null}\n </>\n );\n}\n\n/** A task with no dates: a ghost bar follows the pointer; a click (or a\n * tap) schedules it there. */\nexport function Unscheduled({ row, dayWidth, length, onSchedule }) {\n const [day, setDay] = useState(null);\n const dayAt = (e) => {\n const r = e.currentTarget.getBoundingClientRect();\n return Math.max(0, Math.floor((e.clientX - r.left) / dayWidth));\n };\n const w = Math.max(length * dayWidth, 24);\n return (\n // raw: a whole timeline row is the place to click; a kit Button would\n // be one small target in a row a person points anywhere along.\n <button\n type=\"button\"\n aria-label={`Schedule ${row.task.title || \"Untitled\"}: pick a day`}\n tabIndex={-1}\n className=\"absolute inset-0 cursor-copy outline-none\"\n onPointerMove={(e) => setDay(dayAt(e))}\n onPointerLeave={() => setDay(null)}\n onClick={(e) => onSchedule(row.task, dayAt(e))}\n >\n {day !== null ? (\n <span\n className=\"pointer-events-none absolute flex items-center overflow-hidden whitespace-nowrap rounded-md border border-dashed border-primary/60 bg-primary/5 px-2 text-xs text-muted-foreground\"\n style={{ left: day * dayWidth, width: w, top: (ROW - BAR_H) / 2, height: BAR_H }}\n >\n {w >= 96 ? \"Click to schedule\" : \"\"}\n </span>\n ) : null}\n </button>\n );\n}\n"
30
+ },
31
+ {
32
+ "path": "registry/blocks/gantt/schedule.js",
33
+ "type": "registry:component",
34
+ "target": "@components/makersclaw/blocks/gantt/schedule.js",
35
+ "content": "// The gantt's arithmetic — pure, no React, no pixels read from the DOM.\n// The zoom levels, the window a timeline covers, its two header tiers,\n// where a day sits in pixels, what a drag or a resize saves (and which\n// waiting tasks it pushes along), and the elbow a dependency arrow takes.\n// The dates are task-model's: wall-calendar strings, day numbers for math.\n\nimport {\n addDays,\n descendantIds,\n diffDays,\n endOfMonth,\n formatDate,\n isWeekend,\n shiftDates,\n spanOf,\n spanPatch,\n startOfMonth,\n startOfWeek,\n timeScale,\n} from \"../task-model\";\n\n/** The zoom levels: how wide a day is, what the columns are, how much\n * room the window keeps either side of the work, and its least length. */\nexport const ZOOMS = {\n day: { key: \"day\", label: \"Day\", unit: \"day\", dayWidth: 36, pad: 7, min: 56, top: \"month\" },\n week: { key: \"week\", label: \"Week\", unit: \"week\", dayWidth: 16, pad: 14, min: 150, top: \"month\" },\n month: { key: \"month\", label: \"Month\", unit: \"month\", dayWidth: 5, pad: 45, min: 420, top: \"year\" },\n quarter: { key: \"quarter\", label: \"Quarter\", unit: \"quarter\", dayWidth: 1.75, pad: 90, min: 900, top: \"year\" },\n};\n\nexport const zoomOf = (key) => ZOOMS[key] ?? ZOOMS.week;\n\n/** The first day of `day`'s column at `unit`. */\nexport function unitStart(day, unit, weekStartsOn = 1) {\n if (unit === \"day\" || unit === \"week\") return startOfWeek(day, weekStartsOn);\n if (unit === \"month\") return startOfMonth(day);\n return timeScale(day, day, \"quarter\")[0]?.start ?? day;\n}\n\n/** The last day of `day`'s column at `unit`. */\nexport function unitEnd(day, unit, weekStartsOn = 1) {\n if (unit === \"day\" || unit === \"week\") return addDays(startOfWeek(day, weekStartsOn), 6);\n if (unit === \"month\") return endOfMonth(day);\n return timeScale(day, day, \"quarter\")[0]?.end ?? day;\n}\n\n/** The window a timeline covers: every task's span and today, padded by\n * the zoom, snapped to whole columns, at least the zoom's `min` days.\n * → { from, to, days } */\nexport function ganttRange(tasks, zoomKey, today, { weekStartsOn = 1 } = {}) {\n const z = zoomOf(zoomKey);\n let lo = null;\n let hi = null;\n for (const t of tasks ?? []) {\n const s = spanOf(t);\n if (!s) continue;\n if (lo === null || s.start < lo) lo = s.start;\n if (hi === null || s.end > hi) hi = s.end;\n }\n if (today) {\n if (lo === null || today < lo) lo = today;\n if (hi === null || today > hi) hi = today;\n }\n if (lo === null) return { from: null, to: null, days: 0 };\n let from = unitStart(addDays(lo, -z.pad), z.unit, weekStartsOn);\n let to = unitEnd(addDays(hi, z.pad), z.unit, weekStartsOn);\n if (diffDays(from, to) + 1 < z.min) to = unitEnd(addDays(from, z.min - 1), z.unit, weekStartsOn);\n return { from, to, days: diffDays(from, to) + 1 };\n}\n\n/** A day's left edge in pixels from the window's first day. */\nexport function xOf(day, from, dayWidth) {\n const d = diffDays(from, day);\n return d === null ? null : d * dayWidth;\n}\n\n/** The two header tiers over [from, to], in pixels: `top` (months, or\n * years when zoomed out) and `bottom` (the zoom's columns), each\n * { key, start, end, x, w, label, weekend?, today? }. */\nexport function headerTiers(from, to, zoomKey, { today, weekStartsOn = 1, locale } = {}) {\n const z = zoomOf(zoomKey);\n const place = (start, end) => {\n const a = start < from ? from : start;\n const b = end > to ? to : end;\n return { x: xOf(a, from, z.dayWidth), w: (diffDays(a, b) + 1) * z.dayWidth };\n };\n const bottom = timeScale(from, to, z.unit, { weekStartsOn, locale }).map((c) => {\n let label = c.label;\n if (z.unit === \"month\") label = formatDate(c.start, { month: \"short\" }, { locale });\n if (z.unit === \"quarter\") label = c.label.split(\" \")[0];\n return {\n key: c.key,\n start: c.start,\n end: c.end,\n label,\n weekend: z.unit === \"day\" && isWeekend(c.start),\n today: Boolean(today) && c.start <= today && today <= c.end,\n ...place(c.start, c.end),\n };\n });\n let top;\n if (z.top === \"month\") {\n top = timeScale(from, to, \"month\", { locale }).map((c) => ({\n key: c.key,\n start: c.start,\n end: c.end,\n label: formatDate(c.start, { month: \"long\", year: \"numeric\" }, { locale }),\n ...place(c.start, c.end),\n }));\n } else {\n top = [];\n for (let y = +from.slice(0, 4); y <= +to.slice(0, 4); y++) {\n const start = `${y}-01-01`;\n const end = `${y}-12-31`;\n top.push({ key: String(y), start, end, label: String(y), ...place(start, end) });\n }\n }\n return { top, bottom };\n}\n\n/** The weekend columns of the window (day and week zoom), for shading. */\nexport function weekendBands(from, to, zoomKey, weekend) {\n const z = zoomOf(zoomKey);\n if (z.unit !== \"day\" && z.unit !== \"week\") return [];\n const out = [];\n const n = diffDays(from, to);\n for (let i = 0; i <= n; i++) {\n const d = addDays(from, i);\n if (!isWeekend(d, weekend)) continue;\n const last = out[out.length - 1];\n if (last && last.end === addDays(d, -1)) last.end = d;\n else out.push({ start: d, end: d });\n }\n return out.map((b) => ({ ...b, x: xOf(b.start, from, z.dayWidth), w: (diffDays(b.start, b.end) + 1) * z.dayWidth }));\n}\n\n/** A milestone: a task marked `milestone: true` (or `type: \"milestone\"`).\n * It sits on one day, its due date (else its start). */\nexport function isMilestone(task) {\n return task?.milestone === true || task?.type === \"milestone\";\n}\n\n/** The span a row draws: its own, or — for a task with subtasks — the\n * union of its own and every descendant's (a summary bar). */\nexport function summarySpan(task, tasks) {\n let span = spanOf(task);\n for (const id of descendantIds(tasks, task.id)) {\n const s = spanOf(tasks.find((t) => t.id === id));\n if (!s) continue;\n span = span ? { start: s.start < span.start ? s.start : span.start, end: s.end > span.end ? s.end : span.end } : s;\n }\n return span;\n}\n\n/** What moving or stretching a task saves, as [{ id, patch }]:\n * - kind \"move\": its dates shift by `days` (its subtasks with it);\n * - kind \"start\" / \"end\": that edge moves by `days` (never past the other);\n * - with `push` (default), every task waiting on a moved one that would\n * now start on or before it ends slides later just enough, keeping its\n * length — and so on down the chain. Nothing is ever pulled earlier.\n * `days` 0 → []. The same answer drives the live preview and the save. */\nexport function planMove(tasks, id, kind, days, { push = true } = {}) {\n if (!days) return [];\n const byId = new Map(tasks.map((t) => [t.id, t]));\n const task = byId.get(id);\n const span = spanOf(task);\n if (!task || (!span && kind !== \"move\")) return [];\n const patches = new Map();\n const current = (tid) => ({ ...byId.get(tid), ...patches.get(tid) });\n const set = (tid, patch) => patches.set(tid, { ...patches.get(tid), ...patch });\n\n if (kind === \"move\") {\n if (span) set(id, shiftDates(task, days));\n for (const d of descendantIds(tasks, id)) if (spanOf(byId.get(d))) set(d, shiftDates(byId.get(d), days));\n } else if (kind === \"start\") {\n const next = addDays(span.start, days);\n set(id, spanPatch(task, next > span.end ? span.end : next, span.end));\n } else if (kind === \"end\") {\n const next = addDays(span.end, days);\n set(id, spanPatch(task, span.start, next < span.start ? span.start : next));\n } else return [];\n\n if (push) {\n const waiting = new Map();\n for (const t of tasks) for (const d of t.dependsOn ?? []) (waiting.get(d) ?? waiting.set(d, []).get(d)).push(t.id);\n const queue = [...patches.keys()];\n let guard = 0;\n while (queue.length && guard++ < 5000) {\n const from = queue.shift();\n const fs = spanOf(current(from));\n if (!fs) continue;\n for (const to of waiting.get(from) ?? []) {\n const ts = spanOf(current(to));\n if (!ts || ts.start > fs.end) continue;\n const by = diffDays(ts.start, fs.end) + 1;\n set(to, shiftDates(current(to), by));\n queue.push(to);\n }\n }\n }\n return [...patches].map(([pid, patch]) => ({ id: pid, patch }));\n}\n\n/** The patch that starts a task the day after the last of its blockers\n * ends, keeping its length (a date conflict's fix). null when it has no\n * scheduled blockers or already starts after them. */\nexport function afterBlockersPatch(task, tasks) {\n const span = spanOf(task);\n if (!span) return null;\n let last = null;\n for (const d of task.dependsOn ?? []) {\n const s = spanOf(tasks.find((t) => t.id === d));\n if (s && (last === null || s.end > last)) last = s.end;\n }\n if (last === null || span.start > last) return null;\n return shiftDates(task, diffDays(span.start, last) + 1);\n}\n\n/** A task's length in calendar days (1 for a one-day task), null when\n * it has no dates. */\nexport function lengthOf(span) {\n return span ? diffDays(span.start, span.end) + 1 : null;\n}\n\n/** An SVG path through `points` ([x, y] pairs) with its corners rounded\n * by `r` (less where a leg is short). */\nexport function roundedPath(points, r = 4) {\n const pts = points.filter((p, i) => i === 0 || p[0] !== points[i - 1][0] || p[1] !== points[i - 1][1]);\n if (pts.length < 2) return \"\";\n const f = (n) => Math.round(n * 10) / 10;\n let d = `M${f(pts[0][0])} ${f(pts[0][1])}`;\n for (let i = 1; i < pts.length - 1; i++) {\n const [px, py] = pts[i - 1];\n const [x, y] = pts[i];\n const [nx, ny] = pts[i + 1];\n const inLen = Math.hypot(x - px, y - py);\n const outLen = Math.hypot(nx - x, ny - y);\n const k = Math.min(r, inLen / 2, outLen / 2);\n const ax = x - ((x - px) / (inLen || 1)) * k;\n const ay = y - ((y - py) / (inLen || 1)) * k;\n const bx = x + ((nx - x) / (outLen || 1)) * k;\n const by = y + ((ny - y) / (outLen || 1)) * k;\n d += ` L${f(ax)} ${f(ay)} Q${f(x)} ${f(y)} ${f(bx)} ${f(by)}`;\n }\n const last = pts[pts.length - 1];\n return `${d} L${f(last[0])} ${f(last[1])}`;\n}\n\n/** The elbow from a finished task to the one waiting on it (finish to\n * start). Boxes are what the rows draw: { x0, x1, y (centre) }. When the\n * waiting task starts far enough right, the line runs out, turns and\n * enters from the left; when it does not (or it starts too early — a\n * conflict), it steps out, drops between the rows and comes back in.\n * → { d, head } — the line, and the arrowhead's path at its end. */\nexport function arrowPath(a, b, { rowHeight = 36, gap = 10, radius = 4 } = {}) {\n const sx = a.x1;\n const sy = a.y;\n const ex = b.x0;\n const ey = b.y;\n let points;\n if (Math.abs(ey - sy) < 1) points = [[sx, sy], [ex, ey]];\n else if (ex - gap >= sx + gap) points = [[sx, sy], [ex - gap, sy], [ex - gap, ey], [ex, ey]];\n else {\n const mid = sy + (ey > sy ? rowHeight / 2 : -rowHeight / 2);\n points = [[sx, sy], [sx + gap, sy], [sx + gap, mid], [ex - gap, mid], [ex - gap, ey], [ex, ey]];\n }\n const f = (n) => Math.round(n * 10) / 10;\n const head = `M${f(ex - 5)} ${f(ey - 3.5)} L${f(ex)} ${f(ey)} L${f(ex - 5)} ${f(ey + 3.5)} Z`;\n return { d: roundedPath(points, radius), head };\n}\n\n/** Pixels → whole days at a zoom (a drag's delta, rounded to the nearest\n * day; never -0). */\nexport function daysFromPixels(px, dayWidth) {\n const n = Math.round((px || 0) / dayWidth);\n return n === 0 ? 0 : n;\n}\n\n"
36
+ },
37
+ {
38
+ "path": "registry/blocks/gantt/README.md",
39
+ "type": "registry:file",
40
+ "target": "@components/makersclaw/blocks/gantt/README.md",
41
+ "content": "# Gantt\n\nThe Gantt view of work, like Linear's timeline or TeamGantt. The task list\nsits on the left — groups that fold, subtasks indented under their parent\n— and the timeline on the right, under a two-tier date header (months over\ndays, or years over months). Each task is a bar in its project's colour,\nfilled as far as it is done; a parent is a summary bracket over its\nsubtasks; a milestone is a diamond on its day; arrows run from each task to\nthe ones waiting on it, red and dashed when the dates disagree. A line marks\ntoday; weekends are shaded.\n\nDrag a bar to move it, an edge to change its start or end, and the dot past\nits end onto another bar to make that one wait on it. Every task waiting on\na moved one slides along *while you drag*, and \"Finishes Fri, Oct 16 +3\ndays\" at the top says what the move cost. Zoom by day, week, month or\nquarter; switch on the critical path and the chain that sets the finish\nlights up while the rest fades. A task with no dates shows a ghost bar under\nthe pointer: click to schedule it there. Search, filters, group and sort\nlive in the shared toolbar above.\n\nEvery change saves through one async `onChange` and shows at once; when the\nsave fails the timeline puts it back and says why.\n\n```jsx\nimport { Gantt } from \"../components/blocks/gantt\"; // a Makersclaw app (\"./blocks/gantt\" from components/)\nimport { Gantt } from \"@/components/makersclaw/blocks/gantt\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component, and\nload the tasks in the server surface (README §4). It is built on\n`task-model` (added with it): the task shape, the change shapes and the\nhelpers are documented there.\n\n## The data\n\nTasks in task-model's shape — `{ id, title, status, assignees, start?,\ndue?, progress?, parentId?, dependsOn?, project?, … }` — with the statuses,\npeople and projects they refer to. What the gantt reads:\n\n- `start` · `due` — the bar runs from start to due, both days included. One\n of them alone is a one-day bar; neither is an undated task (a row with no\n bar, scheduled by a click).\n- `milestone: true` (or `type: \"milestone\"`) — a diamond on its `due` day\n (else its `start`).\n- `dependsOn: [\"t7\"]` — this task waits on t7: an arrow from t7's end to\n its start (finish to start).\n- `progress` (0–100) — the bar's fill; without it, 100 when done, else\n from its subtasks (task-model's `progressOf`).\n- `parentId` — a subtask, drawn under its parent; the parent's bar is a\n summary bracket over all of them and moves them together.\n- `project` — the bar's colour (the project's `hue`, chart-1…5) when\n `colorBy` is `\"project\"`.\n\nA drag saves only `start` and `due` (and, for a link, `dependsOn`).\n\n## Props\n\n- `tasks` — the tasks. Pass `useFixture(rows, SAMPLE).rows` and the\n examples change on screen and save nothing (they are not rows).\n- `statuses` — `[{ id, label, tone?, hue?, done? }]` in order. Default: To\n do, In progress, In review, Done.\n- `people` — `[{ id, name, src? }]`.\n- `projects` — `[{ id, name, hue? }]`: `hue` 1–5 colours the bars.\n- `fields` — custom fields (`{ key, label, kind, options? }`), for the\n toolbar's filters and grouping.\n- `onChange(change)` — save a change; return `{ ok: true }` or\n `{ ok: false, error }` (a throw counts as failed). A move that pushes\n waiting tasks is one `batch` change; a link is an `update` of\n `dependsOn`. In a Makersclaw app, loop over `changeWrites(change)` into\n insertRow / updateRow / deleteRow (task-model README, \"A change\").\n Without `onChange` the timeline only shows: no drag, links, quick-add or\n menus beyond Open.\n- `onOpen(task)` — a click on a bar or a title (Enter on a focused bar),\n and \"Open\" in the row's menu: open your `Sheet` with the task.\n- `view` · `defaultView` · `onViewChange(view)` — search, filters, group\n and sort (task-model's view). The gantt's default: grouped by project,\n sorted by start date. Group by project, status, assignee, priority or\n none.\n- `zoom` · `defaultZoom` (`\"week\"`) · `onZoomChange(zoom)` — `\"day\"` ·\n `\"week\"` · `\"month\"` · `\"quarter\"`.\n- `colorBy` — `\"project\"` (default; the project's hue, else the primary\n colour) · `\"status\"` (the status's tone) · `\"none\"`.\n- `pushDependents` — default true: moving or stretching a task later slides\n every task waiting on it (and theirs) just far enough to start the day\n after it ends, keeping their lengths. Nothing is pulled earlier. False:\n only the dragged task moves, and conflicts show as red arrows.\n- `defaultCritical` — default false: start with the critical path on.\n- `newTaskLength` — default 3: the days a quick-added or click-scheduled\n task spans (a quick-added task starts today).\n- `today` — `\"YYYY-MM-DD\"`: the today line and where the timeline opens.\n Default the browser's today; fix it in a demo.\n- `weekStartsOn` (default 1, Monday) · `weekend` (default `[0, 6]`) ·\n `holidays` — the header's weeks and the shaded days.\n- `toolbar` — default true; false hides search, filter, group, sort and\n \"Add task\". `toolbarExtra` — your controls before \"Add task\".\n- `addLabel` — default `\"Add task\"`.\n- `starters` — plans the empty state offers:\n `[{ label, icon?, tasks: [{ title, days?, milestone?, parallel? }] }]`.\n Tasks run one after another from today, each waiting on the one before;\n `parallel: true` starts with the previous one instead. One press\n creates them all in one change.\n- `emptyTitle` · `emptyBody` — the empty state's words.\n- `height` — default 560: the timeline's most height in pixels (never\n more than 75% of the window); it scrolls inside.\n- `loading` — skeleton bars in place of the timeline.\n- `error` — a danger Notice above it (a failed read).\n- `label` — what it is called to a screen reader, default `\"Timeline\"`.\n- `locale` — dates in the header and labels; default the browser's.\n- `className` — layout around it (width, margin). Never colour.\n\nThe helpers it draws with are exported for a view of your own:\n`ZOOMS`, `ganttRange(tasks, zoom, today)`, `headerTiers(from, to, zoom)`,\n`planMove(tasks, id, \"move\" | \"start\" | \"end\", days, { push })` (what a\ndrag saves, as `[{ id, patch }]`), `afterBlockersPatch(task, tasks)`,\n`isMilestone`, `summarySpan`, `arrowPath(boxA, boxB)`.\n\n## What it does, precisely\n\n- Moving snaps to whole days at every zoom; the dates show beside the bar\n while it moves. Resizing never crosses the other edge (a bar is at least\n one day). A milestone and a summary bracket move but do not resize; a\n summary moves its whole family.\n- Linking: drag the dot past a bar's end onto another row. A link that\n would loop is refused with a notice. The row menu's \"Waits on\" ticks and\n unticks links from the keyboard.\n- A red, dashed arrow means the waiting task starts before the other ends;\n its row shows a warning and its menu offers \"Start after what it waits\n on\".\n- The critical path is the longest chain of linked, dated work\n (task-model's `criticalPath`): its bars ring in the primary colour, its\n arrows thicken, everything else fades, and a line under the chart says\n how long it is.\n- A task in two groups (two assignees, grouped by person) shows in both;\n the first row drags, the second only opens.\n- Keyboard: Tab reaches the titles, menus and bars. On a bar, ← → move it a\n day, Shift + ← → change its end, Alt + ← → its start; the move saves when\n the keys rest (or on Enter); Escape drops it. Enter opens a bar at rest.\n- On a phone the list pane starts hidden (the bars carry their titles);\n the list button in the chart's bar shows it at 176px. Zoom takes its own\n line. Bars scroll under a finger and drag after a short press; edge grips\n and the link dot are pointer-only (the keyboard and the menu cover them).\n- Delete (row menu) takes the subtasks too; Undo in the toast brings them\n back. A move that pushed other tasks also offers Undo.\n\n## Example\n\n```jsx\n// surfaces/plan.jsx — server: load, hand over\nimport { loadRows } from \"../lib/data\";\nimport Plan from \"../components/Plan\";\n\nexport default async function PlanSurface() {\n const { rows, error } = await loadRows({ source: \"public.tasks\", orderBy: \"start asc\" });\n const people = await loadRows({ source: \"public.people\" });\n const projects = await loadRows({ source: \"public.projects\" });\n return <Plan tasks={rows} people={people.rows} projects={projects.rows} error={error} />;\n}\n```\n\n```jsx\n// components/Plan.jsx — client: the timeline, the task sheet\n\"use client\";\nimport { useState } from \"react\";\nimport { deleteRow, insertRow, updateRow } from \"../lib/actions\";\nimport { Sheet } from \"./ui\";\nimport { Gantt } from \"./blocks/gantt\";\nimport { changeWrites } from \"./blocks/task-model\";\n\nconst COLUMNS = [\"id\", \"title\", \"status\", \"assignees\", \"start\", \"due\", \"progress\", \"parentId\", \"dependsOn\", \"project\", \"milestone\"];\n\nasync function save(change) {\n for (const w of changeWrites(change, { columns: COLUMNS, keepNewIds: false })) {\n const r =\n w.op === \"insert\" ? await insertRow({ source: \"public.tasks\", values: w.values, detail: w.values.title })\n : w.op === \"update\" ? await updateRow({ source: \"public.tasks\", id: w.id, patch: w.patch })\n : await deleteRow({ source: \"public.tasks\", id: w.id });\n if (r?.ok === false) return r;\n }\n return { ok: true };\n}\n\nexport default function Plan({ tasks, people, projects, error }) {\n const [open, setOpen] = useState(null);\n return (\n <>\n <Gantt tasks={tasks} people={people} projects={projects} onChange={save} onOpen={setOpen} error={error}\n starters={[{ label: \"Product launch\", tasks: [{ title: \"Scope\", days: 2 }, { title: \"Build\", days: 10 }, { title: \"Launch\", milestone: true }] }]} />\n <Sheet open={Boolean(open)} onClose={() => setOpen(null)} title={open?.title}>{/* the task's fields */}</Sheet>\n </>\n );\n}\n```\n\n`keepNewIds: false` lets the table make a new task's id. A link made\nbetween two tasks created in the same change (a starter) refers to their\nclient ids: keep them (`keepNewIds: true`, a text id column) when you\noffer starters.\n\nBy person, zoomed to days, coloured by status:\n\n```jsx\n<Gantt tasks={tasks} people={people} onChange={save} defaultZoom=\"day\" colorBy=\"status\" defaultView={{ groupBy: \"assignees\" }} />\n```\n\n## What it will not do\n\n- Start-to-start, finish-to-finish or lag on links: every link is finish to\n start, the next day.\n- Count working days: a bar spans calendar days; weekends are shaded, not\n skipped.\n- Baselines, resource levelling or costs.\n- Reorder rows by drag: the order is the view's sort (start date by\n default); change it in the toolbar.\n- Virtualise thousands of rows: filter, or page them in the surface.\n- Fetch: tasks come in as props; the page re-reads after a save.\n\nA timeline it cannot be is your own component in `components/`, built\nfrom task-model's parts and helpers and the ones above — not an edit to\nthis block. Its files are fixed: `npm run check` refuses a change, and\n`makersclaw add gantt` restores them.\n"
42
+ }
43
+ ],
44
+ "categories": [
45
+ "gantt",
46
+ "timeline",
47
+ "schedule",
48
+ "project plan",
49
+ "roadmap",
50
+ "dependencies",
51
+ "critical path",
52
+ "milestones",
53
+ "teamgantt",
54
+ "linear",
55
+ "clickup",
56
+ "ms project",
57
+ "drag",
58
+ "resize",
59
+ "zoom",
60
+ "tasks",
61
+ "project management"
62
+ ],
63
+ "docs": "Installed to components/makersclaw/blocks/gantt. Import: import { Gantt, ZOOMS, ganttRange, headerTiers, planMove, afterBlockersPatch, isMilestone, summarySpan, arrowPath } from \"@/components/makersclaw/blocks/gantt\". Props and an example: components/makersclaw/blocks/gantt/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
64
+ "meta": {
65
+ "makersclaw": {
66
+ "version": "0.3.0",
67
+ "useWhen": "A screen that plans dated work and shows what waits on what: a launch plan, a project schedule, a content calendar with lead times, an office move, a release. Not for an undated to-do list (task-list), stage columns (board), or days on a month grid (calendar)."
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "job-panel",
4
+ "type": "registry:block",
5
+ "title": "JobPanel",
6
+ "description": "The panel that follows work the founder asked the app's agent for (useJob): each step's label as it runs, Stop, then done or the reason it stopped — and nothing on screen when no job runs. variant=\"generator\" wraps it in a prompt, the surface's options, Generate and a results shelf, with no model, provider or API key of its own.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/core.json"
9
+ ],
10
+ "dependencies": [],
11
+ "files": [
12
+ {
13
+ "path": "registry/blocks/job-panel/index.js",
14
+ "type": "registry:component",
15
+ "target": "@components/makersclaw/blocks/job-panel/index.js",
16
+ "content": "// The JobPanel block — `makersclaw add job-panel`. Fixed: compose it\n// through its props (README.md beside this file).\nexport { JobPanel } from \"./job-panel\";\n"
17
+ },
18
+ {
19
+ "path": "registry/blocks/job-panel/job-panel.jsx",
20
+ "type": "registry:component",
21
+ "target": "@components/makersclaw/blocks/job-panel/job-panel.jsx",
22
+ "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { Button, Card, Empty, Field, Icon, Skeleton, Textarea, cx } from \"../../ui\";\n\n// JobPanel — the panel that follows work the founder asked the app's\n// agent for (useJob in lib/hooks.js, README §5 \"Commissioning work\"). It\n// keeps the four rules a hand-drawn panel breaks: it is not there when\n// nothing runs, it shows each step's label (the founder's phrase, never\n// the tool's name), the agent's text is its account and not the\n// deliverable (the rows are), and Stop / Close are always one press away.\n//\n// const job = useJob();\n// <Button onClick={() => job.start(\"Draft a launch plan for Rowhouse\")}>Generate a launch plan</Button>\n// <JobPanel job={job} title=\"Drafting the launch plan\" doneTitle=\"Launch plan drafted\" />\n//\n// variant=\"generator\" is the same panel inside a small studio: a prompt,\n// the surface's own options, Generate, then the results the job made. The\n// block never calls a model and takes no API key or provider: Generate\n// hands the prompt to the app's agent (job.start, or `onGenerate`), and\n// the results are the rows the job wrote, passed back in as `results`.\n//\n// <JobPanel variant=\"generator\" job={job} promptLabel=\"Describe the photo\"\n// options={<Field …><Select …/></Field>}\n// onGenerate={(prompt) => job.start(`Make a product photo: ${prompt}. Size ${size}.`)}\n// results={photos.map((p) => <PhotoCard key={p.id} photo={p} />)} />\n\nfunction StepMark({ status }) {\n if (status === \"done\") return <Icon name=\"check\" size={14} className=\"shrink-0 text-success\" />;\n if (status === \"failed\") return <Icon name=\"warning\" size={14} className=\"shrink-0 text-destructive\" />;\n return <span aria-hidden=\"true\" className=\"mx-[3px] size-2 shrink-0 animate-pulse rounded-full bg-primary\" />;\n}\n\nexport function JobPanel({ variant = \"panel\", ...props }) {\n if (variant === \"generator\") return <Generator {...props} />;\n return <Panel {...props} />;\n}\n\nfunction Panel({\n job,\n title = \"Working on it\",\n doneTitle = \"Done\",\n failedTitle = \"It stopped\",\n onRetry,\n autoClose = 6000,\n maxSteps = 6,\n showText = true,\n className,\n}) {\n const textRef = useRef(null);\n const running = Boolean(job?.running);\n const failed = !running && Boolean(job?.error);\n const done = !running && !failed && Boolean(job?.finishedAt);\n\n // The agent's account grows at the bottom; keep the newest line in view.\n useEffect(() => {\n const el = textRef.current;\n if (el) el.scrollTop = el.scrollHeight;\n }, [job?.text]);\n\n // A clean finish closes itself: the rows it made are the answer.\n useEffect(() => {\n if (!done || !autoClose || !job?.reset) return undefined;\n const t = setTimeout(() => job.reset(), autoClose);\n return () => clearTimeout(t);\n }, [done, autoClose, job?.finishedAt]); // eslint-disable-line react-hooks/exhaustive-deps\n\n if (!job || !(running || failed || done)) return null;\n\n const steps = job.steps ?? [];\n const hidden = Math.max(0, steps.length - maxSteps);\n const visible = steps.slice(hidden);\n const heading = running ? title : failed ? failedTitle : doneTitle;\n\n return (\n <section\n aria-label={heading}\n className={cx(\"rounded-lg border bg-card p-4 text-card-foreground\", failed ? \"border-destructive/50\" : \"border-border\", className)}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p aria-live=\"polite\" className=\"flex items-center gap-2 text-sm font-semibold\">\n {running ? <StepMark status=\"running\" /> : <StepMark status={failed ? \"failed\" : \"done\"} />}\n <span className=\"min-w-0\">{heading}</span>\n </p>\n {failed ? <p className=\"mt-1 text-sm text-muted-foreground\">{job.error}</p> : null}\n </div>\n <div className=\"flex shrink-0 gap-2\">\n {failed && onRetry ? (\n <Button size=\"sm\" variant=\"secondary\" onClick={onRetry}>\n Try again\n </Button>\n ) : null}\n {running ? (\n <Button size=\"sm\" variant=\"quiet\" onClick={() => job.cancel?.()}>\n Stop\n </Button>\n ) : (\n <Button size=\"sm\" variant=\"quiet\" onClick={() => job.reset?.()}>\n Close\n </Button>\n )}\n </div>\n </div>\n {visible.length ? (\n <ol className=\"mt-3 grid gap-1.5 text-sm\">\n {hidden > 0 ? <li className=\"text-xs text-muted-foreground\">{hidden} earlier {hidden === 1 ? \"step\" : \"steps\"}</li> : null}\n {visible.map((step, i) => (\n <li key={`${hidden + i}-${step.name}`} className=\"flex items-center gap-2\">\n <StepMark status={step.status} />\n <span className={step.status === \"running\" ? \"text-foreground\" : \"text-muted-foreground\"}>{step.label ?? \"Working\"}</span>\n </li>\n ))}\n </ol>\n ) : null}\n {showText && job.text ? (\n <div\n ref={textRef}\n className=\"mt-3 max-h-32 overflow-y-auto whitespace-pre-wrap border-t border-border pt-3 text-xs leading-relaxed text-muted-foreground\"\n >\n {job.text}\n </div>\n ) : null}\n </section>\n );\n}\n\nconst isEmpty = (node) => node == null || node === false || node === \"\" || (Array.isArray(node) && node.filter(Boolean).length === 0);\n\n// The generator: a form (prompt, options, Generate), the running job's\n// panel under it, and the results. Stacked, or side by side from lg up.\nfunction Generator({\n job,\n prompt: controlled,\n defaultPrompt = \"\",\n onPromptChange,\n onGenerate,\n heading,\n description,\n promptLabel = \"What should it make?\",\n placeholder = \"Describe what you want…\",\n promptHint,\n promptRows = 4,\n options,\n generateLabel = \"Generate\",\n generateIcon = \"wand\",\n canGenerateEmpty = false,\n disabled = false,\n aside,\n results,\n resultsTitle = \"Results\",\n resultsActions,\n resultsColumns = 1,\n placeholderResult,\n emptyTitle = \"Nothing made yet\",\n emptyBody = \"Describe what you want and press Generate. What it makes lands here.\",\n emptyState,\n layout = \"stacked\",\n title = \"Working on it\",\n doneTitle = \"Done\",\n failedTitle = \"It stopped\",\n onRetry,\n autoClose = 6000,\n maxSteps = 6,\n showText = true,\n className,\n}) {\n const id = useId();\n const [own, setOwn] = useState(defaultPrompt);\n const [error, setError] = useState(null);\n const [starting, setStarting] = useState(false);\n const lastPrompt = useRef(\"\");\n const value = controlled ?? own;\n const setValue = (v) => {\n if (controlled === undefined) setOwn(v);\n onPromptChange?.(v);\n };\n const running = Boolean(job?.running);\n const canGenerate = !disabled && !running && !starting && (canGenerateEmpty || value.trim().length > 0);\n\n async function generate(text = value) {\n if (disabled || running || starting || (!canGenerateEmpty && !text.trim())) return;\n lastPrompt.current = text.trim();\n setError(null);\n setStarting(true);\n let r;\n try {\n r = onGenerate ? await onGenerate(text.trim()) : await job?.start?.(text.trim());\n } catch (e) {\n r = { ok: false, error: e?.message ?? String(e) };\n }\n setStarting(false);\n if (r && r.ok === false && !r.aborted) setError(r.error ?? \"It did not start\");\n }\n // useJob's start resolves when the job ends; a failure it reports is\n // already the panel's error, so the field says only what the panel does not.\n const fieldError = error && !job?.error ? error : null;\n\n const cols = { 1: \"grid-cols-1\", 2: \"grid-cols-1 sm:grid-cols-2\", 3: \"grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\", 4: \"grid-cols-2 lg:grid-cols-4\" };\n const empty = isEmpty(results);\n const split = layout === \"split\";\n\n const form = (\n <Card title={heading} description={description} className=\"min-w-0\">\n <div className=\"grid gap-4\">\n <Field id={id} label={promptLabel} hint={promptHint} error={fieldError ?? undefined}>\n <Textarea\n id={id}\n rows={promptRows}\n value={value}\n placeholder={placeholder}\n disabled={disabled}\n invalid={Boolean(fieldError)}\n aria-describedby={fieldError ? `${id}-error` : promptHint ? `${id}-hint` : undefined}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={(e) => {\n // Cmd/Ctrl+Enter generates; Enter alone is a new line.\n if (e.key === \"Enter\" && (e.metaKey || e.ctrlKey) && !e.nativeEvent.isComposing) {\n e.preventDefault();\n generate();\n }\n }}\n />\n </Field>\n {options ? <div className=\"grid gap-4 sm:grid-cols-2\">{options}</div> : null}\n <div className=\"flex flex-wrap items-center gap-3\">\n <Button variant=\"primary\" icon={running || starting ? undefined : generateIcon} loading={starting} disabled={!canGenerate} onClick={() => generate()}>\n {running ? \"Working…\" : generateLabel}\n </Button>\n <p aria-hidden=\"true\" className=\"hidden text-xs text-muted-foreground md:block\">\n ⌘/Ctrl + Enter\n </p>\n </div>\n <Panel\n job={job}\n title={title}\n doneTitle={doneTitle}\n failedTitle={failedTitle}\n onRetry={onRetry ?? (lastPrompt.current || canGenerateEmpty ? () => generate(lastPrompt.current) : undefined)}\n autoClose={autoClose}\n maxSteps={maxSteps}\n showText={showText}\n />\n </div>\n </Card>\n );\n\n const shelf = (\n <section aria-label={resultsTitle} aria-busy={running || undefined} className=\"grid min-w-0 content-start gap-3\">\n <div className=\"flex min-h-9 items-center justify-between gap-3\">\n <h3 className=\"text-sm font-semibold\">{resultsTitle}</h3>\n {resultsActions ? <div className=\"flex shrink-0 items-center gap-2\">{resultsActions}</div> : null}\n </div>\n {running ? (\n <div className={cx(\"grid gap-3\", cols[resultsColumns] ?? cols[1])}>\n {placeholderResult ?? <Skeleton shape=\"rect\" className=\"h-32 w-full\" />}\n </div>\n ) : null}\n {empty ? (\n running ? null : emptyState ?? <Empty icon=\"sparkles\" title={emptyTitle} body={emptyBody} className=\"rounded-lg border border-dashed border-border\" />\n ) : (\n <div className={cx(\"grid gap-3\", cols[resultsColumns] ?? cols[1])}>{results}</div>\n )}\n </section>\n );\n\n return (\n <div className={cx(\"grid gap-4\", split && \"lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)] lg:items-start\", className)}>\n <div className=\"grid min-w-0 gap-4\">\n {form}\n {aside}\n </div>\n {shelf}\n </div>\n );\n}\n"
23
+ },
24
+ {
25
+ "path": "registry/blocks/job-panel/README.md",
26
+ "type": "registry:file",
27
+ "target": "@components/makersclaw/blocks/job-panel/README.md",
28
+ "content": "# JobPanel\n\nThe panel that follows work the founder asked the app's agent for. It\nshows each step's label as the agent works, a Stop button while it runs,\nthen \"done\" (closing itself a few seconds later) or the reason it\nstopped. When no job runs it renders nothing — a panel that is always\nthere is a monitoring console, and apps do not have one.\n\n```jsx\nimport { JobPanel } from \"../components/blocks/job-panel\"; // a Makersclaw app (\"./blocks/job-panel\" from components/)\nimport { JobPanel } from \"@/components/makersclaw/blocks/job-panel\"; // a shadcn project\n```\n\nA client component. It takes the object `useJob()` returns (lib/hooks.js,\nREADME §5 \"Commissioning work\") and draws it; starting the job stays with\nyour surface.\n\n`variant=\"generator\"` is the panel inside a small studio: a prompt, your\noptions, Generate, the running job, and a shelf of what it made. It holds\nno model, provider or API key: Generate hands the prompt to the app's\nagent (`job.start`, or your `onGenerate`), the agent writes rows, and you\npass those rows back in as `results`.\n\n## Props\n\n- `job` — what `useJob()` returns. Required. Outside Makersclaw, any\n object of the same shape: `running`, `steps` (`[{ name, label, status:\n \"running\" | \"done\" | \"failed\" }]`), `text`, `error`, `finishedAt`, and\n `cancel()` / `reset()`.\n- `title` — while it runs, what the agent is doing, in the founder's\n words: `\"Drafting the launch plan\"`. Default `\"Working on it\"`.\n- `doneTitle` — after a clean finish: `\"Launch plan drafted\"`.\n- `failedTitle` — when it stopped with an error; the error follows it.\n- `onRetry` — shows \"Try again\" beside Close when the job failed: start\n the same job again.\n- `autoClose` — milliseconds after a clean finish before the panel\n closes itself (`job.reset()`); default 6000, `false` to keep it until\n Close.\n- `maxSteps` — the newest steps shown, default 6; older ones become one\n \"N earlier steps\" line.\n- `showText` — the agent's own account under the steps, default true. It\n is what the agent says it did, never the result: the result is rows.\n- `className` — layout around it. Never colour.\n\n### The generator (`variant=\"generator\"`)\n\nEverything above (`job`, `title`, `doneTitle`, `failedTitle`, `onRetry`,\n`autoClose`, `maxSteps`, `showText`) goes to the panel inside it. Then:\n\n- `onGenerate(prompt)` — start the job. Build the ask from the prompt and\n your options and call `job.start(ask)`; return what it returns, or\n `{ ok: false, error }` and the reason shows under the prompt. Default:\n `job.start(prompt)`. Cmd/Ctrl+Enter in the prompt presses Generate.\n Without `onRetry`, the panel's \"Try again\" re-runs the last prompt.\n- `prompt` / `onPromptChange(text)` — control the prompt; or\n `defaultPrompt`. `promptLabel` (\"What should it make?\"), `placeholder`,\n `promptHint`, `promptRows` (4).\n- `options` — your fields, in a two-column grid under the prompt (one\n column on a phone): a size `Select`, a style `ToggleGroup`, a `Slider`.\n Their values are yours; read them in `onGenerate`.\n- `generateLabel` (\"Generate\") · `generateIcon` (\"wand\") ·\n `canGenerateEmpty` (false) · `disabled` (false).\n- `heading` · `description` — the form card's title and line under it.\n- `aside` — a node under the form: tips, a note on cost.\n- `results` — what the job made, as nodes (an array of cards, say). The\n shelf draws them in `resultsColumns` columns (1–4; fewer on a phone).\n `resultsTitle` (\"Results\") and `resultsActions` (nodes beside it).\n- `placeholderResult` — drawn at the top of the shelf while the job runs\n (default a skeleton); the shelf is `aria-busy` meanwhile.\n- `emptyTitle` · `emptyBody` · `emptyState` — the shelf before anything\n is made.\n- `layout` — `\"stacked\"` (default: the form, then the shelf) or\n `\"split\"` (side by side from lg up; stacked on a phone).\n\n## Example\n\n```jsx\n\"use client\";\nimport { useEffect } from \"react\";\nimport { useJob } from \"../lib/hooks\";\nimport { Button } from \"./ui\";\nimport { JobPanel } from \"./blocks/job-panel\";\n\nconst ASK = \"Draft a launch plan for the next product in the list, with dates and owners.\";\n\nexport default function Launches({ launches, reload }) {\n const job = useJob();\n useEffect(() => { reload?.(); }, [job.progress]); // rows appear as they land\n\n return (\n <div className=\"grid gap-4\">\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"secondary\" onClick={() => job.start(ASK)} disabled={job.running}>Generate a launch plan</Button>\n <Button variant=\"primary\" icon=\"plus\" onClick={openNewLaunch}>New launch plan</Button>\n </div>\n <JobPanel job={job} title=\"Drafting the launch plan\" doneTitle=\"Launch plan drafted\" onRetry={() => job.start(ASK)} />\n {/* the launches list — the job's result lands here */}\n </div>\n );\n}\n```\n\nThe verb sits on the object list beside the manual \"New\" (README §5, rule\n1), and the panel sits where the result will appear.\n\nA generator for product photos:\n\n```jsx\n\"use client\";\nimport { useEffect, useState } from \"react\";\nimport { useJob } from \"../lib/hooks\";\nimport { Field, Image, Select } from \"./ui\";\nimport { JobPanel } from \"./blocks/job-panel\";\n\nexport default function PhotoStudio({ photos, reload }) {\n const job = useJob();\n const [size, setSize] = useState(\"square\");\n useEffect(() => { reload?.(); }, [job.progress]); // new photos appear as they land\n\n return (\n <JobPanel\n variant=\"generator\"\n job={job}\n heading=\"New product photo\"\n promptLabel=\"Describe the photo\"\n options={\n <Field id=\"size\" label=\"Size\">\n <Select id=\"size\" value={size} onChange={(e) => setSize(e.target.value)}\n options={[{ value: \"square\", label: \"Square\" }, { value: \"portrait\", label: \"Portrait\" }]} />\n </Field>\n }\n onGenerate={(prompt) => job.start(`Make one ${size} product photo: ${prompt}. Save it to public.photos.`)}\n title=\"Making the photo\"\n doneTitle=\"Photo saved\"\n results={photos.map((p) => <Image key={p.id} src={p.url} alt={p.prompt} ratio=\"square\" rounded=\"lg\" />)}\n resultsColumns={3}\n />\n );\n}\n```\n\nWhat it will not do: call a model, hold a provider or an API key, or\nkeep what was made. The agent does the work through the job; the rows\nare the results.\n\nIts files are fixed: `npm run check` refuses a change, and `makersclaw add\njob-panel` restores them. A different panel is your own component in\n`components/`, built from the kit.\n\n## Credits\n\nThe generator variant is ported from Creative Tim UI (https://github.com/creativetimofficial/ui, ai-image-generator-01 and ai-video-generator-01, MIT), rewritten on the Makersclaw kit without its provider and API-key fields.\n"
29
+ }
30
+ ],
31
+ "categories": [
32
+ "job",
33
+ "agent",
34
+ "generate",
35
+ "progress",
36
+ "steps",
37
+ "running",
38
+ "working",
39
+ "commission",
40
+ "ai",
41
+ "stop",
42
+ "status",
43
+ "generator",
44
+ "prompt",
45
+ "results",
46
+ "image",
47
+ "studio"
48
+ ],
49
+ "docs": "Installed to components/makersclaw/blocks/job-panel. Import: import { JobPanel } from \"@/components/makersclaw/blocks/job-panel\". Props and an example: components/makersclaw/blocks/job-panel/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
50
+ "meta": {
51
+ "makersclaw": {
52
+ "version": "0.3.0",
53
+ "useWhen": "A surface has a verb that asks the agent to make or find something (\"Generate a launch plan\", \"Find 20 leads\", \"Plan next sprint\") and the founder should watch it happen where the result will land. variant=\"generator\" when the screen IS the place to ask: a prompt and a few options, the results under them (product photos, ad copy, names)."
54
+ }
55
+ }
56
+ }