makersclaw 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (401) hide show
  1. package/README.md +9 -3
  2. package/package.json +1 -1
  3. package/r/activity-log.json +52 -0
  4. package/r/agent-changes.json +51 -0
  5. package/r/agent-chat.json +53 -0
  6. package/r/agent-column.json +85 -0
  7. package/r/agent-dock.json +57 -0
  8. package/r/agent-kit.json +49 -0
  9. package/r/agent-marks.json +49 -0
  10. package/r/agent-pane.json +93 -0
  11. package/r/ai-action.json +56 -0
  12. package/r/annotate.json +3 -3
  13. package/r/approval-request.json +2 -2
  14. package/r/approval.json +54 -0
  15. package/r/ask-bar.json +50 -0
  16. package/r/audio-player.json +2 -2
  17. package/r/auth-form.json +2 -2
  18. package/r/automation-editor.json +66 -0
  19. package/r/automation-list.json +57 -0
  20. package/r/automation-model.json +118 -0
  21. package/r/automations-screen.json +71 -0
  22. package/r/board.json +2 -2
  23. package/r/calendar-view.json +3 -3
  24. package/r/cart.json +2 -2
  25. package/r/chart.json +2 -2
  26. package/r/chat-pane.json +84 -0
  27. package/r/chat.json +2 -2
  28. package/r/code-editor.json +2 -2
  29. package/r/comment-box.json +3 -3
  30. package/r/connect-card.json +51 -0
  31. package/r/contact-section.json +2 -2
  32. package/r/core.json +13 -7
  33. package/r/cost.json +51 -0
  34. package/r/csv-import.json +3 -3
  35. package/r/data-picker.json +60 -0
  36. package/r/data-table.json +2 -2
  37. package/r/diagram.json +2 -2
  38. package/r/doc-editor.json +2 -2
  39. package/r/doc-page.json +3 -3
  40. package/r/email-render.json +2 -2
  41. package/r/faq.json +2 -2
  42. package/r/file-upload.json +2 -2
  43. package/r/first-run.json +63 -0
  44. package/r/fix-note.json +57 -0
  45. package/r/flow-canvas.json +2 -2
  46. package/r/flow-editor.json +79 -0
  47. package/r/footer.json +2 -2
  48. package/r/gantt.json +3 -3
  49. package/r/job-panel.json +2 -2
  50. package/r/json-editor.json +3 -3
  51. package/r/library-shelf.json +56 -0
  52. package/r/map.json +2 -2
  53. package/r/margin-notes.json +67 -0
  54. package/r/menubar.json +2 -2
  55. package/r/mind-map.json +3 -3
  56. package/r/navigation-menu.json +2 -2
  57. package/r/needs-you.json +55 -0
  58. package/r/order-detail.json +2 -2
  59. package/r/org-chart.json +3 -3
  60. package/r/pace-bar.json +69 -0
  61. package/r/page-builder.json +2 -2
  62. package/r/payment-methods.json +2 -2
  63. package/r/pdf-document.json +2 -2
  64. package/r/pdf-viewer.json +2 -2
  65. package/r/product-detail.json +2 -2
  66. package/r/product-grid.json +3 -3
  67. package/r/record-form.json +2 -2
  68. package/r/record-grid.json +52 -0
  69. package/r/record-notes.json +4 -4
  70. package/r/registry.json +4086 -1512
  71. package/r/run-history.json +52 -0
  72. package/r/run-view.json +80 -0
  73. package/r/schedule-card.json +54 -0
  74. package/r/schedule-engine.json +79 -0
  75. package/r/schedule-gantt.json +83 -0
  76. package/r/schedule-ruler.json +64 -0
  77. package/r/search-dialog.json +2 -2
  78. package/r/settings-list.json +2 -2
  79. package/r/setup-checklist.json +2 -2
  80. package/r/share-dialog.json +2 -2
  81. package/r/sidebar.json +2 -2
  82. package/r/signature-pad.json +2 -2
  83. package/r/sketch-canvas.json +2 -2
  84. package/r/source-receipt.json +52 -0
  85. package/r/spreadsheet.json +3 -3
  86. package/r/stat-cards.json +3 -3
  87. package/r/step-picker.json +53 -0
  88. package/r/step-settings.json +72 -0
  89. package/r/suggestion.json +58 -0
  90. package/r/sum-bar.json +64 -0
  91. package/r/task-list.json +3 -3
  92. package/r/task-model.json +2 -2
  93. package/r/testimonials.json +2 -2
  94. package/r/tier-map.json +67 -0
  95. package/r/time-grid.json +73 -0
  96. package/r/time-strip.json +64 -0
  97. package/r/top-bar.json +66 -0
  98. package/r/transaction-list.json +2 -2
  99. package/r/trigger-picker.json +61 -0
  100. package/r/whiteboard.json +3 -3
  101. package/r/workflow-view.json +55 -0
  102. package/registry/blocks/activity-log/README.md +112 -0
  103. package/registry/blocks/activity-log/activity-log.jsx +351 -0
  104. package/registry/blocks/activity-log/block.json +33 -0
  105. package/registry/blocks/activity-log/demo.jsx +48 -0
  106. package/registry/blocks/activity-log/index.js +4 -0
  107. package/registry/blocks/agent-changes/README.md +94 -0
  108. package/registry/blocks/agent-changes/agent-changes.jsx +118 -0
  109. package/registry/blocks/agent-changes/block.json +28 -0
  110. package/registry/blocks/agent-changes/demo.jsx +39 -0
  111. package/registry/blocks/agent-changes/index.js +3 -0
  112. package/registry/blocks/agent-chat/README.md +145 -0
  113. package/registry/blocks/agent-chat/agent-chat.jsx +415 -0
  114. package/registry/blocks/agent-chat/block.json +30 -0
  115. package/registry/blocks/agent-chat/demo.jsx +165 -0
  116. package/registry/blocks/agent-chat/index.js +3 -0
  117. package/registry/blocks/agent-column/README.md +197 -0
  118. package/registry/blocks/agent-column/agent-column.jsx +492 -0
  119. package/registry/blocks/agent-column/block.json +67 -0
  120. package/registry/blocks/agent-column/column-picker.jsx +163 -0
  121. package/registry/blocks/agent-column/demo.jsx +50 -0
  122. package/registry/blocks/agent-column/index.js +7 -0
  123. package/registry/blocks/agent-column/model.js +164 -0
  124. package/registry/blocks/agent-column/receipt.jsx +483 -0
  125. package/registry/blocks/agent-column/spreadsheet-type.jsx +24 -0
  126. package/registry/blocks/agent-dock/README.md +210 -0
  127. package/registry/blocks/agent-dock/agent-dock.jsx +356 -0
  128. package/registry/blocks/agent-dock/block.json +47 -0
  129. package/registry/blocks/agent-dock/demo.jsx +153 -0
  130. package/registry/blocks/agent-dock/dock-model.js +118 -0
  131. package/registry/blocks/agent-dock/index.js +5 -0
  132. package/registry/blocks/agent-kit/README.md +73 -0
  133. package/registry/blocks/agent-kit/agent-kit.jsx +86 -0
  134. package/registry/blocks/agent-kit/block.json +10 -0
  135. package/registry/blocks/agent-kit/demo.jsx +35 -0
  136. package/registry/blocks/agent-kit/index.js +3 -0
  137. package/registry/blocks/agent-marks/README.md +80 -0
  138. package/registry/blocks/agent-marks/agent-marks.jsx +133 -0
  139. package/registry/blocks/agent-marks/block.json +27 -0
  140. package/registry/blocks/agent-marks/demo.jsx +49 -0
  141. package/registry/blocks/agent-marks/index.js +3 -0
  142. package/registry/blocks/agent-pane/README.md +225 -0
  143. package/registry/blocks/agent-pane/agent-pane.jsx +244 -0
  144. package/registry/blocks/agent-pane/block.json +41 -0
  145. package/registry/blocks/agent-pane/composer.jsx +6 -0
  146. package/registry/blocks/agent-pane/demo.jsx +233 -0
  147. package/registry/blocks/agent-pane/dock-context.js +10 -0
  148. package/registry/blocks/agent-pane/index.js +4 -0
  149. package/registry/blocks/agent-pane/prose.jsx +84 -0
  150. package/registry/blocks/agent-pane/questions.jsx +134 -0
  151. package/registry/blocks/agent-pane/transcript.jsx +268 -0
  152. package/registry/blocks/agent-pane/work.jsx +368 -0
  153. package/registry/blocks/ai-action/README.md +94 -0
  154. package/registry/blocks/ai-action/ai-action.jsx +188 -0
  155. package/registry/blocks/ai-action/block.json +34 -0
  156. package/registry/blocks/ai-action/demo.jsx +93 -0
  157. package/registry/blocks/ai-action/index.js +3 -0
  158. package/registry/blocks/approval/README.md +145 -0
  159. package/registry/blocks/approval/approval.jsx +163 -0
  160. package/registry/blocks/approval/block.json +31 -0
  161. package/registry/blocks/approval/demo.jsx +121 -0
  162. package/registry/blocks/approval/index.js +3 -0
  163. package/registry/blocks/ask-bar/README.md +105 -0
  164. package/registry/blocks/ask-bar/ask-bar.jsx +166 -0
  165. package/registry/blocks/ask-bar/block.json +26 -0
  166. package/registry/blocks/ask-bar/demo.jsx +78 -0
  167. package/registry/blocks/ask-bar/index.js +3 -0
  168. package/registry/blocks/automation-editor/README.md +291 -0
  169. package/registry/blocks/automation-editor/automation-editor.jsx +656 -0
  170. package/registry/blocks/automation-editor/block.json +39 -0
  171. package/registry/blocks/automation-editor/demo.jsx +284 -0
  172. package/registry/blocks/automation-editor/index.js +4 -0
  173. package/registry/blocks/automation-editor/run-inputs.jsx +82 -0
  174. package/registry/blocks/automation-list/README.md +129 -0
  175. package/registry/blocks/automation-list/automation-list.jsx +476 -0
  176. package/registry/blocks/automation-list/block.json +35 -0
  177. package/registry/blocks/automation-list/demo.jsx +67 -0
  178. package/registry/blocks/automation-list/index.js +3 -0
  179. package/registry/blocks/automation-model/README.md +109 -0
  180. package/registry/blocks/automation-model/block.json +135 -0
  181. package/registry/blocks/automation-model/catalog.js +256 -0
  182. package/registry/blocks/automation-model/check.js +161 -0
  183. package/registry/blocks/automation-model/demo.jsx +42 -0
  184. package/registry/blocks/automation-model/edit.js +262 -0
  185. package/registry/blocks/automation-model/fixes.js +287 -0
  186. package/registry/blocks/automation-model/glyph.jsx +13 -0
  187. package/registry/blocks/automation-model/graph.js +410 -0
  188. package/registry/blocks/automation-model/index.js +33 -0
  189. package/registry/blocks/automation-model/overlay.js +175 -0
  190. package/registry/blocks/automation-model/parts.jsx +94 -0
  191. package/registry/blocks/automation-model/schedule.js +53 -0
  192. package/registry/blocks/automation-model/templates.js +128 -0
  193. package/registry/blocks/automation-model/values.js +29 -0
  194. package/registry/blocks/automation-model/words.js +439 -0
  195. package/registry/blocks/automations-screen/README.md +207 -0
  196. package/registry/blocks/automations-screen/automations-screen.jsx +452 -0
  197. package/registry/blocks/automations-screen/block.json +47 -0
  198. package/registry/blocks/automations-screen/demo.jsx +364 -0
  199. package/registry/blocks/automations-screen/index.js +4 -0
  200. package/registry/blocks/automations-screen/screen-model.js +129 -0
  201. package/registry/blocks/chat-pane/README.md +143 -0
  202. package/registry/blocks/chat-pane/block.json +48 -0
  203. package/registry/blocks/chat-pane/chat-attach.js +166 -0
  204. package/registry/blocks/chat-pane/chat-composer.jsx +195 -0
  205. package/registry/blocks/chat-pane/chat-context.js +17 -0
  206. package/registry/blocks/chat-pane/chat-model.js +43 -0
  207. package/registry/blocks/chat-pane/chat-pane.jsx +398 -0
  208. package/registry/blocks/chat-pane/chats-list.jsx +52 -0
  209. package/registry/blocks/chat-pane/demo.jsx +103 -0
  210. package/registry/blocks/chat-pane/index.js +9 -0
  211. package/registry/blocks/connect-card/README.md +99 -0
  212. package/registry/blocks/connect-card/block.json +10 -0
  213. package/registry/blocks/connect-card/connect-card.jsx +103 -0
  214. package/registry/blocks/connect-card/demo.jsx +49 -0
  215. package/registry/blocks/connect-card/index.js +3 -0
  216. package/registry/blocks/cost/README.md +115 -0
  217. package/registry/blocks/cost/block.json +10 -0
  218. package/registry/blocks/cost/cost.jsx +176 -0
  219. package/registry/blocks/cost/demo.jsx +66 -0
  220. package/registry/blocks/cost/index.js +3 -0
  221. package/registry/blocks/data-picker/README.md +81 -0
  222. package/registry/blocks/data-picker/block.json +38 -0
  223. package/registry/blocks/data-picker/data-picker.jsx +210 -0
  224. package/registry/blocks/data-picker/demo.jsx +34 -0
  225. package/registry/blocks/data-picker/fields.js +89 -0
  226. package/registry/blocks/data-picker/index.js +4 -0
  227. package/registry/blocks/data-table/demo.jsx +1 -1
  228. package/registry/blocks/first-run/README.md +118 -0
  229. package/registry/blocks/first-run/block.json +11 -0
  230. package/registry/blocks/first-run/demo.jsx +88 -0
  231. package/registry/blocks/first-run/first-run-model.js +51 -0
  232. package/registry/blocks/first-run/first-run.jsx +156 -0
  233. package/registry/blocks/first-run/index.js +5 -0
  234. package/registry/blocks/first-run/probe-rules.js +26 -0
  235. package/registry/blocks/fix-note/README.md +79 -0
  236. package/registry/blocks/fix-note/block.json +35 -0
  237. package/registry/blocks/fix-note/demo.jsx +30 -0
  238. package/registry/blocks/fix-note/fix-note.jsx +235 -0
  239. package/registry/blocks/fix-note/index.js +3 -0
  240. package/registry/blocks/flow-editor/README.md +108 -0
  241. package/registry/blocks/flow-editor/block.json +44 -0
  242. package/registry/blocks/flow-editor/demo.jsx +115 -0
  243. package/registry/blocks/flow-editor/flow-canvas.jsx +568 -0
  244. package/registry/blocks/flow-editor/flow-editor.jsx +154 -0
  245. package/registry/blocks/flow-editor/index.js +6 -0
  246. package/registry/blocks/flow-editor/minimap.jsx +78 -0
  247. package/registry/blocks/flow-editor/outline.jsx +134 -0
  248. package/registry/blocks/library-shelf/README.md +117 -0
  249. package/registry/blocks/library-shelf/block.json +28 -0
  250. package/registry/blocks/library-shelf/demo.jsx +68 -0
  251. package/registry/blocks/library-shelf/index.js +3 -0
  252. package/registry/blocks/library-shelf/library-shelf.jsx +211 -0
  253. package/registry/blocks/library-shelf/probe-rules.js +15 -0
  254. package/registry/blocks/margin-notes/README.md +222 -0
  255. package/registry/blocks/margin-notes/block.json +10 -0
  256. package/registry/blocks/margin-notes/demo.jsx +144 -0
  257. package/registry/blocks/margin-notes/index.js +20 -0
  258. package/registry/blocks/margin-notes/margin-model.js +224 -0
  259. package/registry/blocks/margin-notes/margin-notes.jsx +465 -0
  260. package/registry/blocks/needs-you/README.md +177 -0
  261. package/registry/blocks/needs-you/block.json +34 -0
  262. package/registry/blocks/needs-you/demo.jsx +143 -0
  263. package/registry/blocks/needs-you/index.js +3 -0
  264. package/registry/blocks/needs-you/needs-you.jsx +441 -0
  265. package/registry/blocks/pace-bar/README.md +230 -0
  266. package/registry/blocks/pace-bar/block.json +10 -0
  267. package/registry/blocks/pace-bar/demo.jsx +255 -0
  268. package/registry/blocks/pace-bar/index.js +22 -0
  269. package/registry/blocks/pace-bar/pace-bar.jsx +461 -0
  270. package/registry/blocks/pace-bar/pace-model.js +288 -0
  271. package/registry/blocks/record-grid/README.md +100 -0
  272. package/registry/blocks/record-grid/block.json +28 -0
  273. package/registry/blocks/record-grid/demo.jsx +43 -0
  274. package/registry/blocks/record-grid/index.js +3 -0
  275. package/registry/blocks/record-grid/record-grid.jsx +251 -0
  276. package/registry/blocks/run-history/README.md +138 -0
  277. package/registry/blocks/run-history/block.json +10 -0
  278. package/registry/blocks/run-history/demo.jsx +74 -0
  279. package/registry/blocks/run-history/index.js +3 -0
  280. package/registry/blocks/run-history/run-history.jsx +218 -0
  281. package/registry/blocks/run-view/README.md +174 -0
  282. package/registry/blocks/run-view/block.json +50 -0
  283. package/registry/blocks/run-view/demo.jsx +286 -0
  284. package/registry/blocks/run-view/index.js +5 -0
  285. package/registry/blocks/run-view/run-controls.jsx +266 -0
  286. package/registry/blocks/run-view/run-view.jsx +236 -0
  287. package/registry/blocks/run-view/runs-list.jsx +99 -0
  288. package/registry/blocks/schedule-card/README.md +121 -0
  289. package/registry/blocks/schedule-card/block.json +10 -0
  290. package/registry/blocks/schedule-card/demo.jsx +82 -0
  291. package/registry/blocks/schedule-card/index.js +3 -0
  292. package/registry/blocks/schedule-card/schedule-card.jsx +136 -0
  293. package/registry/blocks/schedule-engine/README.md +137 -0
  294. package/registry/blocks/schedule-engine/block.json +10 -0
  295. package/registry/blocks/schedule-engine/demo.jsx +82 -0
  296. package/registry/blocks/schedule-engine/engine.js +769 -0
  297. package/registry/blocks/schedule-engine/index.js +9 -0
  298. package/registry/blocks/schedule-engine/ops.js +71 -0
  299. package/registry/blocks/schedule-engine/place.js +294 -0
  300. package/registry/blocks/schedule-engine/tree.js +79 -0
  301. package/registry/blocks/schedule-engine/waits.js +80 -0
  302. package/registry/blocks/schedule-gantt/README.md +159 -0
  303. package/registry/blocks/schedule-gantt/block.json +10 -0
  304. package/registry/blocks/schedule-gantt/demo.jsx +223 -0
  305. package/registry/blocks/schedule-gantt/gantt-model.js +219 -0
  306. package/registry/blocks/schedule-gantt/geometry.js +176 -0
  307. package/registry/blocks/schedule-gantt/index.js +26 -0
  308. package/registry/blocks/schedule-gantt/schedule-gantt.jsx +882 -0
  309. package/registry/blocks/schedule-gantt/sheet.jsx +372 -0
  310. package/registry/blocks/schedule-gantt/track.jsx +190 -0
  311. package/registry/blocks/schedule-ruler/README.md +148 -0
  312. package/registry/blocks/schedule-ruler/block.json +10 -0
  313. package/registry/blocks/schedule-ruler/demo.jsx +181 -0
  314. package/registry/blocks/schedule-ruler/index.js +21 -0
  315. package/registry/blocks/schedule-ruler/ruler-model.js +239 -0
  316. package/registry/blocks/schedule-ruler/schedule-ruler.jsx +548 -0
  317. package/registry/blocks/source-receipt/README.md +97 -0
  318. package/registry/blocks/source-receipt/block.json +30 -0
  319. package/registry/blocks/source-receipt/demo.jsx +71 -0
  320. package/registry/blocks/source-receipt/index.js +3 -0
  321. package/registry/blocks/source-receipt/source-receipt.jsx +107 -0
  322. package/registry/blocks/spreadsheet/spreadsheet.jsx +15 -1
  323. package/registry/blocks/step-picker/README.md +78 -0
  324. package/registry/blocks/step-picker/block.json +31 -0
  325. package/registry/blocks/step-picker/demo.jsx +23 -0
  326. package/registry/blocks/step-picker/index.js +3 -0
  327. package/registry/blocks/step-picker/step-picker.jsx +144 -0
  328. package/registry/blocks/step-settings/README.md +100 -0
  329. package/registry/blocks/step-settings/block.json +44 -0
  330. package/registry/blocks/step-settings/demo.jsx +93 -0
  331. package/registry/blocks/step-settings/fields.jsx +58 -0
  332. package/registry/blocks/step-settings/flow-settings.jsx +115 -0
  333. package/registry/blocks/step-settings/index.js +5 -0
  334. package/registry/blocks/step-settings/step-settings.jsx +290 -0
  335. package/registry/blocks/suggestion/README.md +171 -0
  336. package/registry/blocks/suggestion/block.json +40 -0
  337. package/registry/blocks/suggestion/demo.jsx +91 -0
  338. package/registry/blocks/suggestion/index.js +3 -0
  339. package/registry/blocks/suggestion/suggestion.jsx +436 -0
  340. package/registry/blocks/sum-bar/README.md +122 -0
  341. package/registry/blocks/sum-bar/block.json +10 -0
  342. package/registry/blocks/sum-bar/demo.jsx +110 -0
  343. package/registry/blocks/sum-bar/index.js +5 -0
  344. package/registry/blocks/sum-bar/sum-bar.jsx +209 -0
  345. package/registry/blocks/sum-bar/sum-model.js +126 -0
  346. package/registry/blocks/tier-map/README.md +217 -0
  347. package/registry/blocks/tier-map/block.json +10 -0
  348. package/registry/blocks/tier-map/demo.jsx +222 -0
  349. package/registry/blocks/tier-map/index.js +24 -0
  350. package/registry/blocks/tier-map/tier-map.jsx +574 -0
  351. package/registry/blocks/tier-map/tier-model.js +318 -0
  352. package/registry/blocks/time-grid/README.md +305 -0
  353. package/registry/blocks/time-grid/block.json +10 -0
  354. package/registry/blocks/time-grid/demo.jsx +279 -0
  355. package/registry/blocks/time-grid/grid-model.js +322 -0
  356. package/registry/blocks/time-grid/index.js +25 -0
  357. package/registry/blocks/time-grid/time-grid.jsx +1325 -0
  358. package/registry/blocks/time-strip/README.md +141 -0
  359. package/registry/blocks/time-strip/block.json +10 -0
  360. package/registry/blocks/time-strip/demo.jsx +129 -0
  361. package/registry/blocks/time-strip/index.js +19 -0
  362. package/registry/blocks/time-strip/strip-model.js +178 -0
  363. package/registry/blocks/time-strip/time-strip.jsx +260 -0
  364. package/registry/blocks/top-bar/README.md +149 -0
  365. package/registry/blocks/top-bar/block.json +35 -0
  366. package/registry/blocks/top-bar/demo.jsx +78 -0
  367. package/registry/blocks/top-bar/index.js +5 -0
  368. package/registry/blocks/top-bar/probe-rules.js +74 -0
  369. package/registry/blocks/top-bar/top-bar-model.js +152 -0
  370. package/registry/blocks/top-bar/top-bar.jsx +244 -0
  371. package/registry/blocks/trigger-picker/README.md +98 -0
  372. package/registry/blocks/trigger-picker/block.json +38 -0
  373. package/registry/blocks/trigger-picker/demo.jsx +59 -0
  374. package/registry/blocks/trigger-picker/index.js +3 -0
  375. package/registry/blocks/trigger-picker/trigger-picker.jsx +374 -0
  376. package/registry/blocks/workflow-view/README.md +123 -0
  377. package/registry/blocks/workflow-view/block.json +10 -0
  378. package/registry/blocks/workflow-view/demo.jsx +90 -0
  379. package/registry/blocks/workflow-view/index.js +3 -0
  380. package/registry/blocks/workflow-view/workflow-view.jsx +203 -0
  381. package/src/install.mjs +77 -16
  382. package/src/registry.mjs +3 -0
  383. package/templates/next/browser/lib/data.js +6 -1
  384. package/templates/next/files/README.md +627 -24
  385. package/templates/next/files/check/automation-format.ts +2103 -0
  386. package/templates/next/files/check/automation-package.ts +142 -0
  387. package/templates/next/files/check/run.mjs +968 -9
  388. package/templates/next/files/components/ui/field.jsx +17 -4
  389. package/templates/next/files/components/ui/icon.jsx +13 -0
  390. package/templates/next/files/components/ui/index.js +2 -1
  391. package/templates/next/files/components/ui/layout.jsx +77 -2
  392. package/templates/next/files/components/ui/notice.jsx +70 -0
  393. package/templates/next/files/components/ui/page.jsx +124 -0
  394. package/templates/next/files/components/ui/stat.jsx +22 -7
  395. package/templates/next/files/components/ui/toast.jsx +6 -3
  396. package/templates/next/files/lib/automations.js +660 -0
  397. package/templates/next/files/lib/claw.js +241 -5
  398. package/templates/next/files/lib/door.js +92 -0
  399. package/templates/next/files/lib/hooks.js +1627 -8
  400. package/templates/next/files/lib/platform.js +529 -21
  401. package/templates/next/template.json +13 -3
@@ -0,0 +1,64 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "time-strip",
4
+ "type": "registry:block",
5
+ "title": "TimeStrip",
6
+ "description": "One bar for a span that has a length (a call, a recording, a shift), from 0 to its end, with marks at the times that matter on it. Each mark is a thin coloured post at its time (a category colour 1 to 5, or a status tone); a pressed mark is taller and ringed, and arrow keys walk from one mark to the next. Stretches somebody was away are hatched; a part can be dragged out on the bar and its two ends nudged with the arrow keys (a slider each), and the bar says it back in words. An axis under it names the start, the end and the whole steps between (00:00, 15:00, 30:00, 58:40), or the app's own labels (\"before the client joined\"). A small bar (22 px) sits under a record's title; a large one (44 px) is the screen's signature. Positions are percentages of the length, so it draws the same on the server. Loading and failed are drawn; the sums (toSeconds, clockOf, marksOf, rangeOf, ticksFor, nextMark) are exported as plain functions.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json"
9
+ ],
10
+ "dependencies": [],
11
+ "files": [
12
+ {
13
+ "path": "registry/blocks/time-strip/index.js",
14
+ "type": "registry:component",
15
+ "target": "@components/makersclaw/blocks/time-strip/index.js",
16
+ "content": "// The TimeStrip block — `makersclaw add time-strip`. Fixed: compose it\n// through its props (README.md beside this file). The model's helpers are\n// plain functions; they work on the server too.\nexport { TimeStrip } from \"./time-strip\";\nexport {\n clockOf,\n countMarks,\n lengthOf,\n marksOf,\n nextMark,\n pctOf,\n rangeOf,\n rangesOf,\n secondsAt,\n shareOf,\n stripWords,\n ticksFor,\n toSeconds,\n} from \"./strip-model\";\n"
17
+ },
18
+ {
19
+ "path": "registry/blocks/time-strip/time-strip.jsx",
20
+ "type": "registry:component",
21
+ "target": "@components/makersclaw/blocks/time-strip/time-strip.jsx",
22
+ "content": "\"use client\";\n\nimport { useRef, useState } from \"react\";\nimport { Button, Notice, Skeleton, cx } from \"../../ui\";\nimport { clockOf, countMarks, lengthOf, marksOf, nextMark, pctOf, rangeOf, rangesOf, secondsAt, shareOf, stripWords, ticksFor, toSeconds } from \"./strip-model\";\n\n// One bar for a span that has a length (a call, a recording, a shift), with\n// marks at the times that matter on it. The marks are the app's: a cut, a\n// comment, a decision. The bar can show the stretch somebody was away\n// (hatched), and can let a person drag out a part of it.\n//\n// Props in, callbacks out (`onMark`, `onSelect`): the block never reads or\n// writes data. Positions are percentages of the length, so the server draws\n// what the browser draws. README.md beside this file lists every prop.\n\n// Literal class names, so Tailwind sees every one of them.\nconst COLOR = {\n neutral: \"bg-muted-foreground\",\n primary: \"bg-primary\",\n info: \"bg-info\",\n success: \"bg-success\",\n warning: \"bg-warning\",\n danger: \"bg-destructive\",\n};\nconst HUE = { 1: \"bg-chart-1\", 2: \"bg-chart-2\", 3: \"bg-chart-3\", 4: \"bg-chart-4\", 5: \"bg-chart-5\" };\n\nconst SIZE = {\n sm: { track: \"h-[22px]\", mark: \"inset-y-[3px] w-[5px]\", on: \"inset-y-0 w-[7px]\", handle: \"inset-y-0 w-2\" },\n lg: { track: \"h-11\", mark: \"inset-y-1.5 w-1.5\", on: \"inset-y-0.5 w-2\", handle: \"inset-y-0 w-2.5\" },\n};\n\nconst HATCH = { backgroundImage: \"repeating-linear-gradient(135deg, currentColor 0 1px, transparent 1px 5px)\" };\n\nconst colorOf = (m) => (m.hue && HUE[m.hue] ? HUE[m.hue] : COLOR[m.tone] ?? \"bg-foreground\");\n\n/**\n * The strip.\n *\n * Props: `length` (seconds, or \"mm:ss\"), `marks` `[{ id, at, hue?, tone?,\n * label? }]`, `away` `[[from, to]]`, `selection` `{ from, to }`,\n * `onSelect(range | null)`, `selected` (a mark id), `onMark(mark)`, `size`\n * (sm | lg), `axis` (true, false, or `[{ at, label? }]`), `step`, `noun`,\n * `label`, `status`, `error`, `onRetry`, `className`.\n */\nexport function TimeStrip({\n length,\n marks,\n away,\n selection,\n onSelect,\n selected,\n onMark,\n size = \"sm\",\n axis = true,\n ticks = 5,\n step = 1,\n noun = \"mark\",\n label = \"Timeline\",\n status = \"ready\",\n error,\n onRetry,\n className,\n}) {\n const track = useRef(null);\n const [drag, setDrag] = useState(null);\n const total = lengthOf(length);\n const look = SIZE[size] ?? SIZE.sm;\n const placed = marksOf(marks, total);\n const gaps = rangesOf(away, total);\n const shown = drag ?? (selection ? rangeOf(selection.from, selection.to, total) : null);\n const canPick = Boolean(onSelect) && total > 0;\n\n if (status === \"loading\") {\n return (\n <div role=\"status\" aria-label={`${label}, loading`} className={cx(\"grid gap-1\", className)}>\n <Skeleton className={cx(\"w-full\", look.track)} />\n {axis ? <Skeleton className=\"h-3 w-full\" /> : null}\n </div>\n );\n }\n if (status === \"failed\") {\n return (\n <Notice\n tone=\"danger\"\n className={className}\n action={\n onRetry ? (\n <Button size=\"sm\" onClick={onRetry}>\n Try again\n </Button>\n ) : undefined\n }\n >\n {error || \"The timeline could not be read.\"}\n </Notice>\n );\n }\n\n // The bar's own ticks, or the app's.\n const labels = (() => {\n if (axis === false || total <= 0) return [];\n if (Array.isArray(axis)) {\n const list = axis\n .map((t) => ({ ...t, seconds: toSeconds(t?.at) }))\n .filter((t) => t.seconds != null)\n .sort((a, b) => a.seconds - b.seconds);\n return list.map((t, i) => ({ at: t.seconds, label: t.label ?? clockOf(t.seconds), pct: pctOf(shareOf(t.seconds, total)), edge: i === 0 && t.seconds <= 0 ? \"start\" : i === list.length - 1 && t.seconds >= total ? \"end\" : null }));\n }\n return ticksFor(total, ticks);\n })();\n\n function secondsFromPointer(e) {\n const box = track.current?.getBoundingClientRect();\n if (!box || !box.width) return 0;\n return secondsAt((e.clientX - box.left) / box.width, total, step);\n }\n\n function down(e) {\n if (!canPick || e.button !== 0) return;\n const at = secondsFromPointer(e);\n e.currentTarget.setPointerCapture?.(e.pointerId);\n setDrag({ anchor: at, from: at, to: at, fromPct: 0, widthPct: 0, moving: false });\n }\n function move(e) {\n if (!drag) return;\n const at = secondsFromPointer(e);\n const r = rangeOf(drag.anchor, at, total);\n setDrag({ ...drag, moving: true, ...(r ?? { from: at, to: at, fromPct: pctOf(at / total), widthPct: 0 }) });\n }\n function up(e) {\n if (!drag) return;\n const at = secondsFromPointer(e);\n const r = rangeOf(drag.anchor, at, total, { min: Math.max(1, Number(step) || 1) - 1 });\n setDrag(null);\n e.currentTarget.releasePointerCapture?.(e.pointerId);\n // A press that went nowhere clears the part; a drag picks one.\n onSelect(r ? { from: r.from, to: r.to } : null);\n }\n\n function nudge(which, e) {\n if (!shown) return;\n const unit = Math.max(1, Number(step) || 1) * (e.shiftKey ? 10 : 1);\n const keys = { ArrowLeft: -unit, ArrowDown: -unit, ArrowRight: unit, ArrowUp: unit };\n let from = shown.from;\n let to = shown.to;\n const move = which === \"from\" ? \"from\" : \"to\";\n let value = move === \"from\" ? from : to;\n if (e.key in keys) value += keys[e.key];\n else if (e.key === \"Home\") value = 0;\n else if (e.key === \"End\") value = total;\n else if (e.key === \"Escape\") {\n e.preventDefault();\n onSelect(null);\n return;\n } else return;\n e.preventDefault();\n if (move === \"from\") from = value;\n else to = value;\n const r = rangeOf(from, to, total, { min: 0 });\n if (r) onSelect({ from: r.from, to: r.to });\n }\n\n const words = stripWords({ length: total, marks: placed, noun });\n const kinds = countMarks(placed);\n\n return (\n <div role=\"group\" aria-label={label} className={cx(\"relative grid min-w-0 gap-1\", className)}>\n <div\n ref={track}\n className={cx(\"relative overflow-hidden rounded-[4px] border border-border bg-muted\", look.track, canPick && \"cursor-crosshair touch-none select-none\")}\n onPointerDown={down}\n onPointerMove={move}\n onPointerUp={up}\n onPointerCancel={() => setDrag(null)}\n >\n {gaps.map((g, i) => (\n <span key={`away-${i}`} aria-hidden=\"true\" title={g.label} className=\"absolute inset-y-0 left-(--at) w-(--width) text-input\" style={{ ...HATCH, \"--at\": `${g.fromPct}%`, \"--width\": `${g.widthPct}%` }} />\n ))}\n {shown && shown.widthPct > 0 ? (\n <span aria-hidden=\"true\" className=\"absolute inset-y-0 left-(--at) w-(--width) rounded-[4px] border-2 border-primary bg-primary/10\" style={{ \"--at\": `${shown.fromPct}%`, \"--width\": `${shown.widthPct}%` }} />\n ) : null}\n {placed.map((m) => {\n const on = selected != null && m.id === selected;\n const name = `${m.label ? `${m.label}, ` : \"\"}${noun} at ${clockOf(m.seconds)}`;\n const className = cx(\n \"absolute left-(--at) -translate-x-1/2 rounded-[2px] outline-none before:absolute before:inset-y-0 before:-inset-x-1.5 before:content-[''] focus-visible:ring-2 focus-visible:ring-ring\",\n on ? look.on : look.mark,\n colorOf(m),\n on && \"ring-2 ring-background\",\n );\n const style = { \"--at\": `${m.pct}%` };\n return onMark ? (\n <button\n key={m.id}\n type=\"button\"\n aria-label={name}\n aria-pressed={on}\n title={name}\n className={className}\n style={style}\n onPointerDown={(e) => e.stopPropagation()}\n onClick={() => onMark(m)}\n onKeyDown={(e) => {\n if (e.key !== \"ArrowRight\" && e.key !== \"ArrowLeft\") return;\n const to = nextMark(placed, m.id, e.key === \"ArrowRight\" ? 1 : -1);\n const el = e.currentTarget.parentElement?.querySelector(`[data-mark=\"${CSS.escape(String(to))}\"]`);\n if (el && to !== m.id) {\n e.preventDefault();\n el.focus();\n }\n }}\n data-mark={m.id}\n />\n ) : (\n <span key={m.id} role=\"img\" aria-label={name} title={name} className={className} style={style} />\n );\n })}\n {canPick && shown && shown.widthPct > 0\n ? [\n [\"from\", shown.from, shown.fromPct],\n [\"to\", shown.to, shown.fromPct + shown.widthPct],\n ].map(([which, value, at]) => (\n <button\n key={which}\n type=\"button\"\n role=\"slider\"\n aria-label={which === \"from\" ? \"Part starts at\" : \"Part ends at\"}\n aria-valuemin={0}\n aria-valuemax={total}\n aria-valuenow={Math.round(value)}\n aria-valuetext={clockOf(value)}\n className={cx(\"absolute left-(--at) -translate-x-1/2 cursor-ew-resize rounded-[2px] bg-primary outline-none focus-visible:ring-2 focus-visible:ring-ring\", look.handle)}\n style={{ \"--at\": `${at}%` }}\n onPointerDown={(e) => e.stopPropagation()}\n onKeyDown={(e) => nudge(which, e)}\n />\n ))\n : null}\n </div>\n {labels.length ? (\n <div aria-hidden=\"true\" className=\"relative h-4 text-[11px] tabular-nums text-muted-foreground\">\n {labels.map((t, i) => (\n <span\n key={`${t.at}-${i}`}\n className={cx(\"absolute left-(--at) top-0 whitespace-nowrap\", t.edge === \"end\" ? \"-translate-x-full\" : t.edge === \"start\" ? \"\" : \"-translate-x-1/2\")}\n style={{ \"--at\": `${t.pct}%` }}\n >\n {t.label}\n </span>\n ))}\n </div>\n ) : null}\n <span className=\"sr-only\">\n {words}\n {Object.keys(kinds).length > 1 ? `, in ${Object.keys(kinds).length} kinds` : \"\"}\n {shown && shown.widthPct > 0 ? `. Part picked: ${clockOf(shown.from)} to ${clockOf(shown.to)}.` : \"\"}\n </span>\n </div>\n );\n}\n"
23
+ },
24
+ {
25
+ "path": "registry/blocks/time-strip/strip-model.js",
26
+ "type": "registry:component",
27
+ "target": "@components/makersclaw/blocks/time-strip/strip-model.js",
28
+ "content": "// time-strip: the arithmetic (registry/blocks/time-strip/strip-model.js).\n//\n// The block draws one bar for a span that has a length (a call, a recording,\n// a shift) and puts marks on it. What has to be right is where a mark lands\n// (a share of the length, clamped), how a time is read and said (\"04:12\",\n// \"1:02:03\"), which ticks the axis shows, how a dragged range is ordered\n// and kept inside the bar, and which mark a key moves to. All plain\n// functions, no React, so the server draws what the browser draws.\n//\n// A time is a number of seconds from the start, or a string \"mm:ss\",\n// \"h:mm:ss\" or \"hh:mm:ss\". Anything else is null: a bad time is never\n// rounded into a place on the bar.\n\nconst MAX_LENGTH = 7 * 24 * 3600;\n\n/** Seconds in a time: 76, \"01:16\", \"1:02:03\", \"76\" → 76, 76, 3723, 76. null when it is not a time. */\nexport function toSeconds(value) {\n if (typeof value === \"number\") return Number.isFinite(value) && value >= 0 ? value : null;\n if (typeof value !== \"string\") return null;\n const text = value.trim();\n if (!text) return null;\n if (/^\\d+(\\.\\d+)?$/.test(text)) return Number(text);\n const parts = text.split(\":\");\n if (parts.length < 2 || parts.length > 3 || !parts.every((p) => /^\\d{1,2}(\\.\\d+)?$/.test(p))) return null;\n const nums = parts.map(Number);\n if (nums.slice(1).some((n) => n >= 60)) return null;\n return nums.reduce((total, n) => total * 60 + n, 0);\n}\n\n/** A span's length in whole seconds, or 0 when it has none. */\nexport function lengthOf(value) {\n const s = toSeconds(value);\n if (s == null || s <= 0) return 0;\n return Math.min(MAX_LENGTH, Math.round(s));\n}\n\nconst two = (n) => String(n).padStart(2, \"0\");\n\n/** Seconds as a clock: 252 → \"04:12\"; 3723 → \"1:02:03\". `long` forces hours: 252 → \"0:04:12\". */\nexport function clockOf(seconds, { long = false } = {}) {\n const s = toSeconds(seconds);\n if (s == null) return \"\";\n const whole = Math.round(s);\n const h = Math.floor(whole / 3600);\n const m = Math.floor((whole % 3600) / 60);\n const sec = whole % 60;\n return h > 0 || long ? `${h}:${two(m)}:${two(sec)}` : `${two(m)}:${two(sec)}`;\n}\n\n/** Where a time sits on the bar, as a share 0..1 of `length`. null when it is not a time or there is no length. */\nexport function shareOf(at, length) {\n const s = toSeconds(at);\n const total = lengthOf(length);\n if (s == null || total <= 0) return null;\n return Math.min(1, Math.max(0, s / total));\n}\n\n/** A share as a percent of the bar, rounded to a thousandth so the markup is steady. */\nexport function pctOf(share) {\n if (share == null) return null;\n return Math.round(share * 100000) / 1000;\n}\n\n/**\n * The marks a bar draws: `[{ id, at, ... }]` in time order, each with its\n * `seconds`, `share` and `pct`. A mark with no readable time is left out (it\n * has no place); a time past the end sits at the end. Ties keep the order\n * given. `id` falls back to its index.\n */\nexport function marksOf(marks, length) {\n const total = lengthOf(length);\n if (!Array.isArray(marks) || total <= 0) return [];\n const out = [];\n marks.forEach((m, index) => {\n if (!m || typeof m !== \"object\") return;\n const seconds = toSeconds(m.at);\n if (seconds == null) return;\n const share = shareOf(seconds, total);\n out.push({ ...m, id: m.id ?? `m${index}`, index, seconds: Math.min(seconds, total), share, pct: pctOf(share) });\n });\n return out.sort((a, b) => a.seconds - b.seconds || a.index - b.index);\n}\n\n/**\n * Stretches laid on the bar (the part the person was away, a selection):\n * `[[from, to]]` or `[{ from, to, label? }]`. Each is ordered, kept inside\n * [0, length] and dropped when nothing is left of it.\n */\nexport function rangesOf(ranges, length) {\n const total = lengthOf(length);\n if (!Array.isArray(ranges) || total <= 0) return [];\n const out = [];\n for (const r of ranges) {\n const raw = Array.isArray(r) ? { from: r[0], to: r[1] } : r;\n const r2 = rangeOf(raw?.from, raw?.to, total);\n if (r2) out.push({ ...(Array.isArray(r) ? {} : raw), ...r2 });\n }\n return out;\n}\n\n/** Two times as a range kept inside the bar and in order: { from, to, fromPct, widthPct } or null when they enclose nothing. */\nexport function rangeOf(a, b, length, { min = 0 } = {}) {\n const total = lengthOf(length);\n const x = toSeconds(a);\n const y = toSeconds(b);\n if (x == null || y == null || total <= 0) return null;\n const from = Math.min(total, Math.max(0, Math.min(x, y)));\n const to = Math.min(total, Math.max(0, Math.max(x, y)));\n if (to - from <= min) return null;\n return { from, to, fromPct: pctOf(from / total), widthPct: pctOf((to - from) / total) };\n}\n\n/** A pointer's place on the bar (0..1 across it) as a time, snapped to `step` seconds. */\nexport function secondsAt(fraction, length, step = 1) {\n const total = lengthOf(length);\n if (total <= 0 || !Number.isFinite(fraction)) return 0;\n const raw = Math.min(1, Math.max(0, fraction)) * total;\n const s = Math.max(1, Number(step) || 1);\n return Math.min(total, Math.round(raw / s) * s);\n}\n\n/** A nice step under `want` seconds: 1, 2, 5, 10, 15, 30 s; 1, 2, 5, 10, 15, 30 min; 1, 2, 3, 6, 12 h. */\nconst STEPS = [1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600, 7200, 10800, 21600, 43200];\n\n/**\n * Axis ticks for a bar: about `count` evenly spaced labels, always the start\n * and the end. `[{ at, label, share, pct, edge }]`; `edge` is \"start\" or\n * \"end\" for the two ends so a label can be kept inside the bar. The inner\n * ticks fall on whole steps (a quarter hour, a minute) and are dropped when\n * they would crowd the end.\n */\nexport function ticksFor(length, count = 5) {\n const total = lengthOf(length);\n if (total <= 0) return [];\n const want = Math.max(2, Math.min(12, Math.round(count)));\n const raw = total / (want - 1);\n // The whole step nearest the even spacing, a little over at most: 63 s apart is a minute, not two.\n const step = [...STEPS].reverse().find((s) => s <= raw * 1.25) ?? STEPS[0];\n const at = [0];\n for (let t = step; t < total; t += step) {\n if (total - t < step * 0.4) break;\n at.push(t);\n }\n at.push(total);\n return at.map((t, i) => ({ at: t, label: clockOf(t), share: t / total, pct: pctOf(t / total), edge: i === 0 ? \"start\" : i === at.length - 1 ? \"end\" : null }));\n}\n\n/**\n * The mark a key moves to from `current` (an id, or null): `dir` 1 the next\n * in time, -1 the one before; at either end it stays. With nothing current,\n * 1 goes to the first and -1 to the last.\n */\nexport function nextMark(marks, current, dir) {\n if (!marks.length) return null;\n const at = marks.findIndex((m) => m.id === current);\n if (at < 0) return (dir < 0 ? marks[marks.length - 1] : marks[0]).id;\n const to = Math.min(marks.length - 1, Math.max(0, at + (dir < 0 ? -1 : 1)));\n return marks[to].id;\n}\n\n/** How many marks of each hue / tone: { 1: 2, danger: 1, none: 3 }, for the words a screen reader hears. */\nexport function countMarks(marks) {\n const out = {};\n for (const m of marks) {\n const key = m.hue ? String(m.hue) : (m.tone ?? \"none\");\n out[key] = (out[key] ?? 0) + 1;\n }\n return out;\n}\n\n/** One line for a reader: \"9 marks on a 04:12 call\", \"No marks\". */\nexport function stripWords({ length, marks, noun = \"mark\" }) {\n const total = lengthOf(length);\n const n = marks.length;\n if (total <= 0) return \"Nothing to show\";\n const word = n === 1 ? noun : `${noun}s`;\n return n ? `${n} ${word} in ${clockOf(total)}` : `No ${noun}s in ${clockOf(total)}`;\n}\n"
29
+ },
30
+ {
31
+ "path": "registry/blocks/time-strip/README.md",
32
+ "type": "registry:file",
33
+ "target": "@components/makersclaw/blocks/time-strip/README.md",
34
+ "content": "# TimeStrip\n\nOne bar for a span that has a length, with marks at the times that matter.\n\nA four-minute client call is a bar from `00:00` to `04:12`. Nine things were\ncut from it, and each is a thin post on the bar at the time it was said, in the\ncolour of its kind. A person sees *where in the call* the sensitive things\nhappened, and presses one to go there. The stretch before the client joined is\nhatched. The same bar sits small under a record's title and large as a\nscreen's signature.\n\nIt draws marks the app has already placed. It plays nothing, cuts nothing and\nmeasures nothing. For a chain of dated phases with gates use `schedule-ruler`;\nfor progress against a clock `pace-bar`; for events on an hour grid\n`time-grid`; for a waveform with a playhead `audio-player`.\n\nBorn in a minutes app that drew it by hand on three screens: a call's cuts under\nits title, the same strip large with the transcript, and a part of the call\npicked out to minute.\n\n```jsx\nimport { TimeStrip, toSeconds, clockOf } from \"../components/blocks/time-strip\"; // a Makersclaw app (\"./blocks/time-strip\" from components/)\nimport { TimeStrip } from \"@/components/makersclaw/blocks/time-strip\"; // a shadcn project\n```\n\nA client component. No other block and no npm package. Positions are\npercentages of the length, so the server and the browser draw the same picture;\nnothing is measured except at the moment a person drags.\n\n## Times\n\nA time is a number of seconds from the start, or a string: `\"76\"`, `\"01:16\"`,\n`\"1:02:03\"`. A time that is none of these has no place on the bar: its mark is\nleft out, never rounded into one. A time past the end sits at the end.\n`toSeconds(\"01:16\")` is `76`; `clockOf(3723)` is `\"1:02:03\"` and `clockOf(252)`\nis `\"04:12\"`.\n\n## Props\n\n- `length` — how long the span is: seconds, or a time string. Required. With no\n length the bar is empty and says \"Nothing to show\".\n- `marks` — `[{ id, at, hue?, tone?, label? }]`. `at` is the mark's time.\n - `hue` — `1` to `5`: a category colour (a reason, a speaker, a kind).\n - `tone` — `neutral` · `primary` · `info` · `success` · `warning` ·\n `danger`: a status. `hue` wins over `tone`. Neither: ink.\n - `label` — what a reader hears: \"Pricing\". Spoken as \"Pricing, mark at\n 00:12\".\n - Any other key is kept and handed back to `onMark`.\n- `away` — stretches somebody was away, hatched: `[[from, to]]` or `[{ from, to,\n label? }]`. A stretch with nothing inside the bar is dropped.\n- `selection` — `{ from, to }`: the part picked, drawn with a frame and a handle\n at each end.\n- `onSelect(range | null)` — makes the bar pick-able: drag across it to pick a\n part (`{ from, to }`, in seconds, snapped to `step`); a press that goes nowhere\n clears it (`null`). The two handles are sliders: ← → move one by `step`, with\n Shift by ten steps, Home and End go to the ends, Esc clears. **Give a keyboard\n path of your own as well** (two time fields) for a person who cannot drag.\n- `selected` — the id of the mark to show pressed (taller, ringed).\n- `onMark(mark)` — makes the marks buttons. Called with the mark as laid out\n (`{ ...yours, seconds, share, pct }`). ← → on a focused mark moves to the next\n or the one before. Without it the marks are plain pictures with a label.\n- `size` — `sm` (22 px, default) or `lg` (44 px).\n- `axis` — `true` (default): about `ticks` labels under the bar on whole steps,\n always the start and the end. `false`: none. Or your own labels,\n `[{ at, label? }]`: `[{ at: 0, label: \"00:00 · before the client joined\" },\n { at: 252, label: \"after they left · 04:12\" }]`. The first label keeps to the\n left edge and the last to the right, so a long word never leaves the bar.\n- `ticks` — how many labels `axis: true` aims for (2 to 12). Default 5.\n- `step` — seconds a drag snaps to and a key moves by. Default 1.\n- `noun` — what a mark is called in the words a reader hears. Default `mark`.\n- `label` — the bar's name for a reader. Default \"Timeline\".\n- `status` — `ready` (default), `loading` (a grey bar) or `failed` (a notice\n with `error` and, if you pass `onRetry`, Try again).\n- `className` — layout: width and margin. Never colour.\n\n## Helpers\n\nPlain functions from the same folder, safe on the server:\n\n- `toSeconds(value)` → seconds or `null`; `clockOf(seconds)` → `\"04:12\"`.\n- `marksOf(marks, length)` — the marks as laid out, in time order, each with\n `seconds`, `share` (0..1) and `pct`.\n- `rangeOf(a, b, length)` — two times as an ordered range kept inside the bar,\n or `null` when they enclose nothing. `rangesOf(ranges, length)` for a list.\n- `ticksFor(length, count)` — the axis ticks. `shareOf`, `pctOf`, `secondsAt`.\n- `nextMark(marks, currentId, dir)` — where a key goes.\n- `stripWords({ length, marks, noun })` — \"9 marks in 04:12\".\n\n## Example\n\nA call's cuts under its title, large with the transcript, and a part to pick.\n\n```jsx\n\"use client\";\nimport { useState } from \"react\";\nimport { TimeStrip } from \"../components/blocks/time-strip\";\n\nconst FAMILY = { pricing: 1, third_party: 2, off_record: 3, joke: 4 };\n\nexport function CallStrip({ call, cuts, onGoTo }) {\n const [part, setPart] = useState(null);\n return (\n <>\n <TimeStrip\n label=\"The call, with every cut where it was said\"\n noun=\"cut\"\n length={call.length}\n away={[[0, call.joinedAt], [call.leftAt, call.length]]}\n marks={cuts.map((c) => ({ id: c.id, at: c.at, hue: FAMILY[c.reason] ?? 4, label: c.reasonLabel }))}\n onMark={(m) => onGoTo(m.id)}\n axis={[\n { at: 0, label: \"00:00 · before the client joined\" },\n { at: call.length, label: `after they left · ${call.lengthWords}` },\n ]}\n size=\"lg\"\n />\n <TimeStrip length={call.length} selection={part} onSelect={setPart} step={5} />\n </>\n );\n}\n```\n\n## States\n\nDrawn in `demo.jsx`: a call with nine marks in four colours, large and small; a\nstretch away at each end; a pressed mark; marks pressable and read-only; a part\npicked, and the bar to pick one; an hour-long call with twenty marks; marks on\nthe same second; a mark past the end and one with a bad time; a call with no\nmarks; no length; the app's own axis labels; loading; failed; a 320 px\ncontainer.\n\n## What it will not do\n\n- Play, scrub or cut the thing it shows. It is a picture of marks.\n- Keep the marks. Add, move and remove them in your own data and pass the list.\n- Draw two rows (one lane per speaker): use one strip per lane.\n- Colour a mark by anything but `hue` or `tone`.\n\nA strip it cannot be is your own component in `components/`, built from the\nkit, not an edit to this block. Its files are fixed: `npm run check` refuses a\nchange, and `makersclaw add time-strip` restores them.\n"
35
+ }
36
+ ],
37
+ "categories": [
38
+ "time",
39
+ "strip",
40
+ "timeline",
41
+ "marks",
42
+ "markers",
43
+ "call",
44
+ "meeting",
45
+ "recording",
46
+ "transcript",
47
+ "chapters",
48
+ "cuts",
49
+ "range",
50
+ "select a part",
51
+ "scrubber",
52
+ "minutes",
53
+ "duration",
54
+ "axis",
55
+ "agent"
56
+ ],
57
+ "docs": "Installed to components/makersclaw/blocks/time-strip. Import: import { TimeStrip, clockOf, countMarks, lengthOf, marksOf, nextMark, pctOf, rangeOf, rangesOf, secondsAt, shareOf, stripWords, ticksFor, toSeconds } from \"@/components/makersclaw/blocks/time-strip\". Props and an example: components/makersclaw/blocks/time-strip/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
58
+ "meta": {
59
+ "makersclaw": {
60
+ "version": "0.8.1",
61
+ "useWhen": "A thing that has a length in time and events at moments inside it: the cuts, decisions and comments on a recorded call or meeting, the chapters of a video, the notes pinned to a recording, the incidents in a shift or a day, the stops in a route. It draws marks the app already placed; it does not play, cut or measure anything. Not a dated chain with gates (schedule-ruler), progress against a clock (pace-bar), a calendar of events on an hour grid (time-grid, calendar-view), or an audio waveform with a playhead (audio-player)."
62
+ }
63
+ }
64
+ }
package/r/top-bar.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "top-bar",
4
+ "type": "registry:block",
5
+ "title": "TopBar",
6
+ "description": "The one bar every app draws over its work: where you are (a back button, a breadcrumb that keeps its last label and gives up the earlier ones first, a note), the week's allowance (words and the kit's compact Meter), Find, Ask and the screen's main action. It gives way in a set order as the app's window narrows, measured by the bar's own width (a container query, not the window: an open agent dock takes a quarter of it): under 32rem Find, Ask and the action are icons; from 32rem their words; from 36rem the earlier crumbs and the note; from 42rem the ⌘K key; from 48rem `extra`; from 56rem the allowance bar; from 64rem the allowance in words. A table of every piece's width is the single source (top-bar-model.js), and a test walks every width from a phone to a wide desk and checks what is drawn fits, so nothing overlaps. Ask is never optional: when the app has an agent to ask (a dock from useAgentDock, an onAsk, or an AskProvider above) the bar draws Ask and it opens the agent; no prop leaves it out, and `npm run check` refuses an app that mounts an AgentDock with no way to open it. It draws where it is rendered (an AppLayout header) or into the shell's header slot (TopBarSlot). AskProvider, AskButton, FindButton, BarText and the model's plain functions are exported.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json"
9
+ ],
10
+ "dependencies": [],
11
+ "files": [
12
+ {
13
+ "path": "registry/blocks/top-bar/index.js",
14
+ "type": "registry:component",
15
+ "target": "@components/makersclaw/blocks/top-bar/index.js",
16
+ "content": "// The TopBar block — `makersclaw add top-bar`. Fixed: compose it through its\n// props (README.md beside this file). The model's helpers are plain\n// functions; they work on the server too.\nexport { AskButton, AskProvider, BarText, FindButton, TopBar, TopBarSlot, useAsk } from \"./top-bar\";\nexport { BAR_DEFAULTS, GAP, PIECES, SHOW, SIZES, allowanceOf, allowanceWords, askAction, askDrawn, askOpen, barDefaultsOf, fitsAt, floorRem, resolveBar, usedAt, visibleAt } from \"./top-bar-model\";\n"
17
+ },
18
+ {
19
+ "path": "registry/blocks/top-bar/top-bar.jsx",
20
+ "type": "registry:component",
21
+ "target": "@components/makersclaw/blocks/top-bar/top-bar.jsx",
22
+ "content": "\"use client\";\n\nimport { createContext, useCallback, useContext, useMemo } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { Button, Icon, IconButton, Kbd, Meter, cx } from \"../../ui\";\nimport { SHOW, allowanceOf, allowanceWords, askAction, askDrawn, askOpen, barDefaultsOf, resolveBar } from \"./top-bar-model\";\n\n// TopBar — the one bar every app draws over its work (2026-10-12). It holds\n// where you are, the week's allowance, Find, Ask and the page's action, and\n// it gives way in a set order as the app's window narrows, so nothing runs\n// into anything:\n//\n// always the menu/lead, the last crumb, Find and Ask as icons, the action\n// from 32rem the words on Find, Ask and the action (BarText)\n// from 36rem the earlier crumbs and the note\n// from 42rem the ⌘K key on Find\n// from 48rem `extra` (a switch, a chip)\n// from 56rem the allowance, as a bar\n// from 64rem the allowance in words\n//\n// The sizes are measures of the BAR, not the window: the app lives in a frame\n// on the desk, and an open agent dock takes a quarter of it. top-bar-model.js\n// holds the table; a test walks every width and checks what is drawn fits.\n//\n// Ask is never optional. If the app has an agent to ask (a `dock` from\n// useAgentDock, an `onAsk`, or an AskProvider above the bar) the bar draws\n// Ask and it opens the agent. There is no prop that leaves it out, so an app\n// that mounts an AgentDock cannot have an agent nobody can reach (Gantt\n// Planner, 2026-10-12: the dock was mounted and no button opened it).\n// `npm run check` (rule 19, ask) refuses the app that mounts a dock without\n// one.\n\n/** Where a screen's bar goes: the shell's header slot (an element). Leave it\n * unprovided and the bar draws where it is rendered (as an AppLayout's\n * `header`); provide null until the slot has mounted and it draws nothing. */\nexport const TopBarSlot = createContext(undefined);\n\nconst AskContext = createContext(null);\nconst BarContext = createContext({});\n\n/** Put the app's agent within reach of every bar below, and say the bar's\n * defaults once: the shell wraps its layout in it, with the dock\n * useAgentDock() returned, and no screen has to pass anything to its TopBar.\n * dock what useAgentDock() returned ({ open, toggle })\n * onAsk or your own opener, when there is no dock yet\n * open with onAsk: whether the agent is open (aria-pressed)\n * and the props every screen's bar repeats, which a bar uses unless it passes\n * its own (pass `allowance={null}` on a screen to leave it out there):\n * allowance, allowanceLabel, allowanceUnit, allowancePeriod\n * findEvent, onFind, findLabel, findWord\n * askLabel */\nexport function AskProvider({ dock, onAsk, open, children, ...bar }) {\n const toggle = askAction({ dock, onAsk });\n const isOpen = askOpen({ dock, asking: open });\n const value = useMemo(() => (toggle ? { toggle, open: isOpen } : null), [toggle, isOpen]);\n const d = barDefaultsOf(bar);\n // The allowance is a fresh object each render; what matters is its numbers.\n const key = JSON.stringify([d.allowance?.used ?? null, d.allowance?.cap ?? null, d.allowanceLabel, d.allowanceUnit, d.allowancePeriod, d.findEvent, d.findLabel, d.findWord, d.askLabel]);\n const defaults = useMemo(() => d, [key, d.onFind]); // eslint-disable-line react-hooks/exhaustive-deps\n return (\n <AskContext.Provider value={value}>\n <BarContext.Provider value={defaults}>{children}</BarContext.Provider>\n </AskContext.Provider>\n );\n}\n\n/** { toggle, open } from the nearest AskProvider, or null. */\nexport function useAsk() {\n return useContext(AskContext);\n}\n\n/** A label for the bar's buttons that gives way under 32rem, leaving the\n * icon; a screen reader still hears it (it is sr-only there, so an icon-only\n * button keeps its name). Use it for the action's words: `<Button\n * icon=\"plus\"><BarText>New issue</BarText></Button>`. The button needs an\n * icon, or under 32rem there is nothing to see (npm run check, rule\n * barlabel). Keep the words to about 12 characters. */\nexport function BarText({ children, className }) {\n return <span className={cx(SHOW.actionLabel, className)}>{children}</span>;\n}\n\n/** The Ask button. TopBar draws it; use it on its own only in a bar of your\n * own that cannot be a TopBar (the check accepts it as the way in).\n * dock, onAsk, asking as AskProvider; the provider's is the default\n * label the words from 32rem. Default \"Ask\" */\nexport function AskButton({ dock, onAsk, asking, label = \"Ask\", className }) {\n const context = useContext(AskContext);\n const act = askAction({ dock, onAsk, context });\n const on = useCallback(() => act?.(), [act]);\n if (!askDrawn({ dock, onAsk, context })) return null;\n return (\n <Button size=\"sm\" onClick={on} data-bar-piece=\"ask\" aria-label=\"Ask the agent\" aria-pressed={askOpen({ dock, asking, context })} className={className}>\n <Icon name=\"sparkles\" size={14} />\n <span className={SHOW.askLabel}>{label}</span>\n </Button>\n );\n}\n\n/** The Find button. `onFind`, or else a window event named `event` (the\n * app's finder listens for it).\n * label what a screen reader hears (\"Find an issue, a change or a post\")\n * word the visible word from 32rem. Default \"Find\" */\nexport function FindButton({ onFind, event = \"app:find\", label = \"Find\", word = \"Find\", className }) {\n const on = () => (onFind ? onFind() : window.dispatchEvent(new CustomEvent(event)));\n return (\n <Button size=\"sm\" onClick={on} data-bar-piece=\"find\" aria-label={label} className={className}>\n <Icon name=\"search\" size={14} />\n <span className={SHOW.findLabel}>{word}</span>\n <Kbd className={SHOW.findKey}>⌘K</Kbd>\n </Button>\n );\n}\n\n// The allowance: its words, then the kit's Meter in its compact form (the bar\n// alone; the label and the numbers go to a screen reader and the tooltip).\nfunction AllowanceBar({ allowance, unit, period, label }) {\n const a = allowanceOf(allowance);\n const words = allowanceWords(allowance, { unit, period });\n if (!a) return null;\n return (\n <span data-bar-piece=\"allowance\" className={cx(SHOW.allowance, \"shrink-0 items-center gap-2 whitespace-nowrap text-xs text-muted-foreground\")}>\n <span className=\"tabular-nums\">\n {words.short}\n <span className={SHOW.allowanceWords}> {period}</span>\n </span>\n <Meter value={a.used} max={a.cap} label={label ?? \"Allowance\"} unit={unit} valueText={null} size=\"thin\" compact className=\"w-14\" />\n </span>\n );\n}\n\nfunction Crumb({ items, back }) {\n const last = items[items.length - 1];\n return (\n <nav aria-label=\"Where you are\" data-bar-piece=\"crumb\" className=\"flex min-w-0 shrink items-center gap-1.5 overflow-hidden whitespace-nowrap font-medium\">\n {back ? <IconButton icon=\"chevron-left\" label=\"Back\" size=\"sm\" href={back} /> : null}\n {items.map((c, i) => {\n const here = c === last;\n return (\n <span key={`${c.label}-${i}`} className={cx(\"flex min-w-0 items-center gap-1.5\", here ? \"shrink\" : cx(SHOW.trailFlex, \"shrink-[4]\"))}>\n {i ? (\n <span aria-hidden=\"true\" className={SHOW.trailInline}>\n <Icon name=\"chevron-right\" size={12} className=\"shrink-0 text-muted-foreground\" />\n </span>\n ) : null}\n {c.href && !here ? (\n <span className=\"min-w-0 truncate font-normal\">\n <Button variant=\"link\" tone=\"muted\" size=\"inherit\" href={c.href}>\n {c.label}\n </Button>\n </span>\n ) : (\n <span className={cx(\"min-w-0 truncate\", c.muted && \"font-normal text-muted-foreground\")} aria-current={here ? \"page\" : undefined}>\n {c.label}\n </span>\n )}\n </span>\n );\n })}\n </nav>\n );\n}\n\n/**\n * One screen's top bar.\n *\n * lead a node before everything (the sidebar trigger, a period picker)\n * back a path: a back button before the crumb\n * crumb [{ label, href?, muted? }]: where you are; the last is this screen\n * note muted words after the crumb (from 36rem)\n * extra nodes before the allowance (from 48rem)\n * allowance { used, cap } of the week's credits, from the platform's budget\n * (null: nothing drawn). `allowanceLabel`, `allowanceUnit` (\"cr\"),\n * `allowancePeriod` (\"this week\"). Left out, the shell's\n * AskProvider says them once; null leaves it out on this screen\n * find false leaves Find out. `onFind`, or `findEvent` (a window event;\n * default \"app:find\"), `findLabel` (for a screen reader), `findWord`.\n * Left out, the AskProvider's are used\n * dock what useAgentDock() returns: Ask opens it\n * onAsk or your own opener. Neither: the nearest AskProvider's. If none,\n * the app has no agent and no Ask is drawn\n * asking whether the agent is open, when you pass onAsk\n * askLabel the word on Ask. Default \"Ask\"\n * action the screen's main action, last in the bar: a Button whose words\n * are <BarText>\n * className spacing only\n *\n * It draws where it is rendered (as an AppLayout `header`), or into the\n * shell's header slot when a TopBarSlot is provided.\n */\nexport function TopBar({\n lead,\n back,\n crumb = [],\n note,\n extra,\n allowance: allowanceOwn,\n allowanceLabel: allowanceLabelOwn,\n allowanceUnit: allowanceUnitOwn,\n allowancePeriod: allowancePeriodOwn,\n find = true,\n onFind: onFindOwn,\n findEvent: findEventOwn,\n findLabel: findLabelOwn,\n findWord: findWordOwn,\n dock,\n onAsk,\n asking,\n askLabel: askLabelOwn,\n action,\n className,\n}) {\n const slot = useContext(TopBarSlot);\n const defaults = useContext(BarContext);\n if (slot === null) return null;\n // What this screen did not pass, the shell's AskProvider said once.\n const { allowance, allowanceLabel, allowanceUnit, allowancePeriod, onFind, findEvent, findLabel, findWord, askLabel } = resolveBar(\n {\n allowance: allowanceOwn,\n allowanceLabel: allowanceLabelOwn,\n allowanceUnit: allowanceUnitOwn,\n allowancePeriod: allowancePeriodOwn,\n onFind: onFindOwn,\n findEvent: findEventOwn,\n findLabel: findLabelOwn,\n findWord: findWordOwn,\n askLabel: askLabelOwn,\n },\n defaults,\n );\n const bar = (\n <div data-top-bar className={cx(\"@container/bar flex h-10 min-w-0 items-center gap-1.5\", slot === undefined && \"px-2 sm:px-3\", className)}>\n {lead ? <span className=\"flex shrink-0 items-center\">{lead}</span> : null}\n <Crumb items={crumb} back={back} />\n {note ? <span data-bar-piece=\"note\" className={cx(SHOW.trailInline, \"min-w-0 shrink-[4] truncate whitespace-nowrap text-muted-foreground\")}>{note}</span> : null}\n <span className=\"min-w-0 flex-1\" />\n {extra ? <span data-bar-piece=\"extra\" className={cx(SHOW.extra, \"shrink-0 items-center gap-1.5\")}>{extra}</span> : null}\n <AllowanceBar allowance={allowance} unit={allowanceUnit} period={allowancePeriod} label={allowanceLabel} />\n {find ? <FindButton onFind={onFind} event={findEvent} label={findLabel} word={findWord} className=\"shrink-0\" /> : null}\n <AskButton dock={dock} onAsk={onAsk} asking={asking} label={askLabel} className=\"shrink-0\" />\n {action ? <span data-bar-piece=\"action\" className=\"flex shrink-0 items-center\">{action}</span> : null}\n </div>\n );\n return slot === undefined ? bar : createPortal(bar, slot);\n}\n"
23
+ },
24
+ {
25
+ "path": "registry/blocks/top-bar/top-bar-model.js",
26
+ "type": "registry:component",
27
+ "target": "@components/makersclaw/blocks/top-bar/top-bar-model.js",
28
+ "content": "// The top bar's rules, as plain functions and one table: what the bar draws\n// at a given width, in what order things give way, and whether Ask is drawn.\n// No React here, so a test can walk every width and the server can use it.\n//\n// Why a table (2026-10-12, the UI review): fourteen apps drew their own top\n// bar, and in Newsletter the week's allowance ran into Find. A bar that is\n// told only \"hide this under 56rem\" overlaps the day an app adds a longer\n// button. So every piece has a width it is allowed to take (`rem`) and the\n// bar width it appears from (`from`, a Tailwind container size), and a test\n// walks the widths from a phone to a wide desk and checks that what is drawn\n// always fits. The CSS classes in `SHOW` are written from the same table.\n\n/** Container sizes, in rem, as Tailwind v4 names them (@lg = 32rem…). */\nexport const SIZES = { xs: 20, sm: 24, md: 28, lg: 32, xl: 36, \"2xl\": 42, \"3xl\": 48, \"4xl\": 56, \"5xl\": 64, \"6xl\": 72 };\n\n/** The width the bar has to give each piece, in rem, and where it appears.\n * `from: null` is always drawn. The crumb's last label is the floor: it\n * truncates, but never gives up the room it is counted here. `extra` and\n * the action's label are the app's: they are held to these widths (the\n * README says how; `BarText` hides a label under `lg`). */\nexport const PIECES = [\n { key: \"lead\", rem: 2.5, from: null },\n { key: \"crumb\", rem: 5, from: null },\n { key: \"find\", rem: 2.5, from: null },\n { key: \"ask\", rem: 2.5, from: null },\n { key: \"action\", rem: 2.5, from: null },\n { key: \"findLabel\", rem: 3.5, from: \"lg\" },\n { key: \"askLabel\", rem: 2.5, from: \"lg\" },\n { key: \"actionLabel\", rem: 7, from: \"lg\" },\n { key: \"trail\", rem: 0, from: \"xl\" }, // earlier crumbs and the note: they truncate, so they take what is left and no more\n { key: \"findKey\", rem: 3.2, from: \"2xl\" },\n { key: \"extra\", rem: 12, from: \"3xl\" },\n { key: \"allowance\", rem: 5, from: \"4xl\" },\n { key: \"allowanceWords\", rem: 8, from: \"5xl\" },\n];\n\n/** Space between neighbours, in rem (gap-1.5). */\nexport const GAP = 0.375;\n\n/** The classes that show a piece from its size and hide it under, written\n * whole so Tailwind reads them. */\nexport const SHOW = {\n findLabel: \"hidden @lg/bar:inline\",\n askLabel: \"hidden @lg/bar:inline\",\n // The action's words stay for a screen reader under 32rem (a button that is\n // only its icon still has its name): sr-only there, drawn from 32rem.\n actionLabel: \"sr-only @lg/bar:not-sr-only\",\n trailFlex: \"hidden @xl/bar:flex\",\n trailInline: \"hidden @xl/bar:inline\",\n findKey: \"hidden @2xl/bar:inline-flex\",\n extra: \"hidden @3xl/bar:flex\",\n allowance: \"hidden @4xl/bar:flex\",\n allowanceWords: \"hidden @5xl/bar:inline\",\n};\n\nconst has = (piece, present) => (piece.key.startsWith(\"find\") ? present.find : piece.key.startsWith(\"ask\") ? present.ask : true);\n\n/** The pieces drawn in a bar `rem` wide. `present`: { find, ask } say whether\n * those are drawn at all (an app can leave Find out; it can never leave\n * Ask out when it has a dock). */\nexport function visibleAt(rem, present = { find: true, ask: true }) {\n return PIECES.filter((p) => has(p, present) && (p.from === null || rem >= SIZES[p.from])).map((p) => p.key);\n}\n\n/** The room the drawn pieces take at `rem`, gaps included. A label rides\n * inside its button, so only these items have a gap between them. */\nconst ITEMS = [\"lead\", \"crumb\", \"find\", \"ask\", \"action\", \"extra\", \"allowance\"];\nexport function usedAt(rem, present = { find: true, ask: true }) {\n const keys = new Set(visibleAt(rem, present));\n const sum = PIECES.filter((p) => keys.has(p.key)).reduce((n, p) => n + p.rem, 0);\n const items = ITEMS.filter((k) => keys.has(k)).length;\n return sum + Math.max(0, items - 1) * GAP;\n}\n\n/** True when what is drawn at `rem` fits with nothing over it. */\nexport function fitsAt(rem, present) {\n return usedAt(rem, present) <= rem;\n}\n\n/** The narrowest bar the table is true for: what is always drawn. */\nexport function floorRem(present) {\n return usedAt(0, present);\n}\n\n/** Is Ask drawn? Whenever the app has an agent to ask: a dock handed over,\n * an `onAsk`, or an AskProvider above the bar. There is no prop that turns\n * it off, and that is the point: an app with a dock cannot have an agent\n * nobody can open. */\nexport function askDrawn({ dock, onAsk, context } = {}) {\n return Boolean(dock || onAsk || context);\n}\n\n/** What pressing Ask does: an `onAsk` the app gave wins, then the dock's\n * toggle, then the provider's. Null when there is nothing to open. */\nexport function askAction({ dock, onAsk, context } = {}) {\n if (typeof onAsk === \"function\") return onAsk;\n if (dock && typeof dock.toggle === \"function\") return dock.toggle;\n if (context && typeof context.toggle === \"function\") return context.toggle;\n return null;\n}\n\n/** Whether the agent is open, for aria-pressed. */\nexport function askOpen({ dock, asking, context } = {}) {\n if (typeof asking === \"boolean\") return asking;\n if (dock && typeof dock.open === \"boolean\") return dock.open;\n if (context && typeof context.open === \"boolean\") return context.open;\n return false;\n}\n\n/** The props a screen's TopBar may leave to the shell: the week's allowance\n * and how Find is reached are the same on every screen of an app, so the\n * shell says them once on its AskProvider and a bar uses them unless it\n * passes its own. */\nexport const BAR_DEFAULTS = [\"allowance\", \"allowanceLabel\", \"allowanceUnit\", \"allowancePeriod\", \"onFind\", \"findEvent\", \"findLabel\", \"findWord\", \"askLabel\"];\n\n/** The shell's defaults, from the props it gave AskProvider: only the keys it\n * set, `undefined` ones left out. */\nexport function barDefaultsOf(props = {}) {\n const out = {};\n for (const key of BAR_DEFAULTS) if (props[key] !== undefined) out[key] = props[key];\n return out;\n}\n\n/** One screen's bar props over the shell's defaults. A prop the screen passes\n * (even `null`, which leaves the allowance out for that screen) wins; one it\n * leaves `undefined` takes the shell's; else the built-in (\"cr\", \"this\n * week\"). */\nexport function resolveBar(own = {}, defaults = {}) {\n const out = {};\n for (const key of BAR_DEFAULTS) out[key] = own[key] !== undefined ? own[key] : defaults[key];\n out.allowanceUnit ??= \"cr\";\n out.allowancePeriod ??= \"this week\";\n return out;\n}\n\n/** The allowance as { left, used, cap, pct } from the platform's budget\n * ({ used, cap }); null when there is no cap to count against. */\nexport function allowanceOf(allowance) {\n const cap = Number(allowance?.cap);\n if (!allowance || !Number.isFinite(cap) || cap <= 0) return null;\n const used = Math.max(0, Number(allowance.used) || 0);\n return { used, cap, left: Math.max(0, cap - used), pct: Math.min(100, Math.round((used / cap) * 100)) };\n}\n\nconst num = (n) => Math.round(Number(n)).toLocaleString(\"en-US\");\n\n/** The allowance in words: \"15,000 cr left\" and, from 64rem, \" this week\". */\nexport function allowanceWords(allowance, { unit = \"cr\", period = \"this week\" } = {}) {\n const a = allowanceOf(allowance);\n if (!a) return null;\n return { short: `${num(a.left)} ${unit} left`, long: `${num(a.left)} ${unit} left ${period}`, period, left: a.left, cap: a.cap };\n}\n"
29
+ },
30
+ {
31
+ "path": "registry/blocks/top-bar/probe-rules.js",
32
+ "type": "registry:component",
33
+ "target": "@components/makersclaw/blocks/top-bar/probe-rules.js",
34
+ "content": "// The layout rules this block ships for the store's probe (makersclaw-store\n// scripts/probe.mjs --suite). They are data: the probe reads them from this\n// block in the app (components/blocks/top-bar/probe-rules.js) and checks each\n// one on every screen, at every window size, with the agent column shut and\n// open. Nothing in the app imports this file; it costs a few hundred bytes.\n//\n// `pieces` and `target` name the bar's parts by their data-bar-piece hook;\n// the second selector in each is the accessible name an older copy of the bar\n// (before the hooks) already has. Each finding is a UI-review number\n// (docs/UI-REVIEW-2026-10-12.md) turned into a measure.\nexport default {\n block: \"top-bar\",\n rules: [\n {\n id: \"pieces-never-intersect\",\n kind: \"disjoint\",\n note: \"UI review 1.1, 1.4: the bar's pieces overlapped\",\n scope: \"[data-top-bar]\",\n pieces: {\n \"Where you are\": '[data-bar-piece=\"crumb\"], nav[aria-label=\"Where you are\"]',\n \"the note\": '[data-bar-piece=\"note\"]',\n \"the extra\": '[data-bar-piece=\"extra\"]',\n \"the allowance\": '[data-bar-piece=\"allowance\"]',\n Find: '[data-bar-piece=\"find\"], button[aria-label^=\"Find\"]',\n Ask: '[data-bar-piece=\"ask\"], button[aria-label=\"Ask the agent\"]',\n \"the action\": '[data-bar-piece=\"action\"]',\n },\n },\n {\n id: \"ask-reachable\",\n kind: \"clickable\",\n note: \"UI review X8: no way to open the agent\",\n scope: \"[data-top-bar]\",\n target: '[data-bar-piece=\"ask\"], button[aria-label=\"Ask the agent\"]',\n name: \"Ask\",\n minSize: 24,\n },\n {\n id: \"find-reachable\",\n kind: \"clickable\",\n note: \"UI review 1.1: Find pushed out or covered\",\n scope: \"[data-top-bar]\",\n target: '[data-bar-piece=\"find\"], button[aria-label^=\"Find\"]',\n name: \"Find\",\n minSize: 24,\n },\n {\n id: \"action-reachable\",\n kind: \"clickable\",\n note: \"the screen's main action stays inside the window\",\n scope: \"[data-top-bar]\",\n target: '[data-bar-piece=\"action\"] button, [data-bar-piece=\"action\"] a',\n name: \"the action\",\n minSize: 24,\n },\n {\n id: \"crumb-label-not-cut-to-nothing\",\n kind: \"visible-text\",\n note: \"UI review 1.2, X3: a label cut to nothing\",\n scope: \"[data-top-bar]\",\n target: '[data-bar-piece=\"crumb\"] [aria-current=\"page\"], nav[aria-label=\"Where you are\"] [aria-current=\"page\"]',\n min: 3,\n },\n {\n id: \"an-agent-has-a-way-in\",\n kind: \"clickable\",\n note: \"UI review X8: a dock is mounted and nothing opens it\",\n target: '[data-bar-piece=\"ask\"], button[aria-label=\"Ask the agent\"], [data-ask]',\n name: \"a control that opens the agent\",\n requiredWhen: \"[data-agent-dock]\",\n minSize: 24,\n },\n ],\n};\n"
35
+ },
36
+ {
37
+ "path": "registry/blocks/top-bar/README.md",
38
+ "type": "registry:file",
39
+ "target": "@components/makersclaw/blocks/top-bar/README.md",
40
+ "content": "# TopBar\n\nThe one bar every app draws over its work (0.8.1): where you are, the week's\nallowance, Find, Ask and the screen's main action.\n\nUntil now fourteen apps drew their own (`components/<app>/frame.jsx`,\n`TopBar`). So a fix to one never reached the others, and each broke in its own\nway. The founder's review of 2026-10-12: Newsletter's \"15,000 cr left\" ran\ninto Find; Gantt Planner mounted the agent's dock and had no button that\nopened it. This is the bar, once.\n\n- **It never overlaps.** Every piece has a width it may take, and the bar\n gives way in a fixed order as the app's window narrows. The sizes are\n measures of the BAR (a container query), not of the screen: an open agent\n dock takes a quarter of the window.\n\n | The bar is | It draws |\n | --- | --- |\n | any width | the lead, the last crumb, Find and Ask as icons, the action as an icon |\n | 32rem | the words on Find, Ask and the action |\n | 36rem | the earlier crumbs and the note |\n | 42rem | the ⌘K key on Find |\n | 48rem | `extra` |\n | 56rem | the allowance, as a bar |\n | 64rem | the allowance in words (\"this week\") |\n\n The table is `PIECES` in `top-bar-model.js`; a test walks every width from\n 18rem to 100rem and checks that what is drawn fits.\n- **Ask can't be left out.** When the app has an agent to ask (a `dock` from\n `useAgentDock()`, an `onAsk`, or an `AskProvider` above the bar) the bar\n draws Ask and it opens the agent. No prop turns it off. `npm run check`\n (rule `ask`) refuses an app that mounts an `AgentDock` with no way to open\n it.\n- **The allowance is the kit's compact Meter**: the bar alone, with its label\n and numbers for a screen reader and the tooltip.\n\n```jsx\nimport { AskProvider, BarText, TopBar, TopBarSlot } from \"../components/blocks/top-bar\"; // a Makersclaw app (\"./blocks/top-bar\" from components/)\nimport { TopBar } from \"@/components/makersclaw/blocks/top-bar\"; // a shadcn project\n```\n\nA client component. It needs the kit (Button, Icon, Kbd, Meter) and nothing\nelse.\n\n## Adopting it (an app that drew its own)\n\n1. In the shell, wrap the layout in `AskProvider` with the dock:\n `<AskProvider dock={dock}>…</AskProvider>`, where `dock = useAgentDock({ storageKey })`.\n Delete the shell's old Ask handling (the `app:ask` window event, the\n `toggleAgent()` bus) that only the old bar called.\n2. Delete the app's own `TopBar` from `components/<app>/frame.jsx` (and its\n `allowanceOf`, `Meter` and `Find`/`Ask` buttons). Keep the rail.\n3. Say what every screen's bar repeats **once, on the `AskProvider`**: the\n platform's budget as `allowance={{ used, cap }}` (and `allowanceLabel`,\n `allowanceUnit`, `allowancePeriod` if they are not the defaults), and how\n Find is reached, `findEvent=\"issues:find\"` if the app's finder listens for a\n window event (`findLabel`, `findWord`, or an `onFind`). Every `<TopBar>`\n below uses them unless it passes its own (`allowance={null}` leaves the\n allowance out on one screen).\n4. Import `TopBar` from `blocks/top-bar` and give each screen's bar only what is\n that screen's: `crumb`, `note`, `extra`, `action`, `back`, `lead`.\n5. The action's words go in `<BarText>`: `<Button variant=\"primary\" size=\"sm\"\n icon=\"plus\"><BarText>New issue</BarText></Button>`. Under 32rem the button\n is its icon and the words stay for a screen reader (`sr-only`). The button\n must have an `icon`: `npm run check` (rule `barlabel`) refuses a `BarText`\n button without one, because under 32rem it would draw nothing. Keep the\n words to about 12 characters.\n6. If the old bar rendered through a portal into the shell's header slot, keep\n the slot and provide it: `<TopBarSlot.Provider value={slotEl}>` (an\n element; `null` until it has mounted). With no provider the bar draws where\n it is rendered, as an `AppLayout` `header`.\n\n## Props\n\n- `lead`: a node before everything (the sidebar trigger, a period picker).\n- `back`: a path; a back button before the crumb.\n- `crumb`: `[{ label, href?, muted? }]`; the last is this screen. The last\n label truncates last.\n- `note`: muted words after the crumb (from 36rem).\n- `extra`: nodes before the allowance (from 48rem). Keep it to about 12rem\n (a switch, a chip).\n- `allowance`: `{ used, cap }` (the platform's budget); nothing when null.\n `allowanceLabel`, `allowanceUnit` (`\"cr\"`), `allowancePeriod` (`\"this\n week\"`). Left out, the `AskProvider`'s (below).\n- `find`: `false` leaves Find out. `onFind`, or `findEvent` (a window event;\n default `\"app:find\"`); `findLabel` (a screen reader's), `findWord`. Left\n out, the `AskProvider`'s.\n- `dock`: what `useAgentDock()` returns. `onAsk`: or your own opener, with\n `asking` for its state. Neither: the nearest `AskProvider`'s. `askLabel`.\n- `action`: the screen's main action, last in the bar.\n- `className`: spacing only.\n\n`AskProvider({ dock, onAsk, open, …defaults })`, `useAsk()` (`{ toggle, open }`\nor null), `AskButton`, `FindButton` and `BarText` are exported.\n\n**Bar defaults.** The provider also carries the props the bars of an app\nrepeat: `allowance`, `allowanceLabel`, `allowanceUnit`, `allowancePeriod`,\n`findEvent`, `onFind`, `findLabel`, `findWord` and `askLabel`. A `TopBar` uses\nthem for any of those it does not pass; a prop it does pass (even `null`) wins.\nThe resolution is `resolveBar(own, defaults)` in the model. `useAsk()` reads\nthe agent (`open` is whether it is open; `toggle` toggles it); to **open** it\nfrom a screen, call `openAgent()` (blocks/agent-dock), not `useAsk()`. `AskButton` is for a bar\nthat cannot be a `TopBar` (the check accepts it as a way to open the dock).\n\n## The model (plain functions)\n\n`PIECES`, `SIZES`, `SHOW`, `visibleAt(rem)`, `usedAt(rem)`, `fitsAt(rem)`,\n`floorRem()`, `askDrawn({ dock, onAsk, context })`, `askAction(...)`,\n`askOpen(...)`, `allowanceOf({ used, cap })`, `allowanceWords(...)`,\n`BAR_DEFAULTS`, `barDefaultsOf(props)`, `resolveBar(own, defaults)`.\n\n## Example\n\n```jsx\n// app/shell.jsx\n\"use client\";\nimport { AppLayout, Button } from \"../components/ui\";\nimport { AgentDock, useAgentDock } from \"../components/blocks/agent-dock\";\nimport { AskProvider, BarText, TopBar } from \"../components/blocks/top-bar\";\n\nexport default function Shell({ children, budget }) {\n const dock = useAgentDock({ storageKey: \"issues\" });\n return (\n <AskProvider dock={dock} allowance={{ used: budget.used, cap: budget.cap }} findEvent=\"issues:find\" findLabel=\"Find an issue\">\n <AppLayout\n header={\n <TopBar\n crumb={[{ label: \"Issues\", href: \"/s/issues\" }, { label: \"Issue 14\" }]}\n action={\n <Button variant=\"primary\" size=\"sm\" icon=\"plus\">\n <BarText>New issue</BarText>\n </Button>\n }\n />\n }\n dock={<AgentDock dock={dock} label=\"Ask the agent\" />}\n >\n {children}\n </AppLayout>\n </AskProvider>\n );\n}\n```\n\n## States drawn in the demo\n\nThe same bar at 320, 480, 640, 800 and 1100 px; Ask with the dock closed and\nopen; a dock passed straight to the bar; an app with no agent (no Ask); a long\ntitle with a back button and no Find.\n"
41
+ }
42
+ ],
43
+ "categories": [
44
+ "top bar",
45
+ "header",
46
+ "breadcrumb",
47
+ "find",
48
+ "ask",
49
+ "agent",
50
+ "allowance",
51
+ "credits",
52
+ "action",
53
+ "shell",
54
+ "chrome",
55
+ "responsive",
56
+ "dock",
57
+ "search"
58
+ ],
59
+ "docs": "Installed to components/makersclaw/blocks/top-bar. Import: import { AskButton, AskProvider, BAR_DEFAULTS, BarText, FindButton, GAP, PIECES, SHOW, SIZES, TopBar, TopBarSlot, allowanceOf, allowanceWords, askAction, askDrawn, askOpen, barDefaultsOf, fitsAt, floorRem, resolveBar, useAsk, usedAt, visibleAt } from \"@/components/makersclaw/blocks/top-bar\". Props and an example: components/makersclaw/blocks/top-bar/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
60
+ "meta": {
61
+ "makersclaw": {
62
+ "version": "0.8.1",
63
+ "useWhen": "Every app's top bar, replacing each app's own components/<app>/frame.jsx TopBar: the shell or each screen's AppLayout header. Any app that mounts an AgentDock uses it (or AskButton in a bar it cannot replace) so the agent can be opened. Not for a bar inside a card or a sheet (use the parts), and not for the rail or the navigation (sidebar, navigation-menu)."
64
+ }
65
+ }
66
+ }
@@ -5,7 +5,7 @@
5
5
  "title": "TransactionList",
6
6
  "description": "Money records as a plain list, in two variants: history (payments in and out, grouped by month, a received / paid / pending / failed mark and a signed amount) and invoices (number, date, a Paid / Due in N days / N days overdue badge, amount, download). Optional date-range filter, Export and per-row download. Empty, loading and error states built in.",
7
7
  "registryDependencies": [
8
- "https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/core.json"
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json"
9
9
  ],
10
10
  "dependencies": [],
11
11
  "files": [
@@ -46,7 +46,7 @@
46
46
  "docs": "Installed to components/makersclaw/blocks/transaction-list. Import: import { TransactionList } from \"@/components/makersclaw/blocks/transaction-list\". Props and an example: components/makersclaw/blocks/transaction-list/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
47
47
  "meta": {
48
48
  "makersclaw": {
49
- "version": "0.8.0",
49
+ "version": "0.8.1",
50
50
  "useWhen": "A billing page's invoices, an account's payment history, a wallet's recent activity: records read top to bottom, newest first. Not for records the founder sorts, filters by column or totals (data-table), or for one order (order-detail)."
51
51
  }
52
52
  }
@@ -0,0 +1,61 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "trigger-picker",
4
+ "type": "registry:block",
5
+ "title": "TriggerPicker",
6
+ "description": "What starts an automation, as a panel: by hand, on a schedule in the team's time zone, when an app sends an event, when one of an app's actions finishes, when a record is added or changed, when the automation's own web address is called (its secret made and replaced here, shown once), or when another automation finishes. The platform says which of these it runs (the list door's triggers); a connection's own events are listed as \"soon\" with what to use meanwhile. The schedule is offered in the shapes people ask for (every day, every weekday, every week, every hour, another schedule as five-field cron) with a time of day, a weekday, the team's zone or another, and a sentence saying it back (\"Runs weekdays at 07:00, team time\").",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json",
9
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/automation-model.json"
10
+ ],
11
+ "dependencies": [],
12
+ "files": [
13
+ {
14
+ "path": "registry/blocks/trigger-picker/index.js",
15
+ "type": "registry:component",
16
+ "target": "@components/makersclaw/blocks/trigger-picker/index.js",
17
+ "content": "// The TriggerPicker block — `makersclaw add trigger-picker`. Fixed:\n// compose it through its props (README.md beside this file).\nexport { TriggerPicker } from \"./trigger-picker\";\n"
18
+ },
19
+ {
20
+ "path": "registry/blocks/trigger-picker/trigger-picker.jsx",
21
+ "type": "registry:component",
22
+ "target": "@components/makersclaw/blocks/trigger-picker/trigger-picker.jsx",
23
+ "content": "\"use client\";\n\n// What starts an automation. Eight ways, the platform tells which it runs:\n// by hand; on a schedule in the team's time zone; when an app sends an\n// event; when one of an app's actions finishes; when a record is added or\n// changed; when the automation's own web address is called; when another\n// automation finishes. A connection's own events (a new mail) are listed as\n// \"soon\", with what to use until then.\n//\n// The schedule is offered in the shapes people ask for (\"every weekday at\n// 07:00\") and kept as a five-field cron; anything the picker cannot read\n// back stays the custom cron it was.\n//\n// Props in, callbacks out; the definition's trigger is one of\n// { type: \"manual\" }\n// { type: \"schedule\", cron, tz }\n// { type: \"event\", topic, app? }\n// { type: \"action\", action, app? }\n// { type: \"record\", kinds?, ops?, app? }\n// { type: \"webhook\" }\n// { type: \"automation\", key, when }\n\nimport { useId, useState } from \"react\";\nimport { Button, Checkbox, Field, Input, Select, Switch, TimeInput, cx } from \"../../ui\";\nimport { Glyph, SidePanel, WEEKDAYS, cronFromParts, cronWords, parseTime, partsFromCron, timeText, triggerGlyph, triggerTitle, triggerWords } from \"../automation-model\";\n\nconst FREQS = [\n { value: \"daily\", label: \"Every day\" },\n { value: \"weekdays\", label: \"Every weekday\" },\n { value: \"weekly\", label: \"Every week\" },\n { value: \"hourly\", label: \"Every hour\" },\n { value: \"custom\", label: \"Another schedule\" },\n];\n\n/** What a platform without the newer triggers offers: two ways, and the rest said honestly. */\nconst FALLBACK_CHOICES = [\n { type: \"manual\", label: \"By hand\", hint: \"Someone presses Run, or the agent runs it.\", available: true },\n { type: \"schedule\", label: \"On a schedule\", hint: \"Every day, every weekday, every hour.\", available: true },\n];\nconst LATER = [\n { id: \"record\", label: \"When something is added\", hint: \"A new person, a new row, a new message.\" },\n { id: \"automation\", label: \"From another automation\", hint: \"One flow starts the next.\" },\n];\n\n/** A fresh trigger of a type, with what the platform lists first filled in. */\nfunction startOf(type, { app, other }) {\n switch (type) {\n case \"schedule\":\n return { type, cron: \"0 7 * * 1-5\", tz: \"team\" };\n case \"event\":\n return { type, topic: app?.topics?.[0] ?? \"\", ...(app && !app.own ? { app: app.name } : {}) };\n case \"action\":\n return { type, action: app?.actions?.[0]?.name ?? \"\", ...(app && !app.own ? { app: app.name } : {}) };\n case \"record\":\n return { type, ...(app && !app.own ? { app: app.name } : {}) };\n case \"automation\":\n return { type, key: other?.[0]?.key ?? \"\", when: \"done\" };\n default:\n return { type };\n }\n}\n\n/** The ways, as a list of options for a Select: what is available, and the current one even if it is not. */\nfunction optionsOf(choices, current) {\n const list = choices.filter((c) => c.available);\n if (current && !list.some((c) => c.type === current)) list.push({ type: current, label: triggerTitle(current), hint: \"\", available: true });\n return list.map((c) => ({ value: c.type, label: c.label }));\n}\n\nfunction Schedule({ uid, trigger, readOnly, onChange }) {\n const parts = partsFromCron(trigger.cron ?? \"0 7 * * 1-5\");\n const apply = (next) => onChange({ type: \"schedule\", cron: cronFromParts({ ...parts, ...next }), tz: trigger.tz ?? \"team\" });\n const words = cronWords(trigger.cron);\n const team = !trigger.tz || trigger.tz === \"team\";\n return (\n <>\n <Field id={`${uid}-freq`} label=\"How often\">\n <Select id={`${uid}-freq`} value={parts.freq} disabled={readOnly} options={FREQS} onChange={(e) => apply({ freq: e.target.value })} />\n </Field>\n {parts.freq === \"weekly\" ? (\n <Field id={`${uid}-day`} label=\"On\">\n <Select id={`${uid}-day`} value={String(parts.weekday)} disabled={readOnly} options={WEEKDAYS.map((d, i) => ({ value: String(i), label: d }))} onChange={(e) => apply({ weekday: Number(e.target.value) })} />\n </Field>\n ) : null}\n {parts.freq === \"daily\" || parts.freq === \"weekdays\" || parts.freq === \"weekly\" ? (\n <Field id={`${uid}-time`} label=\"At\">\n <TimeInput\n id={`${uid}-time`}\n value={timeText(parts.hour, parts.minute)}\n disabled={readOnly}\n onChange={(e) => {\n const t = parseTime(e.target.value);\n if (t) apply(t);\n }}\n />\n </Field>\n ) : null}\n {parts.freq === \"hourly\" ? (\n <Field id={`${uid}-min`} label=\"Minutes past the hour\">\n <Input id={`${uid}-min`} type=\"number\" min={0} max={59} inputMode=\"numeric\" value={parts.minute} disabled={readOnly} onChange={(e) => apply({ minute: Number(e.target.value) || 0 })} />\n </Field>\n ) : null}\n {parts.freq === \"custom\" ? (\n <Field id={`${uid}-cron`} label=\"Schedule\" hint=\"Five fields: minute, hour, day, month, weekday.\">\n <Input id={`${uid}-cron`} className=\"font-mono text-[13px]\" value={trigger.cron} disabled={readOnly} placeholder=\"0 7 * * 1-5\" onChange={(e) => onChange({ type: \"schedule\", cron: e.target.value, tz: trigger.tz })} />\n </Field>\n ) : null}\n <Switch id={`${uid}-team`} checked={team} disabled={readOnly} label=\"In the team's time zone\" onChange={(e) => onChange({ type: \"schedule\", cron: trigger.cron, tz: e.target.checked ? \"team\" : \"UTC\" })} />\n <p className=\"m-0 -mt-2 text-xs text-muted-foreground\">07:00 stays 07:00 through daylight saving. Turn off to name another zone.</p>\n {!team ? (\n <Field id={`${uid}-tz`} label=\"Time zone\">\n <Input id={`${uid}-tz`} className=\"font-mono text-[13px]\" value={trigger.tz} disabled={readOnly} placeholder=\"America/New_York\" onChange={(e) => onChange({ type: \"schedule\", cron: trigger.cron, tz: e.target.value })} />\n </Field>\n ) : null}\n <p className=\"m-0 text-xs text-muted-foreground\">{words ? `Runs ${words.charAt(0).toLowerCase()}${words.slice(1)}${team ? \", team time\" : \"\"}.` : \"That schedule cannot be read yet.\"}</p>\n </>\n );\n}\n\n/** Which app a team automation hears: one picked from the platform's list. An app's own has no choice. */\nfunction AppField({ uid, trigger, apps, ownedByApp, readOnly, onApp, required }) {\n if (ownedByApp || apps.length === 0) return null;\n return (\n <Field id={`${uid}-app`} label=\"In which app\" hint={required ? undefined : \"Leave it open to hear any app.\"}>\n <Select\n id={`${uid}-app`}\n value={trigger.app ?? \"\"}\n disabled={readOnly}\n options={[{ value: \"\", label: required ? \"Pick an app\" : \"Any app\" }, ...apps.map((a) => ({ value: a.name, label: a.name }))]}\n onChange={(e) => onApp(e.target.value)}\n />\n </Field>\n );\n}\n\nfunction Webhook({ uid, hook, onRotate, readOnly, origin }) {\n const [shown, setShown] = useState(null);\n const [asking, setAsking] = useState(false);\n const [busy, setBusy] = useState(false);\n const [note, setNote] = useState(null);\n if (!hook) {\n return <p className=\"m-0 text-xs text-muted-foreground\">Save it first. Its web address is made when it exists, and the secret after that.</p>;\n }\n const base = (origin ?? (typeof window !== \"undefined\" ? window.location.origin : \"\")).replace(/\\/$/, \"\");\n const address = `${base}${hook.path}`;\n const make = async () => {\n setBusy(true);\n setNote(null);\n try {\n const r = await onRotate?.();\n if (r && r.ok === false) setNote(r.error || \"That did not work.\");\n else if (r?.hook?.secret) setShown(r.hook.secret);\n setAsking(false);\n } catch (e) {\n setNote(e?.message || \"That did not work.\");\n } finally {\n setBusy(false);\n }\n };\n return (\n <>\n <Field id={`${uid}-addr`} label=\"Its web address\" hint=\"Post to it with JSON. What it receives is on the run as trigger.payload.body.\">\n <Input id={`${uid}-addr`} readOnly className=\"font-mono text-[12.5px]\" value={address} onFocus={(e) => e.target.select()} />\n </Field>\n {shown ? (\n <Field id={`${uid}-secret`} label=\"Its secret\" hint=\"Shown once: copy it now. Sign the body with it (x-makersclaw-signature), or add ?token=… to the address when a sender cannot sign.\">\n <Input id={`${uid}-secret`} readOnly className=\"font-mono text-[12.5px]\" value={shown} onFocus={(e) => e.target.select()} />\n </Field>\n ) : (\n <p className=\"m-0 text-xs text-muted-foreground\">\n {hook.has_secret ? \"It has a secret, and only the sender who holds it can start a run.\" : \"It has no secret yet, so nothing can start it.\"}\n {hook.last_received_at ? \" Last called \" + new Date(hook.last_received_at).toLocaleString() + \".\" : \"\"}\n </p>\n )}\n {!readOnly ? (\n asking ? (\n <div className=\"grid gap-2 rounded-md border border-border p-2.5\">\n <p className=\"m-0 text-xs\">The old secret stops working at once. Anything that still uses it will be refused.</p>\n <div className=\"flex gap-2\">\n <Button size=\"sm\" variant=\"danger\" onClick={make} disabled={busy}>\n Make a new secret\n </Button>\n <Button size=\"sm\" variant=\"secondary\" onClick={() => setAsking(false)} disabled={busy}>\n Keep the old one\n </Button>\n </div>\n </div>\n ) : (\n <Button size=\"sm\" variant=\"secondary\" className=\"justify-self-start\" icon=\"key\" onClick={() => (hook.has_secret ? setAsking(true) : make())} disabled={busy}>\n {hook.has_secret ? \"Make a new secret\" : \"Make a secret\"}\n </Button>\n )\n ) : null}\n {note ? (\n <p role=\"alert\" className=\"m-0 text-xs text-destructive\">\n {note}\n </p>\n ) : null}\n </>\n );\n}\n\n/**\n * trigger the definition's trigger (see the list at the top)\n * onChange(trigger)\n * readOnly shown, not editable\n * onClose draws the close button\n * catalog what the platform offers (the list door's `triggers`):\n * { choices: [{ type, label, hint, available, why? }],\n * apps: [{ name, actions: [{ name, description }], kinds, topics }],\n * automations: [{ key, name }] }. Without it: by hand and a\n * schedule, and `later` listed as \"soon\".\n * ownedByApp the automation belongs to an app: its actions, kinds and\n * topics are that app's (the first of catalog.apps)\n * selfKey this automation's key: it cannot follow itself\n * hook a webhook trigger's address once the automation is saved:\n * { path, has_secret, rotated_at, last_received_at }\n * onRotateHook async () => { ok, hook: { secret } } | { ok: false, error }\n * origin where the address lives (default: this page's origin)\n * later the not-yet ways to start without a catalog, [{ id, label, hint }]\n */\nexport function TriggerPicker({ trigger, onChange, readOnly = false, onClose, catalog = null, ownedByApp = false, selfKey = null, hook = null, onRotateHook, origin, later }) {\n const uid = useId();\n const current = trigger ?? { type: \"manual\" };\n const type = current.type;\n const choices = catalog?.choices?.length ? catalog.choices : FALLBACK_CHOICES;\n const apps = catalog?.apps ?? [];\n const others = (catalog?.automations ?? []).filter((a) => a.key !== selfKey);\n const scope = ownedByApp ? apps[0] : apps.find((a) => a.name === current.app);\n const scoped = scope ? { ...scope, own: ownedByApp } : null;\n const soon = catalog ? choices.filter((c) => !c.available) : (later ?? LATER).map((l) => ({ type: l.id, label: l.label, hint: l.hint, available: false }));\n const switchTo = (next) => onChange(startOf(next, { app: ownedByApp ? scoped : apps.length === 1 ? { ...apps[0], own: false } : null, other: others }));\n const setApp = (name) => {\n const { app: _drop, ...rest } = current;\n onChange(name ? { ...rest, app: name } : rest);\n };\n\n return (\n <SidePanel glyph={<Glyph name={triggerGlyph(type)} size={20} />} title=\"What starts it\" subtitle={triggerWords(current)} onClose={onClose} label=\"What starts it\">\n <Field id={`${uid}-type`} label=\"It starts\">\n <Select id={`${uid}-type`} value={type} disabled={readOnly} options={optionsOf(choices, type)} onChange={(e) => switchTo(e.target.value)} />\n </Field>\n <p className=\"m-0 -mt-2 text-xs text-muted-foreground\">{choices.find((c) => c.type === type)?.hint ?? \"\"}</p>\n\n {type === \"manual\" ? <p className=\"m-0 text-xs text-muted-foreground\">Nothing starts it on its own. Press Run when you want it to go, or let the agent run it.</p> : null}\n\n {type === \"schedule\" ? <Schedule uid={uid} trigger={current} readOnly={readOnly} onChange={onChange} /> : null}\n\n {type === \"event\" ? (\n <>\n <AppField uid={uid} trigger={current} apps={apps} ownedByApp={ownedByApp} readOnly={readOnly} onApp={setApp} required={false} />\n <Field id={`${uid}-topic`} label=\"Event\" hint=\"The name an app sends, like rows.imported. Keyed events (with a key) run once per item.\">\n <Input id={`${uid}-topic`} className=\"font-mono text-[13px]\" list={`${uid}-topics`} value={current.topic ?? \"\"} disabled={readOnly} placeholder=\"rows.imported\" onChange={(e) => onChange({ ...current, topic: e.target.value.trim().toLowerCase() })} />\n <datalist id={`${uid}-topics`}>{(scoped?.topics ?? apps.flatMap((a) => a.topics ?? [])).map((t) => <option key={t} value={t} />)}</datalist>\n </Field>\n </>\n ) : null}\n\n {type === \"action\" ? (\n <>\n <AppField uid={uid} trigger={current} apps={apps} ownedByApp={ownedByApp} readOnly={readOnly} onApp={(n) => onChange({ type: \"action\", action: \"\", ...(n ? { app: n } : {}) })} required />\n <Field id={`${uid}-action`} label=\"Action\" hint=\"Runs when a person or the app's agent has done it. An action another automation ran does not start this one.\">\n {scoped?.actions?.length ? (\n <Select\n id={`${uid}-action`}\n value={current.action ?? \"\"}\n disabled={readOnly}\n options={[{ value: \"\", label: \"Pick an action\" }, ...scoped.actions.map((a) => ({ value: a.name, label: a.name.replace(/_/g, \" \") }))]}\n onChange={(e) => onChange({ ...current, action: e.target.value })}\n />\n ) : (\n <Input id={`${uid}-action`} className=\"font-mono text-[13px]\" value={current.action ?? \"\"} disabled={readOnly} placeholder=\"add_people\" onChange={(e) => onChange({ ...current, action: e.target.value.trim() })} />\n )}\n </Field>\n </>\n ) : null}\n\n {type === \"record\" ? (\n <>\n <AppField uid={uid} trigger={current} apps={apps} ownedByApp={ownedByApp} readOnly={readOnly} onApp={(n) => onChange({ type: \"record\", ...(n ? { app: n } : {}) })} required />\n <fieldset className=\"m-0 grid min-w-0 gap-1 border-0 p-0\">\n <legend className=\"mb-1 p-0 text-sm font-medium\">Which records</legend>\n {(scoped?.kinds ?? []).map((k) => {\n const picked = current.kinds ?? [];\n return (\n <Checkbox\n key={k}\n id={`${uid}-k-${k}`}\n label={k.replace(/_/g, \" \")}\n checked={picked.includes(k)}\n disabled={readOnly}\n onChange={(e) => {\n const next = e.target.checked ? [...picked, k] : picked.filter((x) => x !== k);\n const { kinds: _drop, ...rest } = current;\n onChange(next.length ? { ...rest, kinds: next } : rest);\n }}\n />\n );\n })}\n <p className=\"m-0 text-xs text-muted-foreground\">{(current.kinds ?? []).length ? \"Only these.\" : \"None picked: every kind of record the owner can read.\"}</p>\n </fieldset>\n <fieldset className=\"m-0 grid min-w-0 gap-1 border-0 p-0\">\n <legend className=\"mb-1 p-0 text-sm font-medium\">When it is</legend>\n {[\n [\"create\", \"Added\"],\n [\"update\", \"Changed\"],\n ].map(([op, label]) => {\n const ops = current.ops ?? [\"create\", \"update\"];\n return (\n <Checkbox\n key={op}\n id={`${uid}-op-${op}`}\n label={label}\n checked={ops.includes(op)}\n disabled={readOnly || (ops.length === 1 && ops[0] === op)}\n onChange={(e) => {\n const next = e.target.checked ? [...ops, op] : ops.filter((x) => x !== op);\n const { ops: _drop, ...rest } = current;\n onChange(next.length === 2 ? rest : { ...rest, ops: next });\n }}\n />\n );\n })}\n </fieldset>\n <p className=\"m-0 text-xs text-muted-foreground\">A person, a screen or the app's agent made the change; the app's own jobs do not start it. One run for each record, at most once a minute.</p>\n </>\n ) : null}\n\n {type === \"webhook\" ? <Webhook uid={uid} hook={hook} onRotate={onRotateHook} readOnly={readOnly} origin={origin} /> : null}\n\n {type === \"automation\" ? (\n <>\n <Field id={`${uid}-key`} label=\"Another automation\" hint=\"It runs after that one has finished, with how it went.\">\n <Select\n id={`${uid}-key`}\n value={current.key ?? \"\"}\n disabled={readOnly}\n options={[{ value: \"\", label: others.length ? \"Pick one\" : \"No other automation yet\" }, ...others.map((a) => ({ value: a.key, label: a.name }))]}\n onChange={(e) => onChange({ ...current, key: e.target.value })}\n />\n </Field>\n <Field id={`${uid}-when`} label=\"When it\">\n <Select\n id={`${uid}-when`}\n value={current.when ?? \"done\"}\n disabled={readOnly}\n options={[\n { value: \"done\", label: \"finishes well\" },\n { value: \"failed\", label: \"fails\" },\n { value: \"any\", label: \"ends either way\" },\n ]}\n onChange={(e) => onChange({ ...current, when: e.target.value })}\n />\n </Field>\n <p className=\"m-0 text-xs text-muted-foreground\">Two automations cannot start each other: the platform refuses a loop when you save.</p>\n </>\n ) : null}\n\n {soon.length ? (\n <ul className={cx(\"m-0 grid list-none gap-1.5 border-t border-border p-0 pt-3\")} aria-label=\"Coming later\">\n {soon.map((l) => (\n <li key={l.type} className=\"flex items-start justify-between gap-2 text-sm text-muted-foreground\">\n <span className=\"grid gap-0.5\">\n <span>{l.label}</span>\n {l.why || l.hint ? <span className=\"text-xs\">{l.why || l.hint}</span> : null}\n </span>\n <span className=\"shrink-0 rounded-full border border-border px-1.5 text-2xs\">soon</span>\n </li>\n ))}\n </ul>\n ) : null}\n </SidePanel>\n );\n}\n"
24
+ },
25
+ {
26
+ "path": "registry/blocks/trigger-picker/README.md",
27
+ "type": "registry:file",
28
+ "target": "@components/makersclaw/blocks/trigger-picker/README.md",
29
+ "content": "# TriggerPicker\n\nWhat starts an automation, as a panel. Seven ways, and the platform says\nwhich of them it runs:\n\n- **By hand**: someone presses Run, or the agent runs it.\n- **On a schedule**: every day, every weekday, every week, every hour, or\n another schedule written as a five-field cron, with a time of day (or the\n minute past the hour, or the weekday) and a sentence that says it back:\n \"Runs weekdays at 07:00, team time.\" **The team's time zone** is the\n default, and it is the point: 07:00 stays 07:00 through daylight saving,\n whoever is looking. A schedule the picker cannot read back stays the\n custom cron it was.\n- **When an app sends an event**: a topic like `rows.imported`. A team\n automation may name the app it hears. Events that carry a `key` run once\n per item; events without one run at most once in five minutes.\n- **When an app action finishes**: one of the app's declared actions, done\n by a person or by the app's agent. An action another automation ran does\n not start it (that would loop); \"after another automation\" is for that.\n- **When a record is added or changed**: in the company brain, for the\n kinds picked (none picked: every kind the owner can read), added and/or\n changed. One run for each record, looked at once a minute. Changes the\n app's own jobs make do not start it.\n- **When its web address is called**: the automation's own address, which\n any system can post JSON to. The panel makes the secret, shows it once,\n and replaces it on request (the old one stops at once). The sender signs\n the body with it, or adds `?token=<secret>` to the address when it cannot\n sign.\n- **When another automation finishes**: done, failed, or either. The\n platform refuses a loop when the automation is saved.\n\nA connection's own events (a new mail, a new row in a sheet) are listed as\n**soon**, with what to use until then.\n\n```jsx\nimport { TriggerPicker } from \"../components/blocks/trigger-picker\"; // a Makersclaw app\nimport { TriggerPicker } from \"@/components/makersclaw/blocks/trigger-picker\"; // a shadcn project\n```\n\n`makersclaw add trigger-picker` lays `automation-model` with it. A client\ncomponent. It is a panel (a head, a body): place it beside or over the\ncanvas. `automation-editor` does that when the trigger card is pressed, and\npasses it what the hooks give it.\n\n## Props\n\n- `trigger`: `{ type: \"manual\" }`, `{ type: \"schedule\", cron, tz }`,\n `{ type: \"event\", topic, app? }`, `{ type: \"action\", action, app? }`,\n `{ type: \"record\", kinds?, ops?, app? }`, `{ type: \"webhook\" }` or\n `{ type: \"automation\", key, when }` (`tz` is `\"team\"` or an IANA zone).\n- `onChange(trigger)`: a whole new trigger, ready to put into the\n definition.\n- `catalog`: what the platform offers, `useAutomations().triggers`:\n `{ choices: [{ type, label, hint, available, why? }], apps: [{ name,\n actions: [{ name, description }], kinds, topics }], automations: [{ key,\n name }] }`. Without it the panel offers by hand and a schedule and lists\n `later` as \"soon\" (a platform from before the other triggers).\n- `ownedByApp`: the automation belongs to an app, so its actions, kinds and\n topics are that app's (the first of `catalog.apps`); a team automation\n picks an app.\n- `selfKey`: this automation's key, which it cannot follow.\n- `hook`: a webhook trigger's address once the automation is saved,\n `useAutomation(id).hook`: `{ path, has_secret, rotated_at, last_received_at }`.\n- `onRotateHook()`: makes or replaces the secret; resolves `{ ok: true,\n hook: { secret } }` (`useAutomation(id).rotateHook`).\n- `origin`: where the address lives (default: this page's origin).\n- `readOnly`: shown, not editable.\n- `onClose`: draws the close button.\n- `later`: without a `catalog`, the ways to start that are not built yet,\n `[{ id, label, hint }]`.\n\n## Never\n\n- Never show a schedule in the viewer's time zone: say it in the\n automation's.\n- Never offer a trigger the platform does not run without saying \"soon\".\n- Never keep the webhook's secret anywhere after it has been shown: the\n platform does not send it again.\n\n## Example\n\n```jsx\n\"use client\";\nimport { useState } from \"react\";\nimport { useAutomation, useAutomations } from \"../lib/hooks\";\nimport { TriggerPicker } from \"../components/blocks/trigger-picker\";\n\nexport default function WhenItRuns({ id }) {\n const { triggers } = useAutomations();\n const { automation, definition, hook, rotateHook } = useAutomation(id);\n const [trigger, setTrigger] = useState(definition?.trigger ?? { type: \"manual\" });\n return (\n <div className=\"max-w-sm overflow-hidden rounded-lg border border-border\">\n <TriggerPicker trigger={trigger} onChange={setTrigger} catalog={triggers} ownedByApp selfKey={automation?.key} hook={hook} onRotateHook={rotateHook} />\n </div>\n );\n}\n```\n"
30
+ }
31
+ ],
32
+ "categories": [
33
+ "automation",
34
+ "workflow",
35
+ "trigger",
36
+ "schedule",
37
+ "cron",
38
+ "time",
39
+ "when",
40
+ "every day",
41
+ "weekday",
42
+ "time zone",
43
+ "manual",
44
+ "n8n",
45
+ "make",
46
+ "event",
47
+ "webhook",
48
+ "record",
49
+ "action",
50
+ "web address",
51
+ "secret",
52
+ "follow"
53
+ ],
54
+ "docs": "Installed to components/makersclaw/blocks/trigger-picker. Import: import { TriggerPicker } from \"@/components/makersclaw/blocks/trigger-picker\". Props and an example: components/makersclaw/blocks/trigger-picker/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
55
+ "meta": {
56
+ "makersclaw": {
57
+ "version": "0.8.1",
58
+ "useWhen": "An automation's trigger is edited in your own screen. automation-editor opens it when the trigger card is pressed; pass it the platform's triggers (useAutomations().triggers) so it offers what the platform runs."
59
+ }
60
+ }
61
+ }
package/r/whiteboard.json CHANGED
@@ -5,8 +5,8 @@
5
5
  "title": "Whiteboard",
6
6
  "description": "A full-page freeform whiteboard on sketch-canvas: a top bar with the board's title (click to rename), the save state (Saving…, Saved, or the error with Retry), a Board / Text view switch, Export PNG and an optional Share button; under it the Miro-style canvas (tool rail, floating toolbar, zoom, undo). An empty board offers starter templates — blank, a grid of titled areas, a 2×2 matrix — and the Text view lists every word on the board for a screen reader or an agent. With onCreateTask, a selected note or text becomes a task from the top bar (\"Create task\", editable title; several notes make one task each), and a small badge marks it on the board from then on. The sketch is plain JSON; saves are async callbacks answering { ok }.",
7
7
  "registryDependencies": [
8
- "https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/core.json",
9
- "https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/sketch-canvas.json"
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json",
9
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/sketch-canvas.json"
10
10
  ],
11
11
  "dependencies": [],
12
12
  "files": [
@@ -59,7 +59,7 @@
59
59
  "docs": "Installed to components/makersclaw/blocks/whiteboard. Import: import { Whiteboard, WHITEBOARD_TEMPLATES, gridSketch, matrixSketch } from \"@/components/makersclaw/blocks/whiteboard\". Props and an example: components/makersclaw/blocks/whiteboard/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
60
60
  "meta": {
61
61
  "makersclaw": {
62
- "version": "0.8.0",
62
+ "version": "0.8.1",
63
63
  "useWhen": "A screen is one board to think on: a team whiteboard, a planning or prioritisation board, a place to sketch an idea and keep it with a record. Not for marking up a screenshot (annotate), sketching a UI to build (sketch-to-app), a structured board of cards and connectors (flow-canvas), or a kanban (board)."
64
64
  }
65
65
  }
@@ -0,0 +1,55 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "workflow-view",
4
+ "type": "registry:block",
5
+ "title": "WorkflowView",
6
+ "description": "One job of the app's agent drawn as a graph on a dotted ground: the Trigger (\"Every weekday at 07:00\", \"When a deal is created\") → each step of the chosen run with how it went, as a mark and a word → the result. Over it a run picker with Re-run; in its head the Live switch and \"Trigger manually\" with its price; beside it the run history and the overview (run-history). Before any run it draws the job's declared steps in pencil, with an honest empty line. Fed by useJobs, useRuns and useRun (lib/hooks.js). Under about 768 px of its own width the history stacks under the graph and the graph runs top to bottom.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json",
9
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/run-history.json"
10
+ ],
11
+ "dependencies": [],
12
+ "files": [
13
+ {
14
+ "path": "registry/blocks/workflow-view/index.js",
15
+ "type": "registry:component",
16
+ "target": "@components/makersclaw/blocks/workflow-view/index.js",
17
+ "content": "// The WorkflowView block — `makersclaw add workflow-view`. Fixed: compose\n// it through its props (README.md beside this file).\nexport { WorkflowView } from \"./workflow-view\";\n"
18
+ },
19
+ {
20
+ "path": "registry/blocks/workflow-view/workflow-view.jsx",
21
+ "type": "registry:component",
22
+ "target": "@components/makersclaw/blocks/workflow-view/workflow-view.jsx",
23
+ "content": "\"use client\";\n\n// A job drawn as a graph on a dotted ground: its Trigger, each step of the\n// chosen run with how it went, and the result; over it the run picker and\n// Re-run; in its head the Live switch and \"Trigger manually\" with its\n// price; beside it the run history and the overview (the sibling\n// run-history block). Before any run it draws the job's declared steps\n// (`plan`) in pencil (dashed), with an honest line, so a job not set up\n// still has something to look at.\n//\n// Props in, callbacks out; it reads nothing and formats nothing. Tokens\n// only. Every status is a mark AND a word. Its own width decides its\n// layout (@container): under about 768 px the history and the overview\n// stack under the graph. The graph always runs top to bottom, each step a\n// full-width node with its whole title: it never scrolls sideways and\n// never cuts a title (a left-to-right strip did both at a desk window's\n// width once a job had six steps: the CRM, 2026-10-11).\n\nimport { Button, Icon, Select, Switch, cx } from \"../../ui\";\nimport { RunHistory, RunOverview } from \"../run-history\";\n\nconst MARK = {\n done: { icon: \"circle-check\", tone: \"text-success\", ring: \"border-success\", word: \"Completed\" },\n ok: { icon: \"circle-check\", tone: \"text-success\", ring: \"border-success\", word: \"Completed\" },\n complete: { icon: \"circle-check\", tone: \"text-success\", ring: \"border-success\", word: \"Completed\" },\n failed: { icon: \"circle-x\", tone: \"text-destructive\", ring: \"border-destructive\", word: \"Failed\" },\n running: { icon: \"loader\", tone: \"text-info\", ring: \"border-info\", word: \"Running\" },\n needs_you: { icon: \"circle-help\", tone: \"text-warning\", ring: \"border-warning\", word: \"Needs you\" },\n stopped: { icon: \"stop\", tone: \"text-muted-foreground\", ring: \"border-border\", word: \"Stopped\" },\n skipped: { icon: \"minus\", tone: \"text-muted-foreground\", ring: \"border-border\", word: \"Skipped\" },\n waiting: { icon: \"circle-dashed\", tone: \"text-muted-foreground\", ring: \"border-border\", word: \"Not reached\" },\n planned: { icon: \"pencil\", tone: \"text-muted-foreground\", ring: \"border-border\", word: \"Not run yet\" },\n};\nconst markOf = (s) => MARK[s] ?? MARK.waiting;\n\n/** The dotted ground: the theme's muted ink, faded by the layer's\n * opacity, so it reads on a light theme and a dark one alike. */\nconst GROUND = { backgroundImage: \"radial-gradient(var(--color-muted-foreground) 1px, transparent 1.5px)\", backgroundSize: \"16px 16px\" };\n\nfunction StatusWord({ status }) {\n const m = markOf(status);\n return (\n <span className={cx(\"inline-flex items-center gap-1 text-[11.5px]\", m.tone)}>\n <Icon name={m.icon} size={12} className={status === \"running\" ? \"animate-spin motion-reduce:animate-none\" : undefined} />\n {m.word}\n </span>\n );\n}\n\n/** One node: a kicker over it (Trigger, Step 2, Result), the label, a\n * detail, its status. `pencil` draws it dashed: planned, not run. */\nfunction Node({ kicker, icon, label, detail, status, pencil = false }) {\n const m = status ? markOf(status) : null;\n return (\n <div className=\"grid w-full min-w-0 gap-1\">\n <div className=\"flex min-w-0 items-center justify-between gap-2 text-[11.5px] text-muted-foreground\">\n <span className=\"truncate\">{kicker}</span>\n {status ? <StatusWord status={status} /> : null}\n </div>\n <div\n className={cx(\n \"grid min-w-0 gap-0.5 rounded-[var(--radius)] border bg-card px-3 py-2.5 text-card-foreground\",\n pencil ? \"border-dashed border-muted-foreground bg-transparent\" : m && status !== \"waiting\" && status !== \"skipped\" ? m.ring : \"border-border\",\n )}\n >\n <span className=\"flex min-w-0 items-start gap-2\">\n {icon ? <Icon name={icon} size={14} className=\"mt-0.5 shrink-0 text-muted-foreground\" /> : null}\n <b className={cx(\"min-w-0 break-words text-[13px] font-semibold\", pencil && \"font-medium text-muted-foreground\")}>{label}</b>\n </span>\n {detail ? <span className=\"min-w-0 break-words text-xs text-muted-foreground\">{detail}</span> : null}\n </div>\n </div>\n );\n}\n\n/** The line between two nodes, going down. */\nfunction Link({ pencil }) {\n return (\n <span aria-hidden=\"true\" className=\"flex shrink-0 justify-start pl-6\">\n <span className={cx(\"block h-5 w-0 border-l\", pencil ? \"border-dashed border-muted-foreground\" : \"border-border\")} />\n </span>\n );\n}\n\n/**\n * title the job's name (\"Morning read\")\n * trigger { label: \"Every weekday at 07:00\", detail: \"Starts on its own\" }\n * steps the chosen run's steps: [{ label, status, detail? }]\n * status: done|failed|running|needs_you|stopped|skipped|waiting\n * result { label: \"Wrote 4 people, 1 deal move\", detail, status }\n * runs RunHistory's rows ({ id, status, title, line, when, cost })\n * selected the run drawn; onSelect(run)\n * overview RunOverview's props, or null\n * live { on, onToggle(next), busy } or null (no switch)\n * run { label: \"Trigger manually\", price: \"about 700 cr\", onRun, loading, disabled, why } or null\n * rerun { onRun, loading, label? } or null: Re-run the chosen run\n * plan the job's declared steps, drawn in pencil before any run\n * empty the honest line before any run\n */\nexport function WorkflowView({\n title,\n trigger,\n steps = [],\n result,\n runs = [],\n selected,\n onSelect,\n overview,\n live = null,\n run = null,\n rerun = null,\n plan = null,\n empty,\n className,\n}) {\n const hasRuns = runs.length > 0;\n const chosen = runs.find((r) => r.id === selected) ?? null;\n const pencil = !hasRuns || (!steps.length && Boolean(plan?.length));\n const drawn = pencil ? (plan ?? []).map((label) => ({ label, status: \"planned\" })) : steps;\n\n return (\n <section aria-label={title} className={cx(\"@container grid min-w-0 overflow-hidden rounded-[var(--radius)] border border-border bg-background text-foreground\", className)}>\n <header className=\"flex min-w-0 flex-wrap items-center gap-x-3 gap-y-2 border-b border-border px-3 py-2\">\n <b className=\"min-w-[10rem] flex-1 truncate text-sm font-semibold\">{title}</b>\n {live ? (\n <Switch\n checked={Boolean(live.on)}\n disabled={Boolean(live.busy)}\n onChange={(e) => live.onToggle?.(Boolean(e?.target?.checked))}\n label={live.on ? \"Live\" : \"Off\"}\n className=\"-my-1.5 text-xs\"\n />\n ) : null}\n {run ? (\n <Button size=\"sm\" icon=\"play\" loading={run.loading} disabled={run.disabled} onClick={run.onRun} title={run.why} className=\"max-w-full\">\n <span className=\"truncate\">{run.label ?? \"Trigger manually\"}</span>\n {run.price ? <span className=\"font-normal text-muted-foreground\">{run.price}</span> : null}\n </Button>\n ) : null}\n </header>\n\n <div className=\"grid min-w-0 @3xl:grid-cols-[minmax(0,1fr)_300px]\">\n <div className=\"relative isolate grid min-w-0 content-start gap-4 p-3 @md:p-4\">\n <span aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0 -z-10 opacity-30\" style={GROUND} />\n {hasRuns ? (\n <div className=\"flex min-w-0 max-w-full flex-wrap items-center gap-2 justify-self-start rounded-[var(--radius)] border border-border bg-card p-1.5 text-card-foreground\">\n <span className=\"block w-72 max-w-full\">\n <Select\n aria-label=\"The run shown\"\n value={chosen?.id ?? \"\"}\n onChange={(e) => {\n const next = runs.find((r) => r.id === e.target.value);\n if (next) onSelect?.(next);\n }}\n options={runs.map((r) => ({ value: r.id, label: r.title }))}\n className=\"min-h-9 text-[13px]\"\n />\n </span>\n {rerun ? (\n <Button size=\"sm\" variant=\"quiet\" icon=\"rotate-cw\" loading={rerun.loading} disabled={rerun.disabled} onClick={rerun.onRun}>\n {rerun.label ?? \"Re-run\"}\n </Button>\n ) : null}\n </div>\n ) : null}\n\n <ol aria-label={pencil ? \"The steps it will take\" : \"The steps of this run\"} className=\"m-0 flex min-w-0 max-w-xl list-none flex-col items-stretch p-0\">\n <li className=\"flex min-w-0 flex-col\">\n <Node kicker=\"Trigger\" icon=\"zap\" label={trigger?.label ?? \"When it starts\"} detail={trigger?.detail} pencil={pencil && !trigger} />\n </li>\n {drawn.map((s, i) => (\n <li key={`${s.label}-${i}`} className=\"flex min-w-0 flex-col\">\n <Link pencil={pencil} />\n <Node kicker={`Step ${i + 1}`} label={s.label} detail={s.detail} status={s.status} pencil={pencil} />\n </li>\n ))}\n {result || pencil ? (\n <li className=\"flex min-w-0 flex-col\">\n <Link pencil={pencil} />\n <Node\n kicker=\"Result\"\n icon=\"flag\"\n label={result?.label ?? \"What it wrote\"}\n detail={result?.detail}\n status={pencil ? undefined : result?.status}\n pencil={pencil}\n />\n </li>\n ) : null}\n </ol>\n\n {pencil ? <p className=\"m-0 max-w-prose rounded-[var(--radius)] bg-background px-2 py-1 text-[13px] text-muted-foreground justify-self-start\">{empty ?? \"No runs yet.\"}</p> : null}\n </div>\n\n <aside aria-label=\"Runs\" className=\"grid min-w-0 content-start border-t border-border bg-card text-card-foreground @3xl:border-l @3xl:border-t-0\">\n <h3 className=\"m-0 px-3.5 pb-1 pt-3 text-sm font-semibold\">Run history</h3>\n <RunHistory runs={runs} selected={selected} onSelect={onSelect} label=\"Run history\" empty={hasRuns ? undefined : \"No runs yet.\"} />\n {overview ? <RunOverview {...overview} className={cx(\"p-3.5\", overview.className)} /> : null}\n </aside>\n </div>\n </section>\n );\n}\n"
24
+ },
25
+ {
26
+ "path": "registry/blocks/workflow-view/README.md",
27
+ "type": "registry:file",
28
+ "target": "@components/makersclaw/blocks/workflow-view/README.md",
29
+ "content": "# WorkflowView\n\n> **Superseded** (0.8.1, the automations engine, D26). One job drawn top to bottom, from `useJobs`, `useRuns` and `useRun`. For an app whose jobs run on the automations engine, use `automation-editor` (a node canvas a member can edit, test and switch on) and `run-view` (one run on the same canvas, the path taken lit). Keep using this block for an app that has not moved: it works as it did.\n\n\nOne job of the app's agent, drawn as a graph on a dotted ground. Part of\nthe agent kit (0.8.1): the app shows its agent's work itself; the\nwindow's Runs tab stays as the full history.\n\n- **The graph**: the Trigger (\"Every weekday at 07:00\", \"When a deal is\n created\", \"Run by Owen\") → each step of the chosen run, with how it\n went as a mark and a word → the result (what it wrote, what needs\n you).\n- **Over it**: the run picker (\"Run #7 · Tue 29 Sep, 07:02\") and Re-run.\n- **In its head**: the Live switch and \"Trigger manually · about 700 cr\".\n- **Beside it**: the run history and the overview (the `run-history`\n block's RunHistory and RunOverview).\n- **Before any run**: the job's declared steps (`plan`) drawn in pencil,\n dashed, with an honest line (\"No runs yet. The first runs Mon 12 Oct,\n 07:00, once a mailbox is connected.\"). A job not set up still has\n something to look at.\n\n```jsx\nimport { WorkflowView } from \"../components/blocks/workflow-view\"; // a Makersclaw app\nimport { WorkflowView } from \"@/components/makersclaw/blocks/workflow-view\"; // a shadcn project\n```\n\nA client component. `makersclaw add workflow-view` lays `run-history`\nwith it. Its own width decides its layout: under about 768 px the\nhistory and the overview stack under the graph. The graph always runs\ntop to bottom, every step a full-width node with its whole title (a\nleft-to-right strip scrolled sideways and cut titles at a desk window's\nwidth, 2026-10-11). It never scrolls sideways. It formats nothing.\n\n## Props\n\n- `title` — the job's name.\n- `trigger` — `{ label, detail }`: what starts it, in words.\n- `steps` — the chosen run's steps, `[{ label, status, detail? }]`.\n `status` is `done` · `failed` · `running` · `needs_you` · `stopped` ·\n `skipped` · `waiting` (not reached yet). useRun's `steps` pass as\n they are.\n- `result` — `{ label, detail, status }`: what the run wrote, or what\n waits on the member.\n- `runs` — RunHistory's rows (`{ id, status, title, line, when, cost }`),\n newest first. `selected`, `onSelect(run)` — the run drawn; the picker\n and the history both choose it.\n- `overview` — RunOverview's props (`{ completed, failed, running, avg,\n credits }`), or null.\n- `live` — `{ on, onToggle(next), busy }`: the app's Live switch. Null:\n no switch.\n- `run` — `{ label, price, onRun, loading, disabled, why }`: \"Trigger\n manually\" with its price on the button. Null: no button.\n- `rerun` — `{ onRun, loading, label? }`: Re-run beside the picker.\n- `plan` — the job's declared steps (useJobs' `job.plan`), drawn in\n pencil before any run.\n- `empty` — the honest line before any run: when it will first run, and\n what it waits on.\n- `className` — layout around it. Never colour.\n\n## Hooks (lib/hooks.js)\n\n- `useJobs()` — the job (`words`, `plan`, `enabled`), the app's `live`,\n `may`, `runNow(id)`, `pause()`, `resume()`.\n- `useRuns({ job })` — the runs and the overview.\n- `useRun(id)` — the chosen run's `steps`, `text` and `needs`.\n\n## Who may do what\n\n- Any member reads the graph, the runs and the overview (a teammate's\n ask is never among the runs: the door leaves it out).\n- **Trigger manually**, **Re-run** and **Live**: the app's owner, a\n workspace owner or an admin — `may.run` and `may.pause`. Pass `run`,\n `rerun` and `live` only when they are true. The door refuses the\n others anyway; a hidden button is never the guard.\n\n## Never\n\n- Never a press that spends without its price on it.\n- Never an Approve: a step that needs the member says \"Needs you\" and\n the screen opens the desk (`openRequest`).\n- Never a status by colour alone: each comes with its mark and its\n word.\n- Never a steps list made up when the run has none: pass the `plan`, or\n nothing, and the empty line.\n\n## Example\n\n```jsx\n\"use client\";\nimport { useState } from \"react\";\nimport { useJobs, useRun, useRuns } from \"../lib/hooks\";\nimport { WorkflowView } from \"../components/blocks/workflow-view\";\n\nconst at = (iso) => new Date(iso).toLocaleString(\"en-GB\", { weekday: \"short\", day: \"numeric\", month: \"short\", hour: \"2-digit\", minute: \"2-digit\" });\n\nexport default function MorningReadFlow({ jobId }) {\n const { jobs, app, may, busy, appBusy, runNow, pause, resume } = useJobs();\n const job = jobs.find((j) => j.id === jobId);\n const { runs, overview } = useRuns({ job: jobId });\n const [picked, setPicked] = useState(null);\n const selected = picked ?? runs[0]?.id ?? null;\n const { run, steps } = useRun(selected);\n if (!job) return null;\n return (\n <WorkflowView\n title={job.name}\n trigger={{ label: job.words ?? \"On its schedule\", detail: job.does }}\n steps={steps}\n result={run ? { label: run.state === \"needs_you\" ? \"Waiting for your yes on the desk\" : run.report ?? \"Done\", status: run.state } : null}\n runs={runs.map((r) => ({ id: r.id, status: r.state, title: `Run #${r.number}`, when: at(r.started_at) }))}\n selected={selected}\n onSelect={(r) => setPicked(r.id)}\n overview={overview && { completed: overview.completed, failed: overview.failed, running: overview.running, avg: overview.avg_ms == null ? null : `${Math.round(overview.avg_ms / 1000)} s` }}\n live={may.pause ? { on: Boolean(app?.live), busy: Boolean(appBusy), onToggle: (on) => (on ? resume() : pause()) } : null}\n run={may.run ? { label: \"Trigger manually\", price: \"about 700 cr\", onRun: () => runNow(job.id), loading: busy[job.id] === \"run\" } : null}\n rerun={may.run ? { onRun: () => runNow(job.id), loading: busy[job.id] === \"run\" } : null}\n plan={job.plan}\n empty={job.next_run_at ? `No runs yet. The first runs ${at(job.next_run_at)}.` : \"No runs yet. It starts once it is switched on.\"}\n />\n );\n}\n```\n"
30
+ }
31
+ ],
32
+ "categories": [
33
+ "workflow",
34
+ "automation",
35
+ "graph",
36
+ "pipeline",
37
+ "steps",
38
+ "trigger",
39
+ "runs",
40
+ "run history",
41
+ "re-run",
42
+ "live",
43
+ "schedule",
44
+ "agent",
45
+ "nodes",
46
+ "kit"
47
+ ],
48
+ "docs": "Installed to components/makersclaw/blocks/workflow-view. Import: import { WorkflowView } from \"@/components/makersclaw/blocks/workflow-view\". Props and an example: components/makersclaw/blocks/workflow-view/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
49
+ "meta": {
50
+ "makersclaw": {
51
+ "version": "0.8.1",
52
+ "useWhen": "Superseded by automation-editor and run-view for an app whose jobs run on the automations engine; still the block for an app that has not moved (its jobs are the agent's wakes). A job's own page: the founder opens an automation to see what it does, how its last runs went and to run it now — a morning read, an inbox sweep, a nightly enrichment, an event-triggered workflow. For the job as a card beside the day's work use schedule-card; for a plain list of runs use run-history."
53
+ }
54
+ }
55
+ }