makersclaw 0.7.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 (419) hide show
  1. package/README.md +17 -4
  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 +4 -4
  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 +5 -5
  26. package/r/chat-pane.json +84 -0
  27. package/r/chat.json +3 -3
  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 +18 -6
  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 +3 -3
  46. package/r/flow-editor.json +79 -0
  47. package/r/footer.json +3 -3
  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 +4 -4
  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 +4 -4
  60. package/r/pace-bar.json +69 -0
  61. package/r/page-builder.json +3 -3
  62. package/r/payment-methods.json +2 -2
  63. package/r/pdf-document.json +2 -2
  64. package/r/pdf-viewer.json +3 -3
  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 +4089 -1510
  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 +3 -3
  82. package/r/signature-pad.json +2 -2
  83. package/r/sketch-canvas.json +3 -3
  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 +4 -4
  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/annotate/README.md +14 -11
  159. package/registry/blocks/approval/README.md +145 -0
  160. package/registry/blocks/approval/approval.jsx +163 -0
  161. package/registry/blocks/approval/block.json +31 -0
  162. package/registry/blocks/approval/demo.jsx +121 -0
  163. package/registry/blocks/approval/index.js +3 -0
  164. package/registry/blocks/ask-bar/README.md +105 -0
  165. package/registry/blocks/ask-bar/ask-bar.jsx +166 -0
  166. package/registry/blocks/ask-bar/block.json +26 -0
  167. package/registry/blocks/ask-bar/demo.jsx +78 -0
  168. package/registry/blocks/ask-bar/index.js +3 -0
  169. package/registry/blocks/automation-editor/README.md +291 -0
  170. package/registry/blocks/automation-editor/automation-editor.jsx +656 -0
  171. package/registry/blocks/automation-editor/block.json +39 -0
  172. package/registry/blocks/automation-editor/demo.jsx +284 -0
  173. package/registry/blocks/automation-editor/index.js +4 -0
  174. package/registry/blocks/automation-editor/run-inputs.jsx +82 -0
  175. package/registry/blocks/automation-list/README.md +129 -0
  176. package/registry/blocks/automation-list/automation-list.jsx +476 -0
  177. package/registry/blocks/automation-list/block.json +35 -0
  178. package/registry/blocks/automation-list/demo.jsx +67 -0
  179. package/registry/blocks/automation-list/index.js +3 -0
  180. package/registry/blocks/automation-model/README.md +109 -0
  181. package/registry/blocks/automation-model/block.json +135 -0
  182. package/registry/blocks/automation-model/catalog.js +256 -0
  183. package/registry/blocks/automation-model/check.js +161 -0
  184. package/registry/blocks/automation-model/demo.jsx +42 -0
  185. package/registry/blocks/automation-model/edit.js +262 -0
  186. package/registry/blocks/automation-model/fixes.js +287 -0
  187. package/registry/blocks/automation-model/glyph.jsx +13 -0
  188. package/registry/blocks/automation-model/graph.js +410 -0
  189. package/registry/blocks/automation-model/index.js +33 -0
  190. package/registry/blocks/automation-model/overlay.js +175 -0
  191. package/registry/blocks/automation-model/parts.jsx +94 -0
  192. package/registry/blocks/automation-model/schedule.js +53 -0
  193. package/registry/blocks/automation-model/templates.js +128 -0
  194. package/registry/blocks/automation-model/values.js +29 -0
  195. package/registry/blocks/automation-model/words.js +439 -0
  196. package/registry/blocks/automations-screen/README.md +207 -0
  197. package/registry/blocks/automations-screen/automations-screen.jsx +452 -0
  198. package/registry/blocks/automations-screen/block.json +47 -0
  199. package/registry/blocks/automations-screen/demo.jsx +364 -0
  200. package/registry/blocks/automations-screen/index.js +4 -0
  201. package/registry/blocks/automations-screen/screen-model.js +129 -0
  202. package/registry/blocks/chart/cartesian.jsx +1 -1
  203. package/registry/blocks/chart/core.jsx +33 -23
  204. package/registry/blocks/chart/pie.jsx +1 -1
  205. package/registry/blocks/chat/README.md +4 -1
  206. package/registry/blocks/chat/demo.jsx +2 -2
  207. package/registry/blocks/chat-pane/README.md +143 -0
  208. package/registry/blocks/chat-pane/block.json +48 -0
  209. package/registry/blocks/chat-pane/chat-attach.js +166 -0
  210. package/registry/blocks/chat-pane/chat-composer.jsx +195 -0
  211. package/registry/blocks/chat-pane/chat-context.js +17 -0
  212. package/registry/blocks/chat-pane/chat-model.js +43 -0
  213. package/registry/blocks/chat-pane/chat-pane.jsx +398 -0
  214. package/registry/blocks/chat-pane/chats-list.jsx +52 -0
  215. package/registry/blocks/chat-pane/demo.jsx +103 -0
  216. package/registry/blocks/chat-pane/index.js +9 -0
  217. package/registry/blocks/connect-card/README.md +99 -0
  218. package/registry/blocks/connect-card/block.json +10 -0
  219. package/registry/blocks/connect-card/connect-card.jsx +103 -0
  220. package/registry/blocks/connect-card/demo.jsx +49 -0
  221. package/registry/blocks/connect-card/index.js +3 -0
  222. package/registry/blocks/cost/README.md +115 -0
  223. package/registry/blocks/cost/block.json +10 -0
  224. package/registry/blocks/cost/cost.jsx +176 -0
  225. package/registry/blocks/cost/demo.jsx +66 -0
  226. package/registry/blocks/cost/index.js +3 -0
  227. package/registry/blocks/data-picker/README.md +81 -0
  228. package/registry/blocks/data-picker/block.json +38 -0
  229. package/registry/blocks/data-picker/data-picker.jsx +210 -0
  230. package/registry/blocks/data-picker/demo.jsx +34 -0
  231. package/registry/blocks/data-picker/fields.js +89 -0
  232. package/registry/blocks/data-picker/index.js +4 -0
  233. package/registry/blocks/data-table/demo.jsx +1 -1
  234. package/registry/blocks/first-run/README.md +118 -0
  235. package/registry/blocks/first-run/block.json +11 -0
  236. package/registry/blocks/first-run/demo.jsx +88 -0
  237. package/registry/blocks/first-run/first-run-model.js +51 -0
  238. package/registry/blocks/first-run/first-run.jsx +156 -0
  239. package/registry/blocks/first-run/index.js +5 -0
  240. package/registry/blocks/first-run/probe-rules.js +26 -0
  241. package/registry/blocks/fix-note/README.md +79 -0
  242. package/registry/blocks/fix-note/block.json +35 -0
  243. package/registry/blocks/fix-note/demo.jsx +30 -0
  244. package/registry/blocks/fix-note/fix-note.jsx +235 -0
  245. package/registry/blocks/fix-note/index.js +3 -0
  246. package/registry/blocks/flow-canvas/README.md +15 -8
  247. package/registry/blocks/flow-editor/README.md +108 -0
  248. package/registry/blocks/flow-editor/block.json +44 -0
  249. package/registry/blocks/flow-editor/demo.jsx +115 -0
  250. package/registry/blocks/flow-editor/flow-canvas.jsx +568 -0
  251. package/registry/blocks/flow-editor/flow-editor.jsx +154 -0
  252. package/registry/blocks/flow-editor/index.js +6 -0
  253. package/registry/blocks/flow-editor/minimap.jsx +78 -0
  254. package/registry/blocks/flow-editor/outline.jsx +134 -0
  255. package/registry/blocks/footer/README.md +3 -2
  256. package/registry/blocks/library-shelf/README.md +117 -0
  257. package/registry/blocks/library-shelf/block.json +28 -0
  258. package/registry/blocks/library-shelf/demo.jsx +68 -0
  259. package/registry/blocks/library-shelf/index.js +3 -0
  260. package/registry/blocks/library-shelf/library-shelf.jsx +211 -0
  261. package/registry/blocks/library-shelf/probe-rules.js +15 -0
  262. package/registry/blocks/margin-notes/README.md +222 -0
  263. package/registry/blocks/margin-notes/block.json +10 -0
  264. package/registry/blocks/margin-notes/demo.jsx +144 -0
  265. package/registry/blocks/margin-notes/index.js +20 -0
  266. package/registry/blocks/margin-notes/margin-model.js +224 -0
  267. package/registry/blocks/margin-notes/margin-notes.jsx +465 -0
  268. package/registry/blocks/mind-map/README.md +4 -2
  269. package/registry/blocks/needs-you/README.md +177 -0
  270. package/registry/blocks/needs-you/block.json +34 -0
  271. package/registry/blocks/needs-you/demo.jsx +143 -0
  272. package/registry/blocks/needs-you/index.js +3 -0
  273. package/registry/blocks/needs-you/needs-you.jsx +441 -0
  274. package/registry/blocks/org-chart/README.md +4 -2
  275. package/registry/blocks/pace-bar/README.md +230 -0
  276. package/registry/blocks/pace-bar/block.json +10 -0
  277. package/registry/blocks/pace-bar/demo.jsx +255 -0
  278. package/registry/blocks/pace-bar/index.js +22 -0
  279. package/registry/blocks/pace-bar/pace-bar.jsx +461 -0
  280. package/registry/blocks/pace-bar/pace-model.js +288 -0
  281. package/registry/blocks/page-builder/README.md +1 -1
  282. package/registry/blocks/page-builder/demo.jsx +3 -3
  283. package/registry/blocks/pdf-viewer/README.md +4 -1
  284. package/registry/blocks/record-grid/README.md +100 -0
  285. package/registry/blocks/record-grid/block.json +28 -0
  286. package/registry/blocks/record-grid/demo.jsx +43 -0
  287. package/registry/blocks/record-grid/index.js +3 -0
  288. package/registry/blocks/record-grid/record-grid.jsx +251 -0
  289. package/registry/blocks/run-history/README.md +138 -0
  290. package/registry/blocks/run-history/block.json +10 -0
  291. package/registry/blocks/run-history/demo.jsx +74 -0
  292. package/registry/blocks/run-history/index.js +3 -0
  293. package/registry/blocks/run-history/run-history.jsx +218 -0
  294. package/registry/blocks/run-view/README.md +174 -0
  295. package/registry/blocks/run-view/block.json +50 -0
  296. package/registry/blocks/run-view/demo.jsx +286 -0
  297. package/registry/blocks/run-view/index.js +5 -0
  298. package/registry/blocks/run-view/run-controls.jsx +266 -0
  299. package/registry/blocks/run-view/run-view.jsx +236 -0
  300. package/registry/blocks/run-view/runs-list.jsx +99 -0
  301. package/registry/blocks/schedule-card/README.md +121 -0
  302. package/registry/blocks/schedule-card/block.json +10 -0
  303. package/registry/blocks/schedule-card/demo.jsx +82 -0
  304. package/registry/blocks/schedule-card/index.js +3 -0
  305. package/registry/blocks/schedule-card/schedule-card.jsx +136 -0
  306. package/registry/blocks/schedule-engine/README.md +137 -0
  307. package/registry/blocks/schedule-engine/block.json +10 -0
  308. package/registry/blocks/schedule-engine/demo.jsx +82 -0
  309. package/registry/blocks/schedule-engine/engine.js +769 -0
  310. package/registry/blocks/schedule-engine/index.js +9 -0
  311. package/registry/blocks/schedule-engine/ops.js +71 -0
  312. package/registry/blocks/schedule-engine/place.js +294 -0
  313. package/registry/blocks/schedule-engine/tree.js +79 -0
  314. package/registry/blocks/schedule-engine/waits.js +80 -0
  315. package/registry/blocks/schedule-gantt/README.md +159 -0
  316. package/registry/blocks/schedule-gantt/block.json +10 -0
  317. package/registry/blocks/schedule-gantt/demo.jsx +223 -0
  318. package/registry/blocks/schedule-gantt/gantt-model.js +219 -0
  319. package/registry/blocks/schedule-gantt/geometry.js +176 -0
  320. package/registry/blocks/schedule-gantt/index.js +26 -0
  321. package/registry/blocks/schedule-gantt/schedule-gantt.jsx +882 -0
  322. package/registry/blocks/schedule-gantt/sheet.jsx +372 -0
  323. package/registry/blocks/schedule-gantt/track.jsx +190 -0
  324. package/registry/blocks/schedule-ruler/README.md +148 -0
  325. package/registry/blocks/schedule-ruler/block.json +10 -0
  326. package/registry/blocks/schedule-ruler/demo.jsx +181 -0
  327. package/registry/blocks/schedule-ruler/index.js +21 -0
  328. package/registry/blocks/schedule-ruler/ruler-model.js +239 -0
  329. package/registry/blocks/schedule-ruler/schedule-ruler.jsx +548 -0
  330. package/registry/blocks/sidebar/README.md +23 -13
  331. package/registry/blocks/sketch-canvas/README.md +15 -9
  332. package/registry/blocks/source-receipt/README.md +97 -0
  333. package/registry/blocks/source-receipt/block.json +30 -0
  334. package/registry/blocks/source-receipt/demo.jsx +71 -0
  335. package/registry/blocks/source-receipt/index.js +3 -0
  336. package/registry/blocks/source-receipt/source-receipt.jsx +107 -0
  337. package/registry/blocks/spreadsheet/spreadsheet.jsx +15 -1
  338. package/registry/blocks/step-picker/README.md +78 -0
  339. package/registry/blocks/step-picker/block.json +31 -0
  340. package/registry/blocks/step-picker/demo.jsx +23 -0
  341. package/registry/blocks/step-picker/index.js +3 -0
  342. package/registry/blocks/step-picker/step-picker.jsx +144 -0
  343. package/registry/blocks/step-settings/README.md +100 -0
  344. package/registry/blocks/step-settings/block.json +44 -0
  345. package/registry/blocks/step-settings/demo.jsx +93 -0
  346. package/registry/blocks/step-settings/fields.jsx +58 -0
  347. package/registry/blocks/step-settings/flow-settings.jsx +115 -0
  348. package/registry/blocks/step-settings/index.js +5 -0
  349. package/registry/blocks/step-settings/step-settings.jsx +290 -0
  350. package/registry/blocks/suggestion/README.md +171 -0
  351. package/registry/blocks/suggestion/block.json +40 -0
  352. package/registry/blocks/suggestion/demo.jsx +91 -0
  353. package/registry/blocks/suggestion/index.js +3 -0
  354. package/registry/blocks/suggestion/suggestion.jsx +436 -0
  355. package/registry/blocks/sum-bar/README.md +122 -0
  356. package/registry/blocks/sum-bar/block.json +10 -0
  357. package/registry/blocks/sum-bar/demo.jsx +110 -0
  358. package/registry/blocks/sum-bar/index.js +5 -0
  359. package/registry/blocks/sum-bar/sum-bar.jsx +209 -0
  360. package/registry/blocks/sum-bar/sum-model.js +126 -0
  361. package/registry/blocks/tier-map/README.md +217 -0
  362. package/registry/blocks/tier-map/block.json +10 -0
  363. package/registry/blocks/tier-map/demo.jsx +222 -0
  364. package/registry/blocks/tier-map/index.js +24 -0
  365. package/registry/blocks/tier-map/tier-map.jsx +574 -0
  366. package/registry/blocks/tier-map/tier-model.js +318 -0
  367. package/registry/blocks/time-grid/README.md +305 -0
  368. package/registry/blocks/time-grid/block.json +10 -0
  369. package/registry/blocks/time-grid/demo.jsx +279 -0
  370. package/registry/blocks/time-grid/grid-model.js +322 -0
  371. package/registry/blocks/time-grid/index.js +25 -0
  372. package/registry/blocks/time-grid/time-grid.jsx +1325 -0
  373. package/registry/blocks/time-strip/README.md +141 -0
  374. package/registry/blocks/time-strip/block.json +10 -0
  375. package/registry/blocks/time-strip/demo.jsx +129 -0
  376. package/registry/blocks/time-strip/index.js +19 -0
  377. package/registry/blocks/time-strip/strip-model.js +178 -0
  378. package/registry/blocks/time-strip/time-strip.jsx +260 -0
  379. package/registry/blocks/top-bar/README.md +149 -0
  380. package/registry/blocks/top-bar/block.json +35 -0
  381. package/registry/blocks/top-bar/demo.jsx +78 -0
  382. package/registry/blocks/top-bar/index.js +5 -0
  383. package/registry/blocks/top-bar/probe-rules.js +74 -0
  384. package/registry/blocks/top-bar/top-bar-model.js +152 -0
  385. package/registry/blocks/top-bar/top-bar.jsx +244 -0
  386. package/registry/blocks/trigger-picker/README.md +98 -0
  387. package/registry/blocks/trigger-picker/block.json +38 -0
  388. package/registry/blocks/trigger-picker/demo.jsx +59 -0
  389. package/registry/blocks/trigger-picker/index.js +3 -0
  390. package/registry/blocks/trigger-picker/trigger-picker.jsx +374 -0
  391. package/registry/blocks/whiteboard/README.md +20 -14
  392. package/registry/blocks/workflow-view/README.md +123 -0
  393. package/registry/blocks/workflow-view/block.json +10 -0
  394. package/registry/blocks/workflow-view/demo.jsx +90 -0
  395. package/registry/blocks/workflow-view/index.js +3 -0
  396. package/registry/blocks/workflow-view/workflow-view.jsx +203 -0
  397. package/src/cli.mjs +2 -2
  398. package/src/install.mjs +100 -22
  399. package/src/registry.mjs +37 -3
  400. package/templates/next/browser/lib/data.js +6 -1
  401. package/templates/next/files/README.md +909 -26
  402. package/templates/next/files/app/shell.jsx +54 -37
  403. package/templates/next/files/check/automation-format.ts +2103 -0
  404. package/templates/next/files/check/automation-package.ts +142 -0
  405. package/templates/next/files/check/run.mjs +1043 -12
  406. package/templates/next/files/components/ui/field.jsx +17 -4
  407. package/templates/next/files/components/ui/icon.jsx +13 -0
  408. package/templates/next/files/components/ui/index.js +3 -1
  409. package/templates/next/files/components/ui/layout.jsx +382 -0
  410. package/templates/next/files/components/ui/notice.jsx +70 -0
  411. package/templates/next/files/components/ui/page.jsx +124 -0
  412. package/templates/next/files/components/ui/stat.jsx +22 -7
  413. package/templates/next/files/components/ui/toast.jsx +6 -3
  414. package/templates/next/files/lib/automations.js +660 -0
  415. package/templates/next/files/lib/claw.js +244 -5
  416. package/templates/next/files/lib/door.js +92 -0
  417. package/templates/next/files/lib/hooks.js +1694 -6
  418. package/templates/next/files/lib/platform.js +611 -14
  419. package/templates/next/template.json +22 -3
@@ -0,0 +1,69 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "pace-bar",
4
+ "type": "registry:block",
5
+ "title": "PaceBar",
6
+ "description": "Progress against a clock. PaceBar is one bar filled to its value with a hard upright tick where it should be by now (the share of a period that has gone), and the stretch between them hatched, so behind is a shape you see; a spend (hours burned against time gone) hatches the other way. PaceList puts many bars on ONE axis: the period's month names and a tick each Monday above, one today line running unbroken through the whole list and its group headings, each row's percent, who last moved it (a person's dot, the agent's mark, or none) and the gap said in weeks (\"4 wk behind\", \"On pace\", \"Not judged yet\", \"Done\"); rows sit in optional groups and a row can be pressed. PaceRuler is the axis alone, with a flag for today (\"Today · 92% gone\"). WeekStrip is the period as its weeks in one row: weeks gone filled, this one marked, a mark on each week a check-in was made (person, agent, none), a dashed ring on a past week with none. The sums are exported as plain functions (paceOf, paceWords, paceSentence: \"57% done, 88% of the time gone\"). Positions are percentages, so it draws the same on the server; narrow, a list row is its name above its bar. Loading, failed and empty states are drawn.",
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/agent-marks.json"
10
+ ],
11
+ "dependencies": [],
12
+ "files": [
13
+ {
14
+ "path": "registry/blocks/pace-bar/index.js",
15
+ "type": "registry:component",
16
+ "target": "@components/makersclaw/blocks/pace-bar/index.js",
17
+ "content": "// The PaceBar block — `makersclaw add pace-bar`. Fixed: compose it through\n// its props (README.md beside this file). The model's helpers are plain\n// functions; they work on the server too.\nexport { PaceBar, PaceList, PaceRuler, WeekStrip, PaceMark } from \"./pace-bar\";\nexport {\n WORDS,\n atDay,\n axisOf,\n dayName,\n dayNumber,\n groupRows,\n isoOf,\n marksOnWeeks,\n paceOf,\n paceSentence,\n paceWords,\n percent,\n periodOf,\n shortDay,\n toneOf,\n weeksOf,\n} from \"./pace-model\";\n"
18
+ },
19
+ {
20
+ "path": "registry/blocks/pace-bar/pace-bar.jsx",
21
+ "type": "registry:component",
22
+ "target": "@components/makersclaw/blocks/pace-bar/pace-bar.jsx",
23
+ "content": "\"use client\";\n\nimport { Button, Empty, Notice, Skeleton, cx } from \"../../ui\";\nimport { AgentMark } from \"../agent-marks\";\nimport { axisOf, dayName, groupRows, marksOnWeeks, paceOf, paceSentence, paceWords, percent, share, shortDay, toneOf, weeksOf, WORDS } from \"./pace-model\";\n\n// Progress against a clock. Four pieces that share one idea, \"how far\n// along, and how far along should it be by now\":\n//\n// PaceBar one bar: filled to `value`, a hard upright tick at the pace,\n// and the stretch between them hatched, so \"behind\" is a shape\n// you see and not a sum you do.\n// PaceList many bars on ONE axis: the period's months and Mondays above,\n// one today line running through the whole list, and each\n// row's gap said in weeks (\"4 wk behind\"). Rows can sit in\n// groups.\n// PaceRuler the axis alone (month names, a tick each Monday, a flag for\n// today), for a screen that lays out its own rows.\n// WeekStrip the period as its weeks in one row: the weeks gone filled,\n// this one marked, a mark on the week each check-in was made,\n// by a person, by the agent, or by no one.\n//\n// Props in, one callback out (`onOpen`); the block imports only React, the\n// kit and the agent's mark. README.md beside this file lists every prop.\n// Server-safe: nothing here measures the page, positions are percentages.\n\n// ---- looks (literal class names, so Tailwind sees every one)\n\nconst FILL = {\n ink: \"bg-foreground\",\n quiet: \"bg-muted-foreground\",\n info: \"bg-info\",\n success: \"bg-success\",\n warning: \"bg-warning\",\n danger: \"bg-destructive\",\n};\n// The hatch is drawn in currentColor, so a text-* token paints it.\nconst HATCH_TEXT = {\n ink: \"text-foreground\",\n quiet: \"text-muted-foreground\",\n info: \"text-info\",\n success: \"text-success\",\n warning: \"text-warning\",\n danger: \"text-destructive\",\n};\nconst GAP_TEXT = { ...HATCH_TEXT, ink: \"text-muted-foreground\" };\nconst HATCH = { backgroundImage: \"repeating-linear-gradient(135deg, currentColor 0 2px, transparent 2px 5px)\" };\n\nconst SIZE = {\n lg: { track: \"h-3.5\", tick: \"-inset-y-2\" },\n md: { track: \"h-2.5\", tick: \"-inset-y-1\" },\n sm: { track: \"h-1.5\", tick: \"-inset-y-1\" },\n};\n\nconst pct = (n) => `${Math.round(n * 1000) / 1000}%`;\nconst asTone = (tone, p, cut) => (tone === \"auto\" || tone == null ? toneOf(p, cut) : FILL[tone] ? tone : \"ink\");\n\n/** What the bar is made of, without a meter's role: the fill, the hatched gap, and the tick when asked. */\nfunction Bars({ p, tone, size, hatch, tick, animate, delay }) {\n const s = SIZE[size] ?? SIZE.md;\n const fill = (p.value ?? 0) * 100;\n const mark = p.pace == null ? null : p.pace * 100;\n const spend = p.kind === \"spend\";\n let from = 0;\n let width = 0;\n if (hatch && mark != null) {\n if (!spend && mark > fill) [from, width] = [fill, mark - fill];\n if (spend && fill > mark) [from, width] = [mark, fill - mark];\n }\n const grow = animate ? \"transition-[width] duration-700 ease-out starting:w-0 motion-reduce:transition-none\" : \"\";\n const wait = animate && delay ? { transitionDelay: `${Math.min(540, Math.max(0, Number(delay) || 0))}ms` } : undefined;\n return (\n <>\n <span className={cx(\"relative block overflow-hidden rounded-[1px] bg-muted\", s.track)}>\n {width ? (\n <span\n aria-hidden=\"true\"\n className={cx(\"absolute inset-y-0 left-(--from) w-(--width) opacity-60\", HATCH_TEXT[tone])}\n style={{ ...HATCH, \"--from\": pct(from), \"--width\": pct(width) }}\n />\n ) : null}\n <span aria-hidden=\"true\" className={cx(\"absolute inset-y-0 left-0 w-(--fill) rounded-[1px]\", FILL[tone], grow)} style={{ \"--fill\": pct(fill), ...wait }} />\n </span>\n {tick && mark != null ? (\n <span aria-hidden=\"true\" className={cx(\"absolute left-(--mark) w-0.5 -translate-x-1/2 bg-foreground ring-1 ring-background\", s.tick)} style={{ \"--mark\": pct(mark) }} />\n ) : null}\n </>\n );\n}\n\n/**\n * One bar against a pace. Give `pace` (0..1) yourself, or give the period\n * (`start`, `end`, `today`) and the share of the time gone is the pace.\n *\n * Props: see README.md. In short: `value`, `pace`, `start`, `end`, `today`,\n * `kind` (progress | spend), `tone` (ink | quiet | info | success | warning\n * | danger | auto), `size` (lg | md | sm), `hatch`, `animate`, `delay`,\n * `words`, `label`, `className`.\n */\nexport function PaceBar({ value, pace, start, end, today, kind = \"progress\", tone = \"ink\", size = \"md\", hatch = true, animate = false, delay = 0, words, tolerance, graceDays, graceFraction, label = \"Progress\", className }) {\n const p = paceOf({ value, pace, start, end, today, kind, tolerance, graceDays, graceFraction });\n const t = asTone(tone, p);\n const valuetext = paceSentence(p, words) || (p.valuePct == null ? \"not known\" : `${p.valuePct}%`);\n return (\n <span\n role=\"meter\"\n aria-label={label}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={p.valuePct ?? 0}\n aria-valuetext={`${valuetext}${p.state === \"worse\" || p.state === \"better\" || p.state === \"on\" ? `. ${paceWords(p, words)}` : \"\"}`}\n className={cx(\"relative block min-w-0\", className)}\n >\n <Bars p={p} tone={t} size={size} hatch={hatch} tick animate={animate} delay={delay} />\n </span>\n );\n}\n\n// ---- marks\n\nconst MARK_WORDS = { person: \"A person\", agent: \"The agent\" };\n\n/**\n * Who made a check-in: a filled dot for a person, the platform's mark for\n * the agent, nothing for no one. With `empty` a mark made by no one is a\n * hollow ring (the place a mark would be); with `missed` it is a dashed\n * ring (a week that should have one and has none). `label` is what a\n * screen reader hears; leave it out when words beside the mark already say it.\n */\nexport function PaceMark({ by, label, empty = false, missed = false, size = \"sm\", className }) {\n const aria = label ? { role: \"img\", \"aria-label\": label, title: label } : { \"aria-hidden\": \"true\" };\n const dot = size === \"sm\" ? \"size-2\" : \"size-2.5\";\n if (by === \"agent\") return <AgentMark size={size === \"sm\" ? \"xs\" : size} label={label} className={className} />;\n if (by === \"person\") return <span {...aria} className={cx(\"inline-block shrink-0 rounded-full bg-foreground\", dot, className)} />;\n if (missed) return <span {...aria} className={cx(\"inline-block shrink-0 rounded-full border border-dashed border-muted-foreground\", dot, className)} />;\n if (empty) return <span {...aria} className={cx(\"inline-block shrink-0 rounded-full border border-muted-foreground\", dot, className)} />;\n return null;\n}\n\n// ---- the ruler\n\n/** The flag's sides: a flag near the right edge hangs left of the line, near the left edge right of it. */\nconst flagSide = (at) => (at > 0.78 ? \"-translate-x-full\" : at < 0.22 ? \"\" : \"-translate-x-1/2\");\n\n/**\n * The axis of a period: month names, a tick each week start, the today\n * line and its flag. Place it in the column your rows' bars share.\n * Props: `start`, `end`, `today`, `weekStartsOn`, `words`, `showToday`,\n * `label`, `className`.\n */\nexport function PaceRuler({ start, end, today, weekStartsOn = 0, words, showToday = true, label = \"Period\", className }) {\n const w = { ...WORDS, ...words };\n const axis = axisOf({ start, end, today, weekStartsOn });\n if (!axis) return null;\n const { period } = axis;\n const at = showToday ? axis.todayAt : null;\n const flag =\n at == null\n ? period.where === \"before\"\n ? w.before\n : null\n : period.where === \"after\"\n ? w.after\n : `${w.today} · ${w.gone(percent(period.share))}`;\n const summary = `${label}: ${shortDay(period.s)} to ${shortDay(period.e)}${period.t != null ? `. ${dayName(period.t)}${period.where === \"in\" ? `, ${percent(period.share)}% of the time gone, ${period.left} ${period.left === 1 ? \"day\" : \"days\"} left` : period.where === \"after\" ? \", ended\" : \", not started\"}` : \"\"}`;\n return (\n <span role=\"img\" aria-label={summary} className={cx(\"relative block h-11 min-w-0\", className)}>\n {flag ? (\n <span\n aria-hidden=\"true\"\n className={cx(\n \"absolute top-0 z-10 whitespace-nowrap rounded-sm px-1.5 text-xs font-semibold leading-4\",\n at == null ? \"left-0 bg-muted text-muted-foreground\" : cx(\"bg-foreground text-background\", flagSide(at)),\n )}\n style={at == null ? undefined : { left: pct(at * 100) }}\n >\n {flag}\n </span>\n ) : null}\n <span aria-hidden=\"true\" className=\"absolute inset-x-0 bottom-0 h-px bg-border\" />\n {axis.weeks.map((d) => (\n <span key={`w${d.day}`} aria-hidden=\"true\" className=\"absolute bottom-0 h-1.5 w-px bg-border\" style={{ left: pct(d.at * 100) }} />\n ))}\n {axis.months.map((m) => (\n <span key={`m${m.day}`} aria-hidden=\"true\">\n <span className=\"absolute bottom-0 h-2.5 w-px bg-foreground\" style={{ left: pct(m.at * 100) }} />\n <span className=\"absolute bottom-3 whitespace-nowrap pl-1 text-xs font-semibold text-foreground\" style={{ left: pct(m.at * 100) }}>\n {m.label}\n </span>\n </span>\n ))}\n {at != null ? <span aria-hidden=\"true\" className=\"absolute inset-y-0 w-0.5 -translate-x-1/2 bg-foreground\" style={{ left: pct(at * 100) }} /> : null}\n </span>\n );\n}\n\n// ---- the list\n\n// Rows are one grid, the same columns in every row, so the bars of every\n// row start and end at the same x: that is the shared axis. Narrow, a row is\n// its name on one line and the bar and number under it.\nconst ROW = \"grid grid-cols-[minmax(0,1fr)_4rem] items-center gap-x-3 gap-y-1 px-3 py-2 @2xl:grid-cols-(--cols)\";\n// The name takes the whole narrow row; wide it is one column, or two (name and\n// meta) in a heading, where nothing sits in the meta column.\nconst NAME = \"col-span-2 @2xl:col-span-1\";\nconst NAME_WIDE = \"col-span-2\";\nconst HEAD = \"text-xs font-semibold uppercase tracking-wide text-muted-foreground\";\n// The today line reaches into the row's padding and its border, so the lines\n// of the rows below one another read as one.\nconst LINE = \"pointer-events-none absolute -top-2 -bottom-[calc(0.5rem+1px)] w-0.5 -translate-x-1/2 bg-foreground\";\n\nfunction Track({ children, at, className }) {\n return (\n <span className={cx(\"relative min-h-5 self-stretch\", className)}>\n {children}\n {at != null ? <span aria-hidden=\"true\" className={LINE} style={{ left: pct(at * 100) }} /> : null}\n </span>\n );\n}\n\n/**\n * Many bars on one axis. `rows` are `{ id, label, sub, value, pace, kind,\n * tone, by, byLabel, meta, note, group }`; `groups` are `{ id, label, sub,\n * note, empty }`.\n *\n * Props: see README.md. In short: `rows`, `groups`, `start`, `end`,\n * `today`, `pace`, `kind`, `tolerance`, `graceDays`, `graceFraction`, `unit`, `words`,\n * `heads`, `ruler`, `metaWidth`, `trackWidth`, `gapWidth`, `size`, `selected`,\n * `onOpen`, `loading`, `error`, `onRetry`, `empty`, `emptyAction`, `label`,\n * `className`.\n */\nexport function PaceList({\n rows,\n groups,\n start,\n end,\n today,\n pace,\n kind = \"progress\",\n tolerance,\n graceDays,\n graceFraction,\n unit,\n words: wordsIn,\n heads,\n ruler = true,\n weekStartsOn = 0,\n warnAt,\n dangerAt,\n metaWidth,\n trackWidth = \"24rem\",\n gapWidth = \"9rem\",\n size = \"sm\",\n selected,\n onOpen,\n loading = false,\n error,\n onRetry,\n empty,\n emptyAction,\n label = \"Progress against the clock\",\n className,\n}) {\n const words = { ...WORDS, ...wordsIn };\n if (loading) {\n return (\n <div aria-busy=\"true\" aria-label={`${label}, loading`} className={cx(\"@container grid min-w-0 gap-3 px-3 py-2\", className)}>\n <Skeleton shape=\"rect\" className=\"h-8 w-full\" />\n {[0, 1, 2, 3].map((i) => (\n <Skeleton key={i} shape=\"rect\" className=\"h-5 w-full\" />\n ))}\n </div>\n );\n }\n if (error) {\n return (\n <div className={cx(\"@container min-w-0\", className)}>\n <Notice tone=\"danger\" title={`${label} did not load`} action={onRetry ? <Button size=\"sm\" onClick={onRetry}>Try again</Button> : null}>\n {typeof error === \"string\" ? error : \"Something went wrong reading the rows.\"}\n </Notice>\n </div>\n );\n }\n\n const axis = axisOf({ start, end, today, weekStartsOn });\n const sharedPace = pace === undefined ? undefined : share(pace);\n // The line: the end of today on the period, or the pace given.\n const lineAt = axis ? axis.todayAt : sharedPace ?? null;\n const list = rows ?? [];\n const cols = [\"minmax(0,1fr)\", metaWidth, `minmax(10rem,${trackWidth})`, \"4rem\", gapWidth].filter(Boolean).join(\" \");\n const style = { \"--cols\": cols };\n // In a heading the name also takes the meta column's place.\n const headName = cx(\"min-w-0\", metaWidth ? NAME_WIDE : NAME);\n\n if (!list.length && !(groups ?? []).length) {\n return (\n <div className={cx(\"@container min-w-0\", className)}>\n <Empty icon=\"clock\" title={empty ?? \"Nothing to measure yet\"} action={emptyAction} />\n </div>\n );\n }\n\n const read = new Map(\n list.map((r) => {\n const p = paceOf({\n value: r.value,\n pace: r.pace === undefined ? sharedPace : r.pace,\n start,\n end,\n today,\n kind: r.kind ?? kind,\n tolerance,\n graceDays,\n graceFraction,\n unit: r.unit ?? unit,\n });\n return [r, { p, tone: asTone(r.tone, p, { warnAt, dangerAt }) }];\n }),\n );\n const grouped = groupRows(list, groups);\n\n const head = heads ?? {};\n const showRuler = ruler && axis;\n const summary = `${label}: ${list.length} ${list.length === 1 ? \"row\" : \"rows\"}${axis?.period.t != null && axis.period.where === \"in\" ? `, ${percent(axis.period.share)}% of the time gone` : \"\"}`;\n\n return (\n <div role=\"group\" aria-label={summary} className={cx(\"@container min-w-0\", className)}>\n {showRuler || head.label || head.pct || head.gap ? (\n <div className={cx(ROW, \"border-b border-border bg-background\")} style={style}>\n <span className={cx(HEAD, \"self-end\", headName)}>{head.label}</span>\n <span className=\"relative col-span-2 min-w-0 self-stretch @2xl:col-span-1\">\n {showRuler ? <PaceRuler start={start} end={end} today={today} weekStartsOn={weekStartsOn} words={words} label={label} /> : null}\n {showRuler && lineAt != null ? <span aria-hidden=\"true\" className=\"absolute top-full h-[calc(0.5rem+1px)] w-0.5 -translate-x-1/2 bg-foreground\" style={{ left: pct(lineAt * 100) }} /> : null}\n </span>\n <span className={cx(HEAD, \"hidden self-end text-right @2xl:block\")}>{head.pct}</span>\n <span className={cx(HEAD, \"hidden self-end @2xl:block\")}>{head.gap}</span>\n </div>\n ) : null}\n {grouped.map(({ group, rows: inGroup }, gi) => (\n <div key={group?.id ?? `rest-${gi}`} role=\"group\" aria-label={group?.label ?? undefined}>\n {group ? (\n <div className={cx(ROW, \"border-b border-border bg-background\")} style={style}>\n <div className={cx(\"flex flex-wrap items-baseline gap-x-3 gap-y-0.5\", headName)}>\n <h3 className=\"text-sm font-semibold\">{group.label}</h3>\n {group.sub ? <span className=\"text-xs text-muted-foreground\">{group.sub}</span> : null}\n {!inGroup.length && group.empty ? <span className=\"text-xs text-muted-foreground\">{group.empty}</span> : null}\n {group.note ? <span className=\"text-xs text-muted-foreground\">{group.note}</span> : null}\n </div>\n <Track at={lineAt} className=\"hidden @2xl:block\" />\n <span className=\"hidden @2xl:block\" />\n <span className=\"hidden @2xl:block\" />\n </div>\n ) : null}\n <ul className=\"m-0 grid list-none p-0\">\n {inGroup.map((r, i) => {\n const { p, tone } = read.get(r);\n const said = r.note ?? paceWords(p, words);\n const Tag = onOpen ? \"button\" : \"div\";\n const on = selected != null && selected === r.id;\n // A row with a pace of its own gets its own tick; one on the shared clock leaves it to the line.\n const own = p.pace != null && lineAt != null && Math.abs(p.pace - lineAt) > 0.004;\n return (\n <li key={r.id ?? i} className=\"min-w-0\">\n <Tag\n {...(onOpen ? { type: \"button\", onClick: () => onOpen(r), \"aria-current\": on ? \"true\" : undefined } : {})}\n className={cx(\n ROW,\n \"w-full border-b border-border text-left outline-none\",\n onOpen && \"cursor-pointer hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset\",\n on && \"bg-accent\",\n )}\n style={style}\n >\n <span className={cx(\"min-w-0\", NAME)}>\n <span className=\"block break-words text-sm font-medium leading-snug\">{r.label}</span>\n {r.sub ? <span className=\"block break-words text-xs text-muted-foreground\">{r.sub}</span> : null}\n </span>\n {metaWidth ? <span className=\"col-span-2 min-w-0 @2xl:col-span-1\">{r.meta}</span> : null}\n <Track at={lineAt} className=\"col-start-1 @2xl:col-start-auto\">\n <span className=\"absolute inset-x-0 top-1/2 block -translate-y-1/2\">\n <span className=\"relative block\">\n <Bars p={p} tone={tone} size={size} hatch tick={own} />\n </span>\n </span>\n </Track>\n <span className=\"col-start-2 flex items-center justify-end gap-1.5 text-sm font-semibold tabular-nums @2xl:col-start-auto\">\n <PaceMark by={r.by} label={r.byLabel ?? (r.by ? MARK_WORDS[r.by] : undefined)} />\n <span>{p.valuePct == null ? \"–\" : `${p.valuePct}%`}</span>\n </span>\n <span className={cx(\"col-span-2 min-w-0 break-words text-xs @2xl:col-span-1\", p.state === \"worse\" && tone !== \"ink\" ? cx(\"font-medium\", GAP_TEXT[tone]) : \"text-muted-foreground\")}>{said}</span>\n </Tag>\n </li>\n );\n })}\n </ul>\n </div>\n ))}\n </div>\n );\n}\n\n// ---- the week strip\n\nconst WEEK_WORD = \"Week\";\n\n/**\n * The period as its weeks in one row. A week gone is filled, the current\n * one is marked, weeks to come are empty. A mark sits on the week its date\n * falls in: a filled dot for a person, the agent's mark, or (with `missed`)\n * a dashed ring on a past week with no mark at all.\n *\n * Props: `start`, `end`, `today`, `marks` [{ id, date, by, label }],\n * `weekStartsOn`, `missed`, `onOpen(week)`, `words`, `label`, `className`.\n */\nexport function WeekStrip({ start, end, today, marks, weekStartsOn = 0, missed = false, onOpen, words, label = \"Weeks\", className }) {\n const w = { week: WEEK_WORD, ...words };\n const base = weeksOf({ start, end, today, weekStartsOn });\n if (!base.length) return null;\n const { weeks, skipped } = marksOnWeeks(base, marks);\n const current = weeks.find((x) => x.current);\n const total = weeks.reduce((n, x) => n + x.marks.length, 0);\n const summary = `${label}: ${weeks.length} ${weeks.length === 1 ? \"week\" : \"weeks\"}${current ? `, ${w.week.toLowerCase()} ${current.n} now` : \"\"}, ${total} ${total === 1 ? \"mark\" : \"marks\"}${skipped ? `, ${skipped} without a date left off` : \"\"}`;\n return (\n <div className={cx(\"@container min-w-0\", className)}>\n <div role=\"region\" aria-label={`${label}, scrolls sideways`} tabIndex={0} className=\"overflow-x-auto rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring\">\n <ol aria-label={summary} className=\"m-0 grid list-none gap-0.5 p-0\" style={{ gridTemplateColumns: `repeat(${weeks.length}, minmax(1.5rem, 1fr))` }}>\n {weeks.map((x) => {\n const Tag = onOpen ? \"button\" : \"div\";\n const name = `${w.week} ${x.n}, ${shortDay(x.from)} to ${shortDay(x.to)}${x.current ? \", now\" : \"\"}${x.marks.length ? `, ${x.marks.map((m) => m.label ?? MARK_WORDS[m.by] ?? \"a mark\").join(\", \")}` : \"\"}`;\n return (\n <li key={x.n} className=\"min-w-0\">\n <Tag\n {...(onOpen ? { type: \"button\", onClick: () => onOpen(x), \"aria-label\": name } : { role: \"img\", \"aria-label\": name })}\n {...(x.current ? { \"aria-current\": \"date\" } : {})}\n className={cx(\"grid w-full min-w-0 justify-items-center gap-1 rounded-sm px-0.5 py-1 outline-none\", onOpen && \"cursor-pointer hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring\")}\n >\n <span aria-hidden=\"true\" className={cx(\"text-xs tabular-nums\", x.current ? \"font-semibold text-foreground\" : \"text-muted-foreground\")}>\n {x.n}\n </span>\n <span\n aria-hidden=\"true\"\n className={cx(\"block w-full rounded-[1px]\", x.state === \"past\" ? \"h-1.5 bg-muted-foreground\" : x.current ? \"h-2.5 bg-foreground\" : \"h-1.5 bg-muted\")}\n />\n <span aria-hidden=\"true\" className=\"flex min-h-3 flex-wrap items-center justify-center gap-0.5\">\n {x.marks.slice(0, 3).map((m) => (\n <PaceMark key={m.id} by={m.by} empty />\n ))}\n {x.marks.length > 3 ? <span className=\"text-xs text-muted-foreground\">+{x.marks.length - 3}</span> : null}\n {missed && x.state === \"past\" && !x.marks.length ? <PaceMark missed /> : null}\n </span>\n </Tag>\n </li>\n );\n })}\n </ol>\n </div>\n </div>\n );\n}\n\nexport default PaceBar;\n"
24
+ },
25
+ {
26
+ "path": "registry/blocks/pace-bar/pace-model.js",
27
+ "type": "registry:component",
28
+ "target": "@components/makersclaw/blocks/pace-bar/pace-model.js",
29
+ "content": "// The pace bar's arithmetic: a period on a clock, how much of it is gone,\n// how a piece of work stands against that, the gap said in weeks (or days,\n// or points), the words for it, the axis (month names, a tick each Monday,\n// a today line) and the weeks of a strip. Pure: no React, no DOM, no clock\n// of its own (`today` always comes in). The components (pace-bar.jsx) draw\n// what this returns; test/pace-bar.test.mjs pins it.\n//\n// A \"day\" is a whole number: days since 1970-01-01 in UTC, so a date string\n// means the same day on the server and in the browser. A period includes\n// both its first and its last day, and TODAY COUNTS AS GONE: on the 23rd of\n// a quarter that began on the 1st of July and ends on the 30th of\n// September, 85 of its 92 days are gone, and 92% of the time is gone.\n\nexport const DAY_MS = 86400000;\nexport const MONTHS = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\nexport const WEEKDAYS = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"];\n\n/** 'YYYY-MM-DD' (or an ISO time) to a day number; null when it is not a date. */\nexport function dayNumber(iso) {\n if (typeof iso === \"number\" && Number.isFinite(iso)) return Math.round(iso);\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})/.exec(String(iso ?? \"\"));\n if (!m) return null;\n const t = Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]));\n // 2026-02-31 is not a day: Date rolls it over, and the roll-over shows.\n if (new Date(t).getUTCMonth() !== Number(m[2]) - 1) return null;\n return Math.round(t / DAY_MS);\n}\n\n/** A day number back to 'YYYY-MM-DD'. */\nexport function isoOf(day) {\n const d = new Date(day * DAY_MS);\n return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, \"0\")}-${String(d.getUTCDate()).padStart(2, \"0\")}`;\n}\n\n/** 0 is Monday, 6 is Sunday. */\nexport const weekdayOf = (day) => (new Date(day * DAY_MS).getUTCDay() + 6) % 7;\nexport const dateOfMonth = (day) => new Date(day * DAY_MS).getUTCDate();\nexport const monthOf = (day) => new Date(day * DAY_MS).getUTCMonth();\n\n/** \"23 Sep\" */\nexport const shortDay = (day) => `${dateOfMonth(day)} ${MONTHS[monthOf(day)]}`;\n/** \"Wed 23 Sep\" */\nexport const dayName = (day) => `${WEEKDAYS[weekdayOf(day)]} ${shortDay(day)}`;\n\n/** A number as a 0..1 share, clamped; null when it is not a number. */\nexport function share(x) {\n if (x == null || x === \"\") return null;\n const v = Number(x);\n return Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : null;\n}\n\n/** A share as a whole percent; null stays null. */\nexport const percent = (x) => (x == null ? null : Math.round(x * 100));\n\n/**\n * The period and where today stands in it.\n * total days in it, both ends counted\n * gone days gone, today counted (0 before it starts, total after it ends)\n * share gone / total, 0..1\n * left days after today (0 on the last day)\n * where \"before\" | \"in\" | \"after\"\n * Null when either end is missing or the end is before the start. With no\n * `today`, `where` is \"in\" with nothing gone and `share` is null: the\n * period is known but the clock is not.\n */\nexport function periodOf({ start, end, today } = {}) {\n const s = dayNumber(start);\n const e = dayNumber(end);\n if (s == null || e == null || e < s) return null;\n const t = dayNumber(today);\n const total = e - s + 1;\n if (t == null) return { s, e, t: null, total, gone: null, share: null, left: null, where: \"in\" };\n if (t < s) return { s, e, t, total, gone: 0, share: 0, left: total, where: \"before\" };\n if (t > e) return { s, e, t, total, gone: total, share: 1, left: 0, where: \"after\" };\n const gone = t - s + 1;\n return { s, e, t, total, gone, share: gone / total, left: e - t, where: \"in\" };\n}\n\n/**\n * How a piece of work stands against the clock.\n *\n * value how far along: 0..1 (a spend: how much is used). Null: not known.\n * pace where it should be by now, 0..1. Left out, it is worked out\n * from `start`, `end` and `today` (the share of the time gone).\n * `null` on purpose: pace is not judged.\n * kind \"progress\" (default: short of the pace is the gap) or\n * \"spend\" (over the pace is the gap: hours burned against\n * time gone)\n * tolerance whole points of the period inside which it is \"on pace\" (5)\n * graceDays days from the start in which it is not judged yet (0)\n * graceFraction the same as a share of the period, 0..1 (0.2: the first\n * fifth; 19 days of a 92-day quarter). The longer of the two\n * counts. Without a period it does nothing.\n * unit \"week\" · \"day\" · \"point\". Default \"week\" when the period is\n * known and the kind is progress; otherwise \"point\".\n *\n * Returns\n * state \"done\" · \"unjudged\" · \"better\" · \"on\" · \"worse\"\n * better/worse mean ahead/behind for progress, under/over for a spend\n * value, pace 0..1 (pace null when not judged)\n * valuePct, pacePct whole percents\n * gapPts whole points between them, always >= 0\n * amount the gap in `unit`s (whole, at least 1 when it is not \"on\"),\n * null when on pace or not judged\n * unit\n * period periodOf(), or null\n */\nexport function paceOf({ value, pace, start, end, today, kind = \"progress\", tolerance = 5, graceDays = 0, graceFraction = 0, unit } = {}) {\n const period = periodOf({ start, end, today });\n const v = share(value);\n // pace: undefined → from the clock; null → none.\n const p = pace === undefined ? (period && period.where !== \"before\" ? period.share : null) : share(pace);\n const spend = kind === \"spend\";\n const u = unit ?? (period && !spend ? \"week\" : \"point\");\n const valuePct = percent(v);\n const pacePct = percent(p);\n const base = { kind, value: v, pace: p, valuePct, pacePct, gapPts: 0, amount: null, unit: u, period };\n if (!spend && v != null && v >= 1) return { ...base, state: \"done\" };\n const fraction = Number.isFinite(graceFraction) && graceFraction > 0 ? Math.min(1, graceFraction) : 0;\n const graceUntil = period ? Math.max(Number(graceDays) || 0, Math.ceil(fraction * period.total)) : 0;\n const grace = period && period.gone != null && period.where === \"in\" && period.gone <= graceUntil;\n if (v == null || p == null || grace) return { ...base, state: \"unjudged\" };\n const diff = spend ? valuePct - pacePct : pacePct - valuePct; // positive: worse\n const gapPts = Math.abs(diff);\n if (gapPts <= tolerance) return { ...base, state: \"on\", gapPts };\n let amount = gapPts;\n if (u === \"day\" && period) amount = Math.max(1, Math.round((gapPts / 100) * period.total));\n else if (u === \"week\" && period) amount = Math.max(1, Math.round(((gapPts / 100) * period.total) / 7));\n return { ...base, state: diff > 0 ? \"worse\" : \"better\", gapPts, amount, unit: period || u === \"point\" ? u : \"point\" };\n}\n\nexport const WORDS = {\n on: \"On pace\",\n unjudged: \"Not judged yet\",\n done: \"Done\",\n today: \"Today\",\n before: \"Not started\",\n after: \"Ended\",\n units: { week: \"wk\", day: \"d\", point: \"pts\" },\n behind: (n, unit) => `${n} ${unit} behind`,\n ahead: (n, unit) => `${n} ${unit} ahead`,\n over: (n, unit) => `${n} ${unit} over`,\n under: (n, unit) => `${n} ${unit} under`,\n sentence: ({ done, gone }) => `${done}% done, ${gone}% of the time gone`,\n spendSentence: ({ done, gone }) => `${done}% used, ${gone}% of the time gone`,\n gone: (n) => `${n}% gone`,\n};\n\nconst merge = (words) => ({ ...WORDS, ...words, units: { ...WORDS.units, ...(words?.units ?? {}) } });\n\n/** The gap in words: \"4 wk behind\", \"On pace\", \"2 wk ahead\", \"Not judged yet\", \"Done\". */\nexport function paceWords(p, words) {\n if (!p) return \"\";\n const w = merge(words);\n if (p.state === \"done\") return w.done;\n if (p.state === \"unjudged\") return w.unjudged;\n if (p.state === \"on\") return w.on;\n const unit = w.units[p.unit] ?? p.unit;\n const spend = p.kind === \"spend\";\n const say = p.state === \"worse\" ? (spend ? w.over : w.behind) : spend ? w.under : w.ahead;\n return say(p.amount, unit);\n}\n\n/** The sum in words: \"57% done, 88% of the time gone\". Empty when either number is missing. */\nexport function paceSentence(p, words) {\n if (!p || p.valuePct == null) return \"\";\n const w = merge(words);\n const gone = p.period?.share != null ? percent(p.period.share) : p.pacePct;\n if (gone == null) return `${p.valuePct}%`;\n return (p.kind === \"spend\" ? w.spendSentence : w.sentence)({ done: p.valuePct, gone });\n}\n\n/**\n * A tone for a state, for a screen that has no status of its own to say:\n * on pace or better is \"ink\", worse by `warnAt` points is \"warning\", by\n * `dangerAt` points \"danger\"; not judged yet is \"quiet\"; done is \"success\".\n */\nexport function toneOf(p, { warnAt = 10, dangerAt = 25 } = {}) {\n if (!p) return \"ink\";\n if (p.state === \"done\") return \"success\";\n if (p.state === \"unjudged\") return \"quiet\";\n if (p.state !== \"worse\") return \"ink\";\n if (p.gapPts >= dangerAt) return \"danger\";\n if (p.gapPts >= warnAt) return \"warning\";\n return \"ink\";\n}\n\n/** Where a day's left edge falls on the period, 0..1 (can be outside it). */\nexport const atDay = (period, day) => (day - period.s) / period.total;\n\n/**\n * The axis of a period: the share each Monday's left edge falls at (a tick\n * each, while the period is short enough to read them), each month's 1st\n * with its name (the period's first day names its month unless the next\n * month starts right after it), and today.\n *\n * weeks [{ n, day, at }] n counts the weeks from 1; `at` is 0..1\n * months [{ day, label, at }]\n * todayAt the share the today line stands at (the end of today), or null\n * where \"before\" | \"in\" | \"after\" (as periodOf)\n */\nexport function axisOf({ start, end, today, weekStartsOn = 0 } = {}) {\n const period = periodOf({ start, end, today });\n if (!period) return null;\n const { s, e, total } = period;\n const weeks = [];\n if (total <= 200) {\n let n = 0;\n for (let d = s; d <= e; d += 1) {\n if ((weekdayOf(d) - weekStartsOn + 7) % 7 === 0) {\n n += 1;\n weeks.push({ n, day: d, at: atDay(period, d) });\n }\n }\n }\n const months = [];\n for (let d = s; d <= e; d += 1) if (dateOfMonth(d) === 1) months.push({ day: d, label: MONTHS[monthOf(d)], at: atDay(period, d) });\n const startsOnFirst = dateOfMonth(s) === 1;\n const crowded = !startsOnFirst && months.length > 0 && months[0].at < 0.09;\n if (!startsOnFirst && !crowded) months.unshift({ day: s, label: MONTHS[monthOf(s)], at: 0 });\n else if (!months.length) months.push({ day: s, label: MONTHS[monthOf(s)], at: 0 });\n const todayAt = period.where === \"in\" && period.share != null ? period.share : period.where === \"after\" ? 1 : null;\n return { period, weeks, months, todayAt, where: period.where };\n}\n\n/**\n * The weeks of a strip: one for each week start (Monday by default) in the\n * period. The days before the first week start belong to week 1, the last\n * week runs to the period's end. Each week says whether it is gone, current\n * or still to come.\n *\n * [{ n, from, to, current, state }] state: \"past\" · \"current\" · \"future\"\n */\nexport function weeksOf({ start, end, today, weekStartsOn = 0 } = {}) {\n const period = periodOf({ start, end, today });\n if (!period) return [];\n const { s, e } = period;\n const starts = [];\n for (let d = s; d <= e; d += 1) if ((weekdayOf(d) - weekStartsOn + 7) % 7 === 0) starts.push(d);\n if (!starts.length) starts.push(s);\n else starts[0] = s;\n return starts.map((from, i) => {\n const to = i + 1 < starts.length ? starts[i + 1] - 1 : e;\n const t = period.t;\n const state = t == null ? \"future\" : t > to ? \"past\" : t >= from ? \"current\" : \"future\";\n return { n: i + 1, from, to, state, current: state === \"current\" };\n });\n}\n\n/**\n * Marks on the weeks: each mark `{ id, date, by, label }` lands on the week\n * its date falls in (a date before the first week goes to the first, after\n * the last to the last; no date is left off, and counted in `skipped`).\n * `by` is \"person\", \"agent\" or nothing.\n * Returns { weeks: [{ ...week, marks: [...] }], skipped }.\n */\nexport function marksOnWeeks(weeks, marks) {\n const out = weeks.map((w) => ({ ...w, marks: [] }));\n let skipped = 0;\n for (const [i, m] of (marks ?? []).entries()) {\n const d = dayNumber(m?.date);\n if (d == null || !out.length) {\n skipped += 1;\n continue;\n }\n const at = d < out[0].from ? 0 : out.findLastIndex((w) => w.from <= d);\n out[Math.max(0, at)].marks.push({ ...m, id: m.id ?? `mark-${i}`, day: d, by: m.by === \"person\" || m.by === \"agent\" ? m.by : null });\n }\n for (const w of out) w.marks.sort((a, b) => a.day - b.day);\n return { weeks: out, skipped };\n}\n\n/**\n * Rows in groups. `groups` is `[{ id, ... }]` in the order to draw; a row\n * names its group with `group`. A row whose group is not listed (or has\n * none) goes in a trailing group with no heading when any group exists.\n * With no `groups` there is one headless group of everything.\n * Returns [{ group | null, rows }].\n */\nexport function groupRows(rows, groups) {\n const list = rows ?? [];\n if (!groups?.length) return [{ group: null, rows: list }];\n const known = new Set(groups.map((g) => g.id));\n const out = groups.map((g) => ({ group: g, rows: list.filter((r) => r.group === g.id) }));\n const rest = list.filter((r) => !known.has(r.group));\n if (rest.length) out.push({ group: null, rows: rest });\n return out;\n}\n"
30
+ },
31
+ {
32
+ "path": "registry/blocks/pace-bar/README.md",
33
+ "type": "registry:file",
34
+ "target": "@components/makersclaw/blocks/pace-bar/README.md",
35
+ "content": "# PaceBar\n\nProgress against a clock: how far along, and how far along it should be by now.\n\n\"58%\" alone says little. 58% with the mark at 91% says *behind*. This block\ndraws that, four ways:\n\n- **PaceBar**: one bar, filled to its value, with a hard upright tick where it\n should be by now. The stretch between the fill and the tick is hatched, so\n \"behind\" is a shape you see, not a sum you do.\n- **PaceList**: many bars on **one axis**. The period's month names and a tick\n each Monday run above. **One today line runs unbroken through the whole\n list**, group headings included. Each row says its percent, who last moved it\n and the gap in weeks (\"4 wk behind\").\n- **PaceRuler**: that axis alone, with a flag for today (\"Today · 92% gone\").\n- **WeekStrip**: the period as its weeks in one row, with a mark on each week a\n check-in was made.\n\nIt draws a result the app has already worked out. It collects no check-ins, it\nchanges no number. For a chain of dated phases with gates use `schedule-ruler`;\nfor a plain meter with no clock use the kit's `Progress`.\n\nBorn in two store apps that each drew it by hand: a goals tracker's today line\ndown a whole quarter, and a project monitor's \"57% done, 88% of the time gone\".\n\n```jsx\nimport { PaceBar, PaceList, PaceRuler, WeekStrip, PaceMark, paceOf, paceWords, paceSentence } from \"../components/blocks/pace-bar\"; // a Makersclaw app (\"./blocks/pace-bar\" from components/)\n// shadcn project: import { PaceList } from \"@/components/makersclaw/blocks/pace-bar\"\n```\n\nA client component. It needs the `agent-marks` block (the agent's mark) and no\nnpm package. Dates are `'YYYY-MM-DD'` strings and are days, never times, so the\nserver and the browser draw the same picture. Positions are percentages;\nnothing is measured.\n\n## The clock\n\nGive a **period** (`start`, `end`, both days included) and **today**. The\npace is the share of the period that has gone, **today counted**: on Wed 23 Sep\na quarter that began 1 Jul and ends 30 Sep has 85 of its 92 days gone, so the\npace is 92%.\n\nOr skip the period and give `pace` yourself (0 to 1): a bar needs no dates.\n\nThe block never reads the clock. `today` always comes in as a prop, so a\nserver render and a browser render agree.\n\n## Props: PaceBar\n\n- `value`: how far along, 0 to 1 (clamped). `null`: not known.\n- `pace`: where it should be by now, 0 to 1. Left out, it is worked out from\n `start`, `end` and `today`. `null` on purpose: no tick, pace not judged.\n- `start`, `end`, `today`: the period.\n- `kind`: `\"progress\"` (default; short of the pace is the gap) or `\"spend\"`\n (over the pace is the gap: hours burned against time gone, a budget used).\n- `tone`: `ink` (default), `quiet`, `info`, `success`, `warning`, `danger`,\n or `auto` (ink while on pace or ahead, warning at 10 points behind, danger at\n 25; success when done; quiet when not judged yet). A tone is a status: leave\n it `ink` when nothing needs attention.\n- `size`: `lg` (14 px), `md` (10 px, default), `sm` (6 px).\n- `hatch`: draw the gap between the fill and the tick (default true).\n- `animate`: fill from zero on first paint (CSS only), off under reduced\n motion. `delay`: milliseconds before it starts, to stagger a list (capped\n at 540).\n- `tolerance`, `graceDays`, `graceFraction`: see \"How it judges\" below.\n- `words`: the app's vocabulary (below).\n- `label`: what a screen reader calls it. It reads the sum as words\n (`role=\"meter\"`): \"62% done, 92% of the time gone. 4 wk behind\".\n- `className`: layout (width, margin). Never colour.\n\n## Props: PaceList\n\n- `rows`: `[{ id, label, sub, value, pace, kind, tone, by, byLabel, meta, note, group, unit }]`\n - `label`, `sub`: the name and the small line under it.\n - `value`: 0 to 1. `pace`: only when this row has a clock of its own (it\n gets its own tick); otherwise the shared line is its pace.\n - `tone`: as above; default `auto`.\n - `by`: who last moved it: `\"person\"` (a filled dot), `\"agent\"` (the agent's\n mark) or nothing. `byLabel`: what a screen reader hears (default \"A\n person\", \"The agent\").\n - `meta`: anything to put in the meta column (an owner's avatar, a count).\n Needs `metaWidth`.\n - `note`: replaces the gap words (\"1 wk behind · Likely\").\n - `group`: the `id` of a group.\n- `groups`: `[{ id, label, sub, note, empty }]` in the order to draw. A heading\n row carries the today line too. `empty` is the line shown when the group has\n no rows. A row whose `group` is not listed goes last, with no heading.\n- `start`, `end`, `today`: the period. They draw the ruler and the line.\n- `pace`: with no period, the shared line's place (0 to 1).\n- `kind`, `tolerance`, `graceDays`, `graceFraction`, `unit`, `warnAt`, `dangerAt`: how rows are\n judged (below). A row's own `kind` or `unit` wins.\n- `heads`: `{ label, meta, pct, gap }`: the column heads, shown from 42rem of\n the container's width.\n- `ruler`: draw the axis above the rows (default true).\n- `weekStartsOn`: weekday the week begins on, 0 = Monday (default).\n- `metaWidth`: a CSS length (\"8rem\") adds a meta column. Without it there is none.\n- `trackWidth` (default `\"24rem\"`, the most the bar takes), `gapWidth` (`\"9rem\"`).\n- `size`: bar thickness (default `sm`).\n- `selected`: the `id` of the row to mark as current.\n- `onOpen(row)`: makes each row a button. Leave it out for a read-only list.\n- `loading`, `error`, `onRetry`, `empty`, `emptyAction`: the other states: a\n skeleton; a danger notice with \"Try again\"; and, with no rows, an empty panel\n titled `empty`.\n- `label`, `className`.\n\nEvery row is one grid with the same columns, so the bars of all rows start and\nend at the same x: that is the shared axis. Under 42rem of its own width a row is\nits name on one line and its bar and percent under it.\n\n## Props: PaceRuler\n\n`start`, `end`, `today`, `weekStartsOn`, `words`, `showToday` (default true),\n`label`, `className`. It is 44px tall and as wide as its parent: put it in the\ncolumn your own rows share, and draw the line down from it with your rows'\ntoday position (`axisOf(...).todayAt`).\n\n## Props: WeekStrip\n\n- `start`, `end`, `today`, `weekStartsOn`.\n- `marks`: `[{ id, date, by, label }]`. A mark lands on the week its date falls\n in (before the first week: the first; after the last: the last). `by`:\n `\"person\"`, `\"agent\"`, or nothing (a hollow ring). More than three marks in a\n week show \"+n\".\n- `missed`: put a dashed ring on a past week with no mark at all.\n- `onOpen(week)`: makes each week a button (`{ n, from, to, state, marks }`).\n- `words`: `{ week: \"Week\" }`. `label`, `className`.\n\nThe strip keeps each week at least 1.5rem wide and scrolls sideways when the\nperiod has too many weeks for the container (focusable, so the keyboard scrolls\nit).\n\n## PaceMark\n\n`<PaceMark by=\"person\" | \"agent\" label empty missed size />`. Nothing for no\none; `empty` draws a hollow ring (a mark made by no one), `missed` a dashed one\n(a mark that should be there).\n\n## How it judges\n\n`paceOf({ value, pace, start, end, today, kind, tolerance, graceDays, graceFraction, unit })`\n(also exported, pure) returns the state:\n\n- `done`: progress at 100% or more.\n- `unjudged`: no value, no pace, the period has not started, or it is inside\n `graceDays` of the start (default 0; a quarterly tracker passes 14), or\n inside `graceFraction` of the period from its start (0..1; `0.2` is the\n first fifth, 19 days of a 92-day quarter). The longer of the two counts.\n- `on`: the gap is within `tolerance` whole points of the period (default 5).\n- `worse` / `better`: behind or ahead (progress); over or under (spend).\n\nThe gap is said in whole weeks, **at least 1**, from the period's length:\n62% against a pace of 92% in a 92-day quarter is `30` points, 27.6 days, \"4 wk\nbehind\". `unit` is `\"week\"` (default when there is a period), `\"day\"` or\n`\"point\"` (no period: \"30 pts behind\").\n\n## Words\n\nEvery string is the app's to change. `words` merges over these:\n\n```\non: \"On pace\" unjudged: \"Not judged yet\" done: \"Done\"\ntoday: \"Today\" before: \"Not started\" after: \"Ended\"\nunits: { week: \"wk\", day: \"d\", point: \"pts\" }\nbehind(n, unit) \"4 wk behind\" ahead(n, unit) \"2 wk ahead\"\nover(n, unit) \"34 pts over\" under(n, unit) \"17 pts under\"\nsentence({ done, gone }) \"57% done, 88% of the time gone\"\nspendSentence({ done, gone }) \"91% used, 57% of the time gone\"\ngone(n) \"92% gone\"\n```\n\nAlso exported, pure and without React, for a screen that wants the same numbers\nin a sentence: `paceOf`, `paceWords(state, words)`, `paceSentence(state,\nwords)`, `toneOf(state, { warnAt, dangerAt })`, `periodOf`, `axisOf`,\n`weeksOf`, `marksOnWeeks`, `groupRows`, `dayNumber`, `isoOf`, `dayName`,\n`shortDay`, `percent`, `WORDS`.\n\n## Example\n\n```jsx\n<PaceList\n label=\"Q3 goals\"\n start=\"2026-07-01\"\n end=\"2026-09-30\"\n today=\"2026-09-23\"\n graceDays={14}\n heads={{ label: \"Goal\", pct: \"Done\", gap: \"Against today\" }}\n metaWidth=\"7rem\"\n groups={[{ id: \"sales\", label: \"Sales\", sub: \"2 goals\" }, { id: \"eng\", label: \"Engineering\", sub: \"1 goal\" }]}\n rows={[\n { id: \"a\", group: \"sales\", label: \"Close the enterprise gap\", value: 0.62, by: \"person\", meta: owner(\"Priya\") },\n { id: \"b\", group: \"sales\", label: \"Turn demos into a motion\", value: 0.87 },\n { id: \"c\", group: \"eng\", label: \"Keep the rate API fast\", value: 0.9, by: \"agent\" },\n ]}\n onOpen={(row) => router.push(`?goal=${row.id}`)}\n/>\n\n<WeekStrip start=\"2026-07-01\" end=\"2026-09-30\" today=\"2026-09-23\" missed\n marks={[{ id: \"m1\", date: \"2026-09-14\", by: \"person\" }, { id: \"m2\", date: \"2026-09-21\", by: \"agent\" }]} />\n\n// \"57% done, 88% of the time gone\"\n<p>{paceSentence(paceOf({ value: 0.57, pace: 0.88 }))}</p>\n\n// A budget: 91% of the hours used with 57% of the time gone\n<PaceBar kind=\"spend\" value={0.91} pace={0.57} tone=\"danger\" label=\"Hours\" />\n```\n\n## States drawn\n\n- **Empty**: a panel with what to do. With groups and no rows, each heading says\n its `empty` line.\n- **One row**, **many rows**, **grouped**.\n- **Very long names**: the name wraps (rows grow); the bar's column is unchanged.\n- **Narrow** (a 320px container): a row is its name above its bar and percent;\n the ruler is under the column heads.\n- **Wide**: the bar column keeps its `trackWidth`; the name takes the rest.\n- **On pace, behind, ahead, done, not judged yet, before the period, after it.**\n- **Spend**: over the pace hatches from the tick to the fill.\n- **Loading**, **failed** (with Try again), **read-only** (no `onOpen`).\n- Reduced motion: nothing moves unless `animate` is set, and then not under it.\n\n## Notes for the app that places it\n\n- It is a picture. Put the numbers in words beside it (the list does: the\n percent and the gap words are in every row).\n- Colour is a signal. The fill is ink unless a row is behind (warning, danger)\n or done (success). Do not tint rows for any other reason.\n- For a goals list, give each group a `note` such as \"2 need attention\" and\n compute it from `paceOf(...).state` yourself; the block does not know your\n words for \"needs attention\".\n- A row with `by` shows who last moved it; keep the dates in your detail view.\n"
36
+ }
37
+ ],
38
+ "categories": [
39
+ "pace",
40
+ "progress",
41
+ "bar",
42
+ "target",
43
+ "behind",
44
+ "ahead",
45
+ "on track",
46
+ "today line",
47
+ "quarter",
48
+ "okr",
49
+ "goals",
50
+ "key results",
51
+ "burn",
52
+ "budget",
53
+ "spend",
54
+ "sprint",
55
+ "weeks behind",
56
+ "check-in",
57
+ "cadence",
58
+ "deadline",
59
+ "ruler",
60
+ "agent"
61
+ ],
62
+ "docs": "Installed to components/makersclaw/blocks/pace-bar. Import: import { PaceBar, PaceList, PaceRuler, WeekStrip, PaceMark, WORDS, atDay, axisOf, dayName, dayNumber, groupRows, isoOf, marksOnWeeks, paceOf, paceSentence, paceWords, percent, periodOf, shortDay, toneOf, weeksOf } from \"@/components/makersclaw/blocks/pace-bar\". Props and an example: components/makersclaw/blocks/pace-bar/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
63
+ "meta": {
64
+ "makersclaw": {
65
+ "version": "0.8.1",
66
+ "useWhen": "Any screen where progress belongs to a period and the question is 'are we on pace': a quarter's goals and their key results, a project's work done against its days gone, hours burned against hours sold, a sprint, a savings or budget target, a course or a reading plan, a campaign's delivery against its flight dates, a weekly check-in cadence. Draws a result the app has already worked out; it does not collect check-ins. Not for a chain of dated phases with gates (schedule-ruler), editable bars with dependencies (gantt), a plain meter with no clock (Progress in the kit), or a time series (chart)."
67
+ }
68
+ }
69
+ }
@@ -5,7 +5,7 @@
5
5
  "title": "PageBuilder",
6
6
  "description": "A visual page editor (GrapesJS core) in the workspace theme: our own clean panels instead of GrapesJS's chrome — an Add panel of section and basic blocks (click or drag), a Layers tree, a Design panel limited to theme-token choices (colours, type, spacing, width, corners, borders), a device toggle (desktop, tablet, phone), undo/redo, preview, and a friendly empty state with starter templates. Pages style with CSS variables, so a saved page repaints with the theme. Saves out as { html, css, project }; PageView shows a saved page in an app.",
7
7
  "registryDependencies": [
8
- "https://cdn.jsdelivr.net/npm/makersclaw@0.7.0/r/core.json"
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json"
9
9
  ],
10
10
  "dependencies": [
11
11
  "grapesjs@0.22.16",
@@ -76,7 +76,7 @@
76
76
  "path": "registry/blocks/page-builder/README.md",
77
77
  "type": "registry:file",
78
78
  "target": "@components/makersclaw/blocks/page-builder/README.md",
79
- "content": "# PageBuilder\n\nA visual page editor in the workspace theme, calm like Webflow's canvas\nwithout its cockpit. The page sits in the middle at desktop, tablet or\nphone width. On the left, **Add** lists blocks as small wireframes —\nsections (hero, features, logos, quote, pricing, questions, call to action,\nfooter) and basics (heading, text, button, image, two columns, list,\ndivider, space). Click one to add it below the selection, or drag it onto\nthe page. **Layers** shows the page as a tree: select, fold, hide while\nediting. On the right, **Design** changes the selected element's look,\nlimited to the theme's choices: text colour, background, text size,\nweight, typeface, alignment, spacing, width, corners and border. It also\nedits a link's address or a picture's source and description, and moves,\nduplicates or deletes the element. The top bar has the save state, the\ndevice toggle, undo and redo, Preview and Save. A new page opens on a\nfriendly empty state with starter templates. On a narrow screen the\npanels move to a bottom bar and open as sheets.\n\nIt is [GrapesJS](https://github.com/GrapesJS/grapesjs) (BSD-3-Clause, the\nfree core — never the commercial Studio SDK) with its React wrapper\n[@grapesjs/react](https://github.com/GrapesJS/react) (MIT). GrapesJS does\nthe page model, the canvas, selection, drag and drop, text editing, undo\nand the export. The panels, the top bar, the empty state and the Design\nchoices are ours, built from the kit. GrapesJS's own panels, style manager,\ntrait manager, asset modal and code viewer are never created. The blocks\nbuilt on this one — `landing-builder` first — import it, never `grapesjs`\ndirectly.\n\n```jsx\nimport { PageBuilder, PageView } from \"../components/blocks/page-builder\"; // a Makersclaw app (\"./blocks/page-builder\" from components/)\nimport { PageBuilder, PageView } from \"@/components/makersclaw/blocks/page-builder\"; // a shadcn project\n```\n\n`PageBuilder` is a client component: render it from a `\"use client\"`\ncomponent. `PageView` and the helpers (`pageDocument`, `cleanHtml`,\n`cleanCss`, `isPageEmpty`, `projectSections`…) work on the server too.\n\n`makersclaw add page-builder` adds `grapesjs` and `@grapesjs/react`\n(pinned, exact) to package.json: run `npm install` after. They load with\nthe page, with no `import()`. The editor is created after the first render.\nUntil then, and on the server, the frame shows a skeleton.\n\n## The data\n\nThe builder hands out a page as three things. You get it from `onSave`,\nfrom `onChange`, and from the handle's `getPage()`:\n\n```js\n{\n html: \"<section class=\\\"pb-section pb-center\\\" …>…</section>…\", // the body's content, cleaned\n css: \".pb-section{padding:88px 24px;background-color:var(--color-background);…}#i4kd{…}\",\n project: { pages: [ … ], styles: [ … ], assets: [], … }, // GrapesJS project JSON\n}\n```\n\n- **Store `project`** (a `jsonb` column). It is what reopens the page for\n editing: pass it back as `page={{ project }}`.\n- **Show `html` + `css`** with `PageView`, or publish them. They are the\n source of truth for what visitors see. They are derived from `project`\n at save time, so store them next to it instead of recomputing them.\n- `page={{ html, css }}` also opens, as a fresh project. This is how a\n template, an import or an agent-written page starts.\n- The HTML is cleaned: no `<script>`, no `on*` handlers, no `javascript:`\n links. The CSS has no `body`, `html` or `*` rules. Only rules the page\n uses are exported.\n\n### Pages keep the theme\n\n**A page never holds a colour. It names theme tokens, so it repaints when\nthe theme changes.** Every Design choice writes a CSS variable:\n`var(--color-primary)`, `var(--color-muted-foreground)`,\n`var(--theme-font-display)`, `var(--theme-radius)`. These are the same\nvariables the app's Tailwind utilities read. The built-in blocks' CSS\n(`BASE_CSS`, `pb-` classes) uses them too. Sizes and spacing are plain\n`rem`/`px` steps.\n\n- **In the editor**, the canvas is an iframe that cannot see the app's\n stylesheet. The builder resolves each token where it sits (its light or\n dark scheme and any `[data-theme]` scope) and writes the values into the\n frame. It does this again when the theme changes. It also copies the\n app's `@font-face` rules, so the page uses the theme's typefaces.\n- **In the app**, `PageView` shows the page. The tokens resolve to the\n workspace theme. Tailwind emits a theme variable only when some source\n names it, so this block names every token a page may use\n (`KEEP_TOKENS`). Installing the block is enough.\n- **Outside the app** (a download, a plain file host),\n `standaloneDocument(page, { root })` returns a full HTML document with\n the tokens resolved and inlined. `pageDocument(page, { tokens })` is the\n same function for the server, if you pass it the `:root{…}` string\n yourself.\n\n## Props\n\n| Prop | Default | |\n| --- | --- | --- |\n| `page` | empty | The page to open: `{ project }` or `{ html, css }`. Read once, when the editor is created. To open another page, change the component's `key` or call the handle's `setPage`. |\n| `onSave` | — | `async (page) => ({ ok, error? })`. Called by Save and ⌘/Ctrl+S with `{ html, css, project }`. On `{ ok: false }`, the page stays unsaved and a toast shows your `error` with \"Try again\". The top bar says \"Not saved\". |\n| `onChange` | — | `(page) => void`, 500 ms after the last edit. Use it for autosave or a live preview elsewhere. |\n| `title` | `\"Untitled page\"` | The name in the top bar. |\n| `blocks` | `[]` | Your own blocks, added after the built-ins. See \"Your own sections\". |\n| `builtIns` | `true` | Which built-in blocks to offer: `true` (sections and basics), `\"sections\"`, `\"basics\"` or `false`. |\n| `templates` | `STARTER_TEMPLATES` | Whole pages offered in the empty state and in \"Start from a template\". Pass `[]` for none. |\n| `styles` | `STYLE_GROUPS` | The Design panel's choices. See \"The Design choices\". |\n| `onUploadImage` | — | `async (file) => ({ ok, url, error? })`. When given, a picture's field gets an Upload button. Without it, people paste an address. |\n| `actions` | — | Extra top-bar controls before Save, such as landing-builder's Publish. Use kit `Button`s at `size=\"sm\"`. |\n| `saveLabel` | `\"Save\"` | The Save button's words. |\n| `builderRef` | — | A ref from `usePageBuilder()`. It receives the handle. |\n| `height` | `720` | The builder's height: a number (px) or any CSS length (`\"min(760px, 88dvh)\"`). It fills its width. |\n| `className` | — | Added to the frame. |\n\n### The handle\n\n```jsx\nconst builder = usePageBuilder();\n<PageBuilder builderRef={builder} … />\nbuilder.current?.getPage(); // { html, css, project }\n```\n\n`getPage()` · `save()` (runs `onSave`, returns its result) ·\n`addBlock(id)` · `loadTemplate(idOrTemplate)` · `setPage({ html, css } |\n{ project })` · `undo()` · `redo()` · `setDevice(\"desktop\" | \"tablet\" |\n\"mobile\")` · `preview(on = true)` · `editor` (the GrapesJS editor, for what\nthis block does not wrap; changes made through it are yours to keep\ntoken-only).\n\n## Your own sections\n\nThis is how `landing-builder` builds on this block. A block is plain data:\n\n```js\nconst HERO_WAITLIST = {\n id: \"hero-waitlist\", // unique across built-ins and yours\n label: \"Waitlist hero\", // the tile's name\n category: \"Hero\", // the Add panel's group heading\n thumb: \"split\", // a wireframe from THUMB_NAMES\n description: \"Headline, one line and an email button.\", // the tile's tooltip\n level: \"section\", // \"section\": added between top-level sections ·\n // \"element\": added inside the selected box\n // (default: \"section\" when category is \"Sections\")\n html: `<section class=\"lb-hero\" data-gjs-name=\"Waitlist hero\">…</section>`,\n css: `.lb-hero{padding:96px 24px;background-color:var(--color-muted);color:var(--color-foreground)}`,\n};\n<PageBuilder blocks={[HERO_WAITLIST]} builtIns=\"basics\" templates={LANDING_TEMPLATES} />\n```\n\n- `html` is the markup, or use `content`, a GrapesJS component definition,\n in its place. `data-gjs-name` names the element in Layers. Any\n `data-gjs-*` attribute sets that GrapesJS property: for example,\n `data-gjs-draggable=\"false\"` or `data-gjs-removable=\"false\"` lock a part.\n- `css` is added to the page once, when the editor opens. **Name only\n tokens for colour** (`var(--color-…)`, from `COLOR_TOKENS`) and\n `--theme-font-*` / `--theme-radius` for faces and corners. Then your\n sections repaint with the theme. Prefix your classes (`lb-`), and reuse\n the built-in `pb-` classes (`pb-button`, `pb-h2`, `pb-lead`,\n `pb-container`…) when they fit. Add an `@media (max-width: 768px)` rule\n for the phone layout. The canvas's phone width is a real narrow frame,\n so media queries work there.\n- Several blocks may share one `css` string. The same rule added twice is\n one rule.\n\nA **template** is a whole page:\n`{ id, label, description?, thumbs?: [\"hero\", \"features\", \"cta\"], html, css? }`\nor `{ id, label, project }`. `thumbs` draws its card, up to four sections\ntall. Picking a template replaces the page. A toast offers Undo when there\nwas something to replace.\n\n`BlockThumb` and `PageThumb` draw the same wireframes elsewhere, for\nexample in a landing-builder gallery of templates.\n\n## The Design choices\n\n`STYLE_GROUPS` is a list of groups. Each group is one row of choices:\n\n```js\n{\n id: \"fill\", label: \"Background\", kind: \"swatch\" | \"segmented\" | \"select\",\n for: [\"link\", \"box\"], // which elements show it: text · link · image · box (all if left out)\n match: \"background-color\", // the property read back to show the current choice\n options: [\n { value: \"\", label: \"Default\", css: { \"background-color\": \"\" } }, // \"\" = reset: the block's own CSS applies\n { value: \"primary\", label: \"Primary\", css: { \"background-color\": \"var(--color-primary)\" } },\n ],\n}\n```\n\nPicking an option writes its `css` onto that element only (an `#id`\nrule). Any property another option of the group sets is cleared. \"Reset\"\nbeside a group removes the group's properties. \"Clear this element's\nstyle\" removes all of them. To offer fewer or different choices, pass\nyour own list, filtered from or modelled on `STYLE_GROUPS`. A swatch\noption's `value` is the token name it paints (`primary` →\n`var(--color-primary)`). `groupsFor`, `currentOption` and `patchFor` are\nthe helpers the panel uses.\n\n## Mouse, keyboard and touch\n\n- Click an element to select it. Double-click text to edit the words\n (bold, italic and link in the small toolbar). Double-click a picture to\n jump to its field. Drag the selected element's handle to move it.\n- ⌘/Ctrl+S saves. ⌘/Ctrl+Z undoes, ⇧⌘Z redoes. Backspace or Delete\n removes the selection (with Undo in a toast when done from the panel).\n Esc leaves Preview.\n- The block tiles, the layer tree (↑ ↓ to move, ← → to fold), the swatches\n (arrow keys) and every control are reachable by keyboard. A tile adds\n on Enter.\n- On a phone, tap a tile to add it. Dragging is for a mouse.\n\n## Example\n\n```jsx\n// surfaces/landing.jsx — client: edit one page\n\"use client\";\nimport { useSearchParams } from \"next/navigation\";\nimport { useRow } from \"../lib/data\";\nimport { updateRow } from \"../lib/actions\";\nimport { PageBuilder, PageView } from \"../components/blocks/page-builder\";\n\nexport default function LandingEditor() {\n const id = useSearchParams().get(\"row\");\n const { row, loading } = useRow({ source: \"pages\", id });\n if (loading || !row) return null;\n return (\n <PageBuilder\n key={row.id}\n title={row.title}\n page={row.project ? { project: row.project } : { html: row.html, css: row.css }}\n onSave={async ({ html, css, project }) => {\n const res = await updateRow({ source: \"pages\", id: row.id, patch: { html, css, project } });\n return res.ok ? { ok: true } : { ok: false, error: \"The page was not saved. Check your connection and try again.\" };\n }}\n />\n );\n}\n\n// The finished page, for a surface of its own that reads the row with the same useRow:\nexport function LandingPage({ row }) {\n return <PageView page={row} label={row.title} />;\n}\n```\n\nIn a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.\n\n## What it will not do\n\n- No live multiplayer. Two people editing the same page is last save wins.\n- No free colour picker, hex field or arbitrary CSS. The Design panel\n offers the theme's choices only, by design. A block's own `css` is where\n a fixed design lives.\n- Styles apply at every width. The canvas's device toggle previews the\n page's own media queries. It does not keep per-device overrides.\n- No forms that submit, and no scripts. The HTML is cleaned of them. A\n signup form belongs to the app (a block beside `PageView`), not to the\n page.\n- No asset library. Pictures are an address, or an upload through\n `onUploadImage`.\n- No publishing, domains or SEO fields. Those belong to `landing-builder`\n and the app.\n\n## Credits\n\nGrapesJS © Artur Arseniev, BSD-3-Clause. @grapesjs/react © Artur\nArseniev, MIT. Both are used unmodified, as npm dependencies. The block\nships none of their code. GrapesJS's stylesheet is imported from the\npackage and repainted with the workspace's tokens.\n"
79
+ "content": "# PageBuilder\n\nA visual page editor in the workspace theme, calm like Webflow's canvas\nwithout its cockpit. The page sits in the middle at desktop, tablet or\nphone width. On the left, **Add** lists blocks as small wireframes —\nsections (hero, features, logos, quote, pricing, questions, call to action,\nfooter) and basics (heading, text, button, image, two columns, list,\ndivider, space). Click one to add it below the selection, or drag it onto\nthe page. **Layers** shows the page as a tree: select, fold, hide while\nediting. On the right, **Design** changes the selected element's look,\nlimited to the theme's choices: text colour, background, text size,\nweight, typeface, alignment, spacing, width, corners and border. It also\nedits a link's address or a picture's source and description, and moves,\nduplicates or deletes the element. The top bar has the save state, the\ndevice toggle, undo and redo, Preview and Save. A new page opens on a\nfriendly empty state with starter templates. On a narrow screen the\npanels move to a bottom bar and open as sheets.\n\nIt is [GrapesJS](https://github.com/GrapesJS/grapesjs) (BSD-3-Clause, the\nfree core — never the commercial Studio SDK) with its React wrapper\n[@grapesjs/react](https://github.com/GrapesJS/react) (MIT). GrapesJS does\nthe page model, the canvas, selection, drag and drop, text editing, undo\nand the export. The panels, the top bar, the empty state and the Design\nchoices are ours, built from the kit. GrapesJS's own panels, style manager,\ntrait manager, asset modal and code viewer are never created. The blocks\nbuilt on this one — `landing-builder` first — import it, never `grapesjs`\ndirectly.\n\n```jsx\nimport { PageBuilder, PageView } from \"../components/blocks/page-builder\"; // a Makersclaw app (\"./blocks/page-builder\" from components/)\nimport { PageBuilder, PageView } from \"@/components/makersclaw/blocks/page-builder\"; // a shadcn project\n```\n\n`PageBuilder` is a client component: render it from a `\"use client\"`\ncomponent. `PageView` and the helpers (`pageDocument`, `cleanHtml`,\n`cleanCss`, `isPageEmpty`, `projectSections`…) work on the server too.\n\n`makersclaw add page-builder` adds `grapesjs` and `@grapesjs/react`\n(pinned, exact) to package.json: run `npm install` after. They load with\nthe page, with no `import()`. The editor is created after the first render.\nUntil then, and on the server, the frame shows a skeleton.\n\n## The data\n\nThe builder hands out a page as three things. You get it from `onSave`,\nfrom `onChange`, and from the handle's `getPage()`:\n\n```js\n{\n html: \"<section class=\\\"pb-section pb-center\\\" …>…</section>…\", // the body's content, cleaned\n css: \".pb-section{padding:88px 24px;background-color:var(--color-background);…}#i4kd{…}\",\n project: { pages: [ … ], styles: [ … ], assets: [], … }, // GrapesJS project JSON\n}\n```\n\n- **Store `project`** (a `jsonb` column). It is what reopens the page for\n editing: pass it back as `page={{ project }}`.\n- **Show `html` + `css`** with `PageView`, or publish them. They are the\n source of truth for what visitors see. They are derived from `project`\n at save time, so store them next to it instead of recomputing them.\n- `page={{ html, css }}` also opens, as a fresh project. This is how a\n template, an import or an agent-written page starts.\n- The HTML is cleaned: no `<script>`, no `on*` handlers, no `javascript:`\n links. The CSS has no `body`, `html` or `*` rules. Only rules the page\n uses are exported.\n\n### Pages keep the theme\n\n**A page never holds a colour. It names theme tokens, so it repaints when\nthe theme changes.** Every Design choice writes a CSS variable:\n`var(--color-primary)`, `var(--color-muted-foreground)`,\n`var(--theme-font-display)`, `var(--theme-radius)`. These are the same\nvariables the app's Tailwind utilities read. The built-in blocks' CSS\n(`BASE_CSS`, `pb-` classes) uses them too. Sizes and spacing are plain\n`rem`/`px` steps.\n\n- **In the editor**, the canvas is an iframe that cannot see the app's\n stylesheet. The builder resolves each token where it sits (its light or\n dark scheme and any `[data-theme]` scope) and writes the values into the\n frame. It does this again when the theme changes. It also copies the\n app's `@font-face` rules, so the page uses the theme's typefaces.\n- **In the app**, `PageView` shows the page. The tokens resolve to the\n workspace theme. Tailwind emits a theme variable only when some source\n names it, so this block names every token a page may use\n (`KEEP_TOKENS`). Installing the block is enough.\n- **Outside the app** (a download, a plain file host),\n `standaloneDocument(page, { root })` returns a full HTML document with\n the tokens resolved and inlined. `pageDocument(page, { tokens })` is the\n same function for the server, if you pass it the `:root{…}` string\n yourself.\n\n## Props\n\n| Prop | Default | |\n| --- | --- | --- |\n| `page` | empty | The page to open: `{ project }` or `{ html, css }`. Read once, when the editor is created. To open another page, change the component's `key` or call the handle's `setPage`. |\n| `onSave` | — | `async (page) => ({ ok, error? })`. Called by Save and ⌘/Ctrl+S with `{ html, css, project }`. On `{ ok: false }`, the page stays unsaved and a toast shows your `error` with \"Try again\". The top bar says \"Not saved\". |\n| `onChange` | — | `(page) => void`, 500 ms after the last edit. Use it for autosave or a live preview elsewhere. |\n| `title` | `\"Untitled page\"` | The name in the top bar. |\n| `blocks` | `[]` | Your own blocks, added after the built-ins. See \"Your own sections\". |\n| `builtIns` | `true` | Which built-in blocks to offer: `true` (sections and basics), `\"sections\"`, `\"basics\"` or `false`. |\n| `templates` | `STARTER_TEMPLATES` | Whole pages offered in the empty state and in \"Start from a template\". Pass `[]` for none. |\n| `styles` | `STYLE_GROUPS` | The Design panel's choices. See \"The Design choices\". |\n| `onUploadImage` | — | `async (file) => ({ ok, url, error? })`. When given, a picture's field gets an Upload button. Without it, people paste an address. |\n| `actions` | — | Extra top-bar controls before Save, such as landing-builder's Publish. Use kit `Button`s at `size=\"sm\"`. |\n| `saveLabel` | `\"Save\"` | The Save button's words. |\n| `builderRef` | — | A ref from `usePageBuilder()`. It receives the handle. |\n| `height` | `720` | The builder's height: a number (px) or any CSS length (`\"100%\"` in a sized box, such as the kit's `AppLayout contentMode=\"fill\"`; never a `vh` length, which `npm run check` refuses). It fills its width. |\n| `className` | — | Added to the frame. |\n\n### The handle\n\n```jsx\nconst builder = usePageBuilder();\n<PageBuilder builderRef={builder} … />\nbuilder.current?.getPage(); // { html, css, project }\n```\n\n`getPage()` · `save()` (runs `onSave`, returns its result) ·\n`addBlock(id)` · `loadTemplate(idOrTemplate)` · `setPage({ html, css } |\n{ project })` · `undo()` · `redo()` · `setDevice(\"desktop\" | \"tablet\" |\n\"mobile\")` · `preview(on = true)` · `editor` (the GrapesJS editor, for what\nthis block does not wrap; changes made through it are yours to keep\ntoken-only).\n\n## Your own sections\n\nThis is how `landing-builder` builds on this block. A block is plain data:\n\n```js\nconst HERO_WAITLIST = {\n id: \"hero-waitlist\", // unique across built-ins and yours\n label: \"Waitlist hero\", // the tile's name\n category: \"Hero\", // the Add panel's group heading\n thumb: \"split\", // a wireframe from THUMB_NAMES\n description: \"Headline, one line and an email button.\", // the tile's tooltip\n level: \"section\", // \"section\": added between top-level sections ·\n // \"element\": added inside the selected box\n // (default: \"section\" when category is \"Sections\")\n html: `<section class=\"lb-hero\" data-gjs-name=\"Waitlist hero\">…</section>`,\n css: `.lb-hero{padding:96px 24px;background-color:var(--color-muted);color:var(--color-foreground)}`,\n};\n<PageBuilder blocks={[HERO_WAITLIST]} builtIns=\"basics\" templates={LANDING_TEMPLATES} />\n```\n\n- `html` is the markup, or use `content`, a GrapesJS component definition,\n in its place. `data-gjs-name` names the element in Layers. Any\n `data-gjs-*` attribute sets that GrapesJS property: for example,\n `data-gjs-draggable=\"false\"` or `data-gjs-removable=\"false\"` lock a part.\n- `css` is added to the page once, when the editor opens. **Name only\n tokens for colour** (`var(--color-…)`, from `COLOR_TOKENS`) and\n `--theme-font-*` / `--theme-radius` for faces and corners. Then your\n sections repaint with the theme. Prefix your classes (`lb-`), and reuse\n the built-in `pb-` classes (`pb-button`, `pb-h2`, `pb-lead`,\n `pb-container`…) when they fit. Add an `@media (max-width: 768px)` rule\n for the phone layout. The canvas's phone width is a real narrow frame,\n so media queries work there.\n- Several blocks may share one `css` string. The same rule added twice is\n one rule.\n\nA **template** is a whole page:\n`{ id, label, description?, thumbs?: [\"hero\", \"features\", \"cta\"], html, css? }`\nor `{ id, label, project }`. `thumbs` draws its card, up to four sections\ntall. Picking a template replaces the page. A toast offers Undo when there\nwas something to replace.\n\n`BlockThumb` and `PageThumb` draw the same wireframes elsewhere, for\nexample in a landing-builder gallery of templates.\n\n## The Design choices\n\n`STYLE_GROUPS` is a list of groups. Each group is one row of choices:\n\n```js\n{\n id: \"fill\", label: \"Background\", kind: \"swatch\" | \"segmented\" | \"select\",\n for: [\"link\", \"box\"], // which elements show it: text · link · image · box (all if left out)\n match: \"background-color\", // the property read back to show the current choice\n options: [\n { value: \"\", label: \"Default\", css: { \"background-color\": \"\" } }, // \"\" = reset: the block's own CSS applies\n { value: \"primary\", label: \"Primary\", css: { \"background-color\": \"var(--color-primary)\" } },\n ],\n}\n```\n\nPicking an option writes its `css` onto that element only (an `#id`\nrule). Any property another option of the group sets is cleared. \"Reset\"\nbeside a group removes the group's properties. \"Clear this element's\nstyle\" removes all of them. To offer fewer or different choices, pass\nyour own list, filtered from or modelled on `STYLE_GROUPS`. A swatch\noption's `value` is the token name it paints (`primary` →\n`var(--color-primary)`). `groupsFor`, `currentOption` and `patchFor` are\nthe helpers the panel uses.\n\n## Mouse, keyboard and touch\n\n- Click an element to select it. Double-click text to edit the words\n (bold, italic and link in the small toolbar). Double-click a picture to\n jump to its field. Drag the selected element's handle to move it.\n- ⌘/Ctrl+S saves. ⌘/Ctrl+Z undoes, ⇧⌘Z redoes. Backspace or Delete\n removes the selection (with Undo in a toast when done from the panel).\n Esc leaves Preview.\n- The block tiles, the layer tree (↑ ↓ to move, ← → to fold), the swatches\n (arrow keys) and every control are reachable by keyboard. A tile adds\n on Enter.\n- On a phone, tap a tile to add it. Dragging is for a mouse.\n\n## Example\n\n```jsx\n// surfaces/landing.jsx — client: edit one page\n\"use client\";\nimport { useSearchParams } from \"next/navigation\";\nimport { useRow } from \"../lib/data\";\nimport { updateRow } from \"../lib/actions\";\nimport { PageBuilder, PageView } from \"../components/blocks/page-builder\";\n\nexport default function LandingEditor() {\n const id = useSearchParams().get(\"row\");\n const { row, loading } = useRow({ source: \"pages\", id });\n if (loading || !row) return null;\n return (\n <PageBuilder\n key={row.id}\n title={row.title}\n page={row.project ? { project: row.project } : { html: row.html, css: row.css }}\n onSave={async ({ html, css, project }) => {\n const res = await updateRow({ source: \"pages\", id: row.id, patch: { html, css, project } });\n return res.ok ? { ok: true } : { ok: false, error: \"The page was not saved. Check your connection and try again.\" };\n }}\n />\n );\n}\n\n// The finished page, for a surface of its own that reads the row with the same useRow:\nexport function LandingPage({ row }) {\n return <PageView page={row} label={row.title} />;\n}\n```\n\nIn a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.\n\n## What it will not do\n\n- No live multiplayer. Two people editing the same page is last save wins.\n- No free colour picker, hex field or arbitrary CSS. The Design panel\n offers the theme's choices only, by design. A block's own `css` is where\n a fixed design lives.\n- Styles apply at every width. The canvas's device toggle previews the\n page's own media queries. It does not keep per-device overrides.\n- No forms that submit, and no scripts. The HTML is cleaned of them. A\n signup form belongs to the app (a block beside `PageView`), not to the\n page.\n- No asset library. Pictures are an address, or an upload through\n `onUploadImage`.\n- No publishing, domains or SEO fields. Those belong to `landing-builder`\n and the app.\n\n## Credits\n\nGrapesJS © Artur Arseniev, BSD-3-Clause. @grapesjs/react © Artur\nArseniev, MIT. Both are used unmodified, as npm dependencies. The block\nships none of their code. GrapesJS's stylesheet is imported from the\npackage and repainted with the workspace's tokens.\n"
80
80
  }
81
81
  ],
82
82
  "categories": [
@@ -98,7 +98,7 @@
98
98
  "docs": "Installed to components/makersclaw/blocks/page-builder. Import: import { PageBuilder, usePageBuilder, readPage, DEVICES, PageView, standaloneDocument, BlockThumb, PageThumb, THUMB_NAMES, BASE_CSS, SECTION_BLOCKS, BASIC_BLOCKS, STARTER_TEMPLATES, STYLE_GROUPS, COLOR_TOKENS, groupsFor, currentOption, patchFor, tokenCss, fontFaceCss, fontLinks, PAGE_BASE_CSS, emptyPage, isPageEmpty, cleanHtml, cleanCss, normalizeBlock, templatePage, pageDocument, projectSections } from \"@/components/makersclaw/blocks/page-builder\". Props and an example: components/makersclaw/blocks/page-builder/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
99
99
  "meta": {
100
100
  "makersclaw": {
101
- "version": "0.7.0",
101
+ "version": "0.8.1",
102
102
  "useWhen": "A screen where someone builds or edits a web page visually: a landing page, a waitlist or launch page, a simple site section, an announcement page. landing-builder is built on it (section templates, publishing). Not for rich text in a document (doc-editor), an email body (newsletter-builder), a form of fields (record-form) or a freeform drawing (sketch-canvas)."
103
103
  }
104
104
  }
@@ -5,7 +5,7 @@
5
5
  "title": "PaymentMethods",
6
6
  "description": "A customer's SAVED payment methods as cards: brand, last four digits, expiry (expired and expiring soon flagged) and a Default badge; Make default, Remove behind a Confirm, and Add, which hands off to the payment provider (Paddle). Never a card-number field. Empty, loading and error states built in.",
7
7
  "registryDependencies": [
8
- "https://cdn.jsdelivr.net/npm/makersclaw@0.7.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": [
@@ -44,7 +44,7 @@
44
44
  "docs": "Installed to components/makersclaw/blocks/payment-methods. Import: import { PaymentMethods } from \"@/components/makersclaw/blocks/payment-methods\". Props and an example: components/makersclaw/blocks/payment-methods/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
45
45
  "meta": {
46
46
  "makersclaw": {
47
- "version": "0.7.0",
47
+ "version": "0.8.1",
48
48
  "useWhen": "A billing or account page where a customer manages the cards and wallets on file. Not for taking a payment (cart's checkout step hands off to Paddle) or for a list of charges (transaction-list)."
49
49
  }
50
50
  }
@@ -5,7 +5,7 @@
5
5
  "title": "PdfDocument",
6
6
  "description": "Make PDFs in the workspace theme (@react-pdf/renderer): a small set of document primitives (headings, text, label/value fields, tables with repeating headers, totals, callouts, badges, rows and columns) and a clean ready-made quote / estimate / invoice layout, rendered in the browser to a Blob with renderPdf(doc). The theme's colours are read from the page when the PDF is made (a PDF cannot read CSS variables): white paper, near-black ink and the theme's primary as the one accent. Also a live-preview hook (usePdf), a Download PDF button, and the quote sums and money formatting as plain functions.",
7
7
  "registryDependencies": [
8
- "https://cdn.jsdelivr.net/npm/makersclaw@0.7.0/r/core.json"
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json"
9
9
  ],
10
10
  "dependencies": [
11
11
  "@react-pdf/renderer@4.9.0"
@@ -72,7 +72,7 @@
72
72
  "docs": "Installed to components/makersclaw/blocks/pdf-document. Import: import { renderPdf, usePdf, PdfDownloadButton, downloadBlob, registerPdfFont, PdfDocument, PdfStack, PdfRow, PdfCol, PdfText, PdfHeading, PdfLabel, PdfField, PdfDivider, PdfCallout, PdfBadge, PdfTable, PdfTotals, PdfLines, PdfView, PdfRawText, PdfImage, PdfLink, PdfThemeProvider, usePdfTheme, PDF_TYPE, QuoteDocument, quoteTotals, lineAmount, round2, formatMoney, formatQuantity, formatPdfDate, resolvePdfTheme, buildPdfTheme, DEFAULT_PDF_THEME } from \"@/components/makersclaw/blocks/pdf-document\". Props and an example: components/makersclaw/blocks/pdf-document/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
73
73
  "meta": {
74
74
  "makersclaw": {
75
- "version": "0.7.0",
75
+ "version": "0.8.1",
76
76
  "useWhen": "A screen produces a document someone downloads, prints, emails or signs: a quote, an invoice, an estimate, a receipt, a statement, a certificate, a one-page report. The quote-builder and proposal blocks print through it. Not for showing an existing PDF on screen (pdf-viewer) or for a web page people read in the app (doc-page)."
77
77
  }
78
78
  }
package/r/pdf-viewer.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "title": "PdfViewer",
6
6
  "description": "A PDF on screen, in the workspace theme (react-pdf + pdf.js): pages in one smooth scrolling column (only the pages near the window are drawn, so long files stay fast), a thumbnail rail, a page box, zoom by steps, fit width, fit page and Ctrl/⌘ + wheel or pinch, find in the text with every match marked, and highlights with notes: select text, choose Highlight or Add note, and it saves through onHighlight; notes are edited in a side list, colours are theme tokens, delete has Undo. Opens a URL, a Blob or bytes; password-protected files ask for the password; a narrow viewer turns its rails into sheets. The pdf.js worker ships with the app (no public/ file, no CDN).",
7
7
  "registryDependencies": [
8
- "https://cdn.jsdelivr.net/npm/makersclaw@0.7.0/r/core.json"
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json"
9
9
  ],
10
10
  "dependencies": [
11
11
  "react-pdf@11.0.0",
@@ -58,7 +58,7 @@
58
58
  "path": "registry/blocks/pdf-viewer/README.md",
59
59
  "type": "registry:file",
60
60
  "target": "@components/makersclaw/blocks/pdf-viewer/README.md",
61
- "content": "# PdfViewer\n\nA PDF on screen, in the workspace theme. The pages scroll in one column;\na rail of thumbnails sits on the left and a page box in the toolbar\njumps anywhere. Zoom by steps, fit the width, fit the page, or hold\nCtrl/⌘ and scroll (a trackpad pinch works too) — the line under the\npointer stays put. ⌘F finds words in the text: every match is marked on\nthe page, Enter walks them.\n\nPeople can highlight. Select a sentence and a small bar offers\n**Highlight**, **Add note** and **Copy**. A highlight shows as a marker\nstroke in a theme colour; its note sits in the Highlights list beside\nthe pages, where it is edited, recoloured or deleted (with Undo). Every\nchange shows at once and saves through your callback; when a save fails,\nthe change goes back and a toast says why.\n\nA narrow viewer (under 900px wide — a phone, a side column) turns the\npage rail and the Highlights list into sheets. Long files stay fast:\nonly the pages near the window are drawn. Password-protected files ask\nfor their password. A file that will not open says why and offers Try\nagain.\n\nIt is [React-PDF](https://github.com/wojtekmaj/react-pdf) (MIT) over\nMozilla's [PDF.js](https://github.com/mozilla/pdf.js) (Apache-2.0)\nunderneath: they parse and draw the pages and the selectable text. The\ntoolbar, rails, search, highlights and states are ours, in the kit. The\nblocks built on this one (proposal-accept, a contract review screen…)\nimport it, never `react-pdf` or `pdfjs-dist`.\n\n```jsx\nimport { PdfViewer } from \"../components/blocks/pdf-viewer\"; // a Makersclaw app (\"./blocks/pdf-viewer\" from components/)\nimport { PdfViewer } from \"@/components/makersclaw/blocks/pdf-viewer\"; // a shadcn project\n```\n\n`PdfViewer` is a client component: render it from a `\"use client\"`\ncomponent. The helpers (`findMatches`, `toPageRects`, `sortHighlights`…)\nare plain functions and work on the server.\n\n## The file, and the worker\n\n`file` is a URL (same origin, or a server that allows CORS), a `Blob` /\n`File` (an upload, or a PDF made with the `pdf-document` block), or the\nbytes (`ArrayBuffer` / `Uint8Array`, copied — your buffer stays usable).\n\nPDF.js parses in a Web Worker. Nothing to set up: the block points\nPDF.js at `new URL(\"pdfjs-dist/build/pdf.worker.min.mjs\", import.meta.url)`,\nwhich the app's bundler resolves at build time and emits as a hashed\nfile under `/_next/static/media/`, with the base path applied. So the\nworker ships with the app, always matches the PDF.js version that calls\nit, and comes from the app's own origin — no file in `public/`, no CDN.\n\n## The data\n\nA highlight is one row:\n\n```js\n{\n id: \"hl_81\", // yours\n page: 3, // 1-based\n rects: [{ x, y, w, h }], // fractions of the page (0…1), top-left origin — hold at any zoom\n text: \"Fees rise by 4% at each annual renewal\", // the words highlighted\n note: \"Ask for a 3% cap.\", // optional\n tone: \"warning\", // warning (default) · info · success · destructive · primary\n author: \"Priya Raman\", // optional, shown under the note\n createdAt: \"2026-09-21\", // optional\n}\n```\n\n`rects` may be left out: a highlight given only by its `text` (and\noptionally its `page`) is placed where those words first appear, once\nthe pages' text has been read. That is how an agent or a server flags a\nclause without knowing any geometry. A quote that is not in the text is\nlisted under \"Not found in the text\". Store the `rects` the viewer sends\nyou in `onHighlight` — they are exact; placing by words is close (the\nwidth inside a line is shared out by characters) and assumes pages that\nare not rotated.\n\nStore a highlight as a row in its own table (`document_id`, `page`,\n`rects` jsonb, `text`, `note`, `tone`, `author`, `created_at`).\n\n## Props\n\n- `file` — the PDF (see above). `null` shows the empty state.\n- `title` — names the viewer for screen readers and shows in the\n toolbar on a wide viewer. Default `\"PDF document\"` for readers, nothing\n shown.\n- `fileName` — what Download saves as. Default: the title + `.pdf`.\n- `highlights` — the rows above. Default `[]`.\n- `onHighlight(h)` — save a new highlight: `h` is `{ page, rects, text,\n note, tone }`. Return `{ ok: true, id }` (your id replaces the viewer's\n temporary one) or `{ ok: false, error }` (a throw counts as failed): it\n is removed and a toast shows `error`. **Without `onHighlight` the viewer\n is read-only**: no selection bar, and the Highlights list (shown only\n when there are highlights) cannot edit.\n- `onHighlightChange(id, patch)` — `patch` is `{ note }` or `{ tone }`.\n Same return; a failure puts the old value back.\n- `onHighlightDelete(id)` — called 5 seconds after a delete, once the\n Undo in its toast has passed (or at once if the viewer unmounts). Same\n return; a failure puts the highlight back.\n- `initialPage` — the page to open on. Default `1`.\n- `onPageChange(page, pageCount)` — whenever the page at the top of the\n window changes.\n- `defaultZoom` — `\"fit\"` (width, the default) · `\"page\"` · a number\n (`1` is 100%).\n- `thumbnails` — the page rail. Default `true`.\n- `search` — find in the text. Default `true`.\n- `download` — the Download button. Default `true`.\n- `notes` — open the Highlights list on a wide viewer at first. Default:\n open when there are highlights.\n- `height` — the viewer's height, any CSS length. Default\n `\"min(80vh, 860px)\"`; it never goes under 384px.\n- `loading` — `true` shows the skeleton (while you fetch or make the\n file) instead of the empty state.\n- `emptyTitle` · `emptyBody` — the words when `file` is empty. Default\n \"No document yet\" / \"When a PDF is attached, it shows here.\"\n- `viewerRef` — a ref from `usePdfViewer()`; `ref.current` then has\n `goToPage(n)`, `goToHighlight(id)`, `search(text)`, `setZoom(z)`, and\n `page` / `pageCount`.\n- `className` — added to the frame (a margin, a width). Its border,\n radius and height are the block's; set height through `height`.\n\nKeys, while focus is in the viewer: ⌘/Ctrl + F find, Enter / Shift+Enter\nnext / previous match, Esc closes find; ⌘/Ctrl + `+` / `-` zoom, `0`\nfits the width; Page Up / Page Down and the arrows scroll; Tab reaches\nevery control and each page thumbnail. Notes: ⌘/Ctrl + Enter saves, Esc\ncancels.\n\n## Example\n\n```jsx\n// surfaces/contract.jsx — client: one document and its highlights\n\"use client\";\nimport { useSearchParams } from \"next/navigation\";\nimport { useRow, useRows, whenLanded } from \"../lib/data\";\nimport { deleteRow, insertRow, updateRow } from \"../lib/actions\";\nimport { PdfViewer } from \"../components/blocks/pdf-viewer\";\n\nconst source = \"highlights\";\n\n// A write answers once the highlights are read again, so the viewer never\n// shows the list from before it.\nasync function landed(res) {\n if (res.ok) await whenLanded();\n return res;\n}\n\nexport default function ContractReview() {\n const id = useSearchParams().get(\"row\");\n const { row: doc, loading } = useRow({ source: \"documents\", id });\n const { rows: highlights } = useRows({ source, filter: { document_id: id } });\n if (loading || !doc) return null;\n return (\n <PdfViewer\n file={doc.url}\n title={doc.title}\n highlights={highlights}\n onHighlight={(h) => insertRow({ source, values: { document_id: doc.id, ...h } }).then(landed)}\n onHighlightChange={(hid, patch) => updateRow({ source, id: hid, patch }).then(landed)}\n onHighlightDelete={(hid) => deleteRow({ source, id: hid }).then(landed)}\n />\n );\n}\n```\n\nIn a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.\n\nPreview a PDF you make (the `pdf-document` block):\n\n```jsx\nconst { blob } = usePdf(() => <QuoteDocument {...quote} />, [quote]);\n<PdfViewer file={blob} loading={!blob} title={`Quote ${quote.number}`} thumbnails={false} />\n```\n\n## What it will not do\n\n- Edit the PDF: fill its forms, sign it, rotate or reorder pages, or\n write highlights into the file. Highlights are rows you store; the PDF\n is untouched. (For signing, the `signature-pad` block.)\n- Search across a line break in one phrase: PDF.js splits text into\n lines, and a match must sit inside one (like most quick finds).\n- Highlight in a scanned PDF (an image with no text layer): there is no\n text to select or find.\n- Keep the old pages up while a NEW file loads: a changed `file` shows\n the skeleton until it has opened.\n- Open a URL on another origin that does not allow CORS: fetch it on\n your server and pass the bytes.\n- Print; the browser's own PDF reader does that from Download.\n\n## Credits\n\nBuilt on [React-PDF](https://github.com/wojtekmaj/react-pdf) by Wojciech\nMaj (MIT) and [PDF.js](https://github.com/mozilla/pdf.js) by Mozilla\n(Apache-2.0), used unmodified. See THIRD-PARTY-NOTICES.md.\n"
61
+ "content": "# PdfViewer\n\nA PDF on screen, in the workspace theme. The pages scroll in one column;\na rail of thumbnails sits on the left and a page box in the toolbar\njumps anywhere. Zoom by steps, fit the width, fit the page, or hold\nCtrl/⌘ and scroll (a trackpad pinch works too) — the line under the\npointer stays put. ⌘F finds words in the text: every match is marked on\nthe page, Enter walks them.\n\nPeople can highlight. Select a sentence and a small bar offers\n**Highlight**, **Add note** and **Copy**. A highlight shows as a marker\nstroke in a theme colour; its note sits in the Highlights list beside\nthe pages, where it is edited, recoloured or deleted (with Undo). Every\nchange shows at once and saves through your callback; when a save fails,\nthe change goes back and a toast says why.\n\nA narrow viewer (under 900px wide — a phone, a side column) turns the\npage rail and the Highlights list into sheets. Long files stay fast:\nonly the pages near the window are drawn. Password-protected files ask\nfor their password. A file that will not open says why and offers Try\nagain.\n\nIt is [React-PDF](https://github.com/wojtekmaj/react-pdf) (MIT) over\nMozilla's [PDF.js](https://github.com/mozilla/pdf.js) (Apache-2.0)\nunderneath: they parse and draw the pages and the selectable text. The\ntoolbar, rails, search, highlights and states are ours, in the kit. The\nblocks built on this one (proposal-accept, a contract review screen…)\nimport it, never `react-pdf` or `pdfjs-dist`.\n\n```jsx\nimport { PdfViewer } from \"../components/blocks/pdf-viewer\"; // a Makersclaw app (\"./blocks/pdf-viewer\" from components/)\nimport { PdfViewer } from \"@/components/makersclaw/blocks/pdf-viewer\"; // a shadcn project\n```\n\n`PdfViewer` is a client component: render it from a `\"use client\"`\ncomponent. The helpers (`findMatches`, `toPageRects`, `sortHighlights`…)\nare plain functions and work on the server.\n\n## The file, and the worker\n\n`file` is a URL (same origin, or a server that allows CORS), a `Blob` /\n`File` (an upload, or a PDF made with the `pdf-document` block), or the\nbytes (`ArrayBuffer` / `Uint8Array`, copied — your buffer stays usable).\n\nPDF.js parses in a Web Worker. Nothing to set up: the block points\nPDF.js at `new URL(\"pdfjs-dist/build/pdf.worker.min.mjs\", import.meta.url)`,\nwhich the app's bundler resolves at build time and emits as a hashed\nfile under `/_next/static/media/`, with the base path applied. So the\nworker ships with the app, always matches the PDF.js version that calls\nit, and comes from the app's own origin — no file in `public/`, no CDN.\n\n## The data\n\nA highlight is one row:\n\n```js\n{\n id: \"hl_81\", // yours\n page: 3, // 1-based\n rects: [{ x, y, w, h }], // fractions of the page (0…1), top-left origin — hold at any zoom\n text: \"Fees rise by 4% at each annual renewal\", // the words highlighted\n note: \"Ask for a 3% cap.\", // optional\n tone: \"warning\", // warning (default) · info · success · destructive · primary\n author: \"Priya Raman\", // optional, shown under the note\n createdAt: \"2026-09-21\", // optional\n}\n```\n\n`rects` may be left out: a highlight given only by its `text` (and\noptionally its `page`) is placed where those words first appear, once\nthe pages' text has been read. That is how an agent or a server flags a\nclause without knowing any geometry. A quote that is not in the text is\nlisted under \"Not found in the text\". Store the `rects` the viewer sends\nyou in `onHighlight` — they are exact; placing by words is close (the\nwidth inside a line is shared out by characters) and assumes pages that\nare not rotated.\n\nStore a highlight as a row in its own table (`document_id`, `page`,\n`rects` jsonb, `text`, `note`, `tone`, `author`, `created_at`).\n\n## Props\n\n- `file` — the PDF (see above). `null` shows the empty state.\n- `title` — names the viewer for screen readers and shows in the\n toolbar on a wide viewer. Default `\"PDF document\"` for readers, nothing\n shown.\n- `fileName` — what Download saves as. Default: the title + `.pdf`.\n- `highlights` — the rows above. Default `[]`.\n- `onHighlight(h)` — save a new highlight: `h` is `{ page, rects, text,\n note, tone }`. Return `{ ok: true, id }` (your id replaces the viewer's\n temporary one) or `{ ok: false, error }` (a throw counts as failed): it\n is removed and a toast shows `error`. **Without `onHighlight` the viewer\n is read-only**: no selection bar, and the Highlights list (shown only\n when there are highlights) cannot edit.\n- `onHighlightChange(id, patch)` — `patch` is `{ note }` or `{ tone }`.\n Same return; a failure puts the old value back.\n- `onHighlightDelete(id)` — called 5 seconds after a delete, once the\n Undo in its toast has passed (or at once if the viewer unmounts). Same\n return; a failure puts the highlight back.\n- `initialPage` — the page to open on. Default `1`.\n- `onPageChange(page, pageCount)` — whenever the page at the top of the\n window changes.\n- `defaultZoom` — `\"fit\"` (width, the default) · `\"page\"` · a number\n (`1` is 100%).\n- `thumbnails` — the page rail. Default `true`.\n- `search` — find in the text. Default `true`.\n- `download` — the Download button. Default `true`.\n- `notes` — open the Highlights list on a wide viewer at first. Default:\n open when there are highlights.\n- `height` — the viewer's height, any CSS length. Default\n `\"min(80vh, 860px)\"` (the block's own); it never goes under 384px.\n Yours is a length, or `\"100%\"` in a sized box such as the kit's\n `AppLayout contentMode=\"fill\"`: a `vh` length in your code is refused\n by `npm run check`.\n- `loading` — `true` shows the skeleton (while you fetch or make the\n file) instead of the empty state.\n- `emptyTitle` · `emptyBody` — the words when `file` is empty. Default\n \"No document yet\" / \"When a PDF is attached, it shows here.\"\n- `viewerRef` — a ref from `usePdfViewer()`; `ref.current` then has\n `goToPage(n)`, `goToHighlight(id)`, `search(text)`, `setZoom(z)`, and\n `page` / `pageCount`.\n- `className` — added to the frame (a margin, a width). Its border,\n radius and height are the block's; set height through `height`.\n\nKeys, while focus is in the viewer: ⌘/Ctrl + F find, Enter / Shift+Enter\nnext / previous match, Esc closes find; ⌘/Ctrl + `+` / `-` zoom, `0`\nfits the width; Page Up / Page Down and the arrows scroll; Tab reaches\nevery control and each page thumbnail. Notes: ⌘/Ctrl + Enter saves, Esc\ncancels.\n\n## Example\n\n```jsx\n// surfaces/contract.jsx — client: one document and its highlights\n\"use client\";\nimport { useSearchParams } from \"next/navigation\";\nimport { useRow, useRows, whenLanded } from \"../lib/data\";\nimport { deleteRow, insertRow, updateRow } from \"../lib/actions\";\nimport { PdfViewer } from \"../components/blocks/pdf-viewer\";\n\nconst source = \"highlights\";\n\n// A write answers once the highlights are read again, so the viewer never\n// shows the list from before it.\nasync function landed(res) {\n if (res.ok) await whenLanded();\n return res;\n}\n\nexport default function ContractReview() {\n const id = useSearchParams().get(\"row\");\n const { row: doc, loading } = useRow({ source: \"documents\", id });\n const { rows: highlights } = useRows({ source, filter: { document_id: id } });\n if (loading || !doc) return null;\n return (\n <PdfViewer\n file={doc.url}\n title={doc.title}\n highlights={highlights}\n onHighlight={(h) => insertRow({ source, values: { document_id: doc.id, ...h } }).then(landed)}\n onHighlightChange={(hid, patch) => updateRow({ source, id: hid, patch }).then(landed)}\n onHighlightDelete={(hid) => deleteRow({ source, id: hid }).then(landed)}\n />\n );\n}\n```\n\nIn a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.\n\nPreview a PDF you make (the `pdf-document` block):\n\n```jsx\nconst { blob } = usePdf(() => <QuoteDocument {...quote} />, [quote]);\n<PdfViewer file={blob} loading={!blob} title={`Quote ${quote.number}`} thumbnails={false} />\n```\n\n## What it will not do\n\n- Edit the PDF: fill its forms, sign it, rotate or reorder pages, or\n write highlights into the file. Highlights are rows you store; the PDF\n is untouched. (For signing, the `signature-pad` block.)\n- Search across a line break in one phrase: PDF.js splits text into\n lines, and a match must sit inside one (like most quick finds).\n- Highlight in a scanned PDF (an image with no text layer): there is no\n text to select or find.\n- Keep the old pages up while a NEW file loads: a changed `file` shows\n the skeleton until it has opened.\n- Open a URL on another origin that does not allow CORS: fetch it on\n your server and pass the bytes.\n- Print; the browser's own PDF reader does that from Download.\n\n## Credits\n\nBuilt on [React-PDF](https://github.com/wojtekmaj/react-pdf) by Wojciech\nMaj (MIT) and [PDF.js](https://github.com/mozilla/pdf.js) by Mozilla\n(Apache-2.0), used unmodified. See THIRD-PARTY-NOTICES.md.\n"
62
62
  }
63
63
  ],
64
64
  "categories": [
@@ -81,7 +81,7 @@
81
81
  "docs": "Installed to components/makersclaw/blocks/pdf-viewer. Import: import { PdfViewer, usePdfViewer, HIGHLIGHT_TONES, findMatches, toPageRects, mergeRects, sortHighlights, highlightAt, fitScale, ZOOM_STEPS } from \"@/components/makersclaw/blocks/pdf-viewer\". Props and an example: components/makersclaw/blocks/pdf-viewer/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
82
82
  "meta": {
83
83
  "makersclaw": {
84
- "version": "0.7.0",
84
+ "version": "0.8.1",
85
85
  "useWhen": "A screen shows an existing PDF: a contract or proposal to review, a signed agreement, an uploaded invoice or statement, a report, a generated quote before it is sent; or people mark up a PDF and leave notes on passages. Pair it with pdf-document to preview a PDF you make. Not for making PDFs (pdf-document), for rich text people edit (doc-editor), or for marking up a screenshot (annotate)."
86
86
  }
87
87
  }
@@ -5,7 +5,7 @@
5
5
  "title": "Product detail",
6
6
  "description": "One product on its own page: pictures (thumbnails, a side rail, a carousel, a tall stack, or one scene with the buy box floating over it), brand, name, price with the old price struck through, star rating and review count, options (sizes as a segmented row, colours as swatches, long lists as a select, plans as cards with their own price), a quantity control, add to cart through an async callback that may fail, buy now, save to favourites, perks, and details in an accordion or tabs. Loading, error, not-found and sold-out states.",
7
7
  "registryDependencies": [
8
- "https://cdn.jsdelivr.net/npm/makersclaw@0.7.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": [
@@ -69,7 +69,7 @@
69
69
  "docs": "Installed to components/makersclaw/blocks/product-detail. Import: import { ProductDetail, ProductMedia, OptionPicker, QuantityControl, Price, formatPrice, cartLine } from \"@/components/makersclaw/blocks/product-detail\". Props and an example: components/makersclaw/blocks/product-detail/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
70
70
  "meta": {
71
71
  "makersclaw": {
72
- "version": "0.7.0",
72
+ "version": "0.8.1",
73
73
  "useWhen": "Selling one thing a customer looks at before buying: a product page, a template or digital download with plans, a merch item, a booking add-on. Not for many products at once (product-grid), the cart itself (cart), or a plan comparison table."
74
74
  }
75
75
  }
@@ -5,8 +5,8 @@
5
5
  "title": "Product grid",
6
6
  "description": "Products as cards in a grid: picture, badge, favourite heart, brand, name, star rating, price with the old one struck through, and a quick add to cart through an async callback that may fail (\"Choose options\" when the product has sizes or colours). Facet filters as selects that also match a product's option values, a sort, a live count, a lead tile that takes a double cell (a picture or a muted looping video), and a promo card for collection tiles. Loading, error, empty and no-matches states; two columns on a phone.",
7
7
  "registryDependencies": [
8
- "https://cdn.jsdelivr.net/npm/makersclaw@0.7.0/r/core.json",
9
- "https://cdn.jsdelivr.net/npm/makersclaw@0.7.0/r/product-detail.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/product-detail.json"
10
10
  ],
11
11
  "dependencies": [],
12
12
  "files": [
@@ -58,7 +58,7 @@
58
58
  "docs": "Installed to components/makersclaw/blocks/product-grid. Import: import { ProductGrid, ProductCard, PromoTile, productMatches } from \"@/components/makersclaw/blocks/product-grid\". Props and an example: components/makersclaw/blocks/product-grid/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
59
59
  "meta": {
60
60
  "makersclaw": {
61
- "version": "0.7.0",
61
+ "version": "0.8.1",
62
62
  "useWhen": "A shop's catalogue, a collection page, related products under a product page, a row of promotions ('New', 'Sale'). Not for records to scan and act on in columns (data-table), one product's page (product-detail), or the cart (cart)."
63
63
  }
64
64
  }
@@ -5,7 +5,7 @@
5
5
  "title": "RecordForm",
6
6
  "description": "Edit or create one record from a list of fields given as data (type, label, required, options, help, validation), grouped into titled sections — text, email, number, password, select, combobox, tags, radio, choice cards with prices, checkboxes, switch, date, time, rating, a photo and a row of images. It checks as you type, saves through an async onSave that answers { ok } or per-field errors shown under each field, and sits in a card, on a page (sections split title-left) or in a dialog.",
7
7
  "registryDependencies": [
8
- "https://cdn.jsdelivr.net/npm/makersclaw@0.7.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": [
@@ -60,7 +60,7 @@
60
60
  "docs": "Installed to components/makersclaw/blocks/record-form. Import: import { RecordForm } from \"@/components/makersclaw/blocks/record-form\". Props and an example: components/makersclaw/blocks/record-form/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
61
61
  "meta": {
62
62
  "makersclaw": {
63
- "version": "0.7.0",
63
+ "version": "0.8.1",
64
64
  "useWhen": "A screen edits one record's fields: a profile, a product, a team member, account details, a sign-up or application form, a settings page of fields with one Save. Not for a list of records (data-table), for toggles that each save on their own (settings-list), or for sign-in (auth-form)."
65
65
  }
66
66
  }
@@ -0,0 +1,52 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "record-grid",
4
+ "type": "registry:block",
5
+ "title": "RecordGrid",
6
+ "description": "A dense grid for reading many records at once: a row number and a tick at the start of each row, the name column pinned beside them while the rest scrolls sideways, headers and cells you draw yourself (a column's live progress, its cost, its menu; an answer with its mark), whole-row marks (selected, highlighted, struck, dimmed), one active cell moved by the arrow keys and opened with Enter, and a line after the last row. Only the rows in the window are drawn, so 2,000 rows of 32px scroll smoothly. It scrolls itself inside a bounded parent.",
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/record-grid/index.js",
14
+ "type": "registry:component",
15
+ "target": "@components/makersclaw/blocks/record-grid/index.js",
16
+ "content": "// The RecordGrid block — `makersclaw add record-grid`. Fixed: compose it\n// through its props (README.md beside this file).\nexport { RecordGrid } from \"./record-grid\";\n"
17
+ },
18
+ {
19
+ "path": "registry/blocks/record-grid/record-grid.jsx",
20
+ "type": "registry:component",
21
+ "target": "@components/makersclaw/blocks/record-grid/record-grid.jsx",
22
+ "content": "\"use client\";\n\n// RecordGrid — a dense grid for reading many records at once: a row\n// number and a tick at the start of every row, the name column pinned\n// beside them while the rest scrolls sideways, a header that is yours to\n// draw (a column's live progress, its cost, its menu), a cell that is\n// yours to draw, marks on whole rows (selected, highlighted, struck,\n// dimmed), one active cell, and the keyboard. Only the rows in the window\n// are drawn, so 2,000 rows scroll like twenty. It scrolls itself: give it\n// a bounded parent (h-full, flex-1 min-h-0).\n//\n// Props in, callbacks out; it never fetches and formats nothing. Tokens\n// only.\n\nimport { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from \"react\";\nimport { cx } from \"../../ui\";\n\nconst keyOf = (row, rowKey) => (typeof rowKey === \"function\" ? rowKey(row) : row?.[rowKey ?? \"id\"]);\n\n/**\n * rows the records, in order\n * columns [{ key, width (px), header (node), cell(row, index) (node),\n * className?, headerClassName?, label? (a screen reader's\n * name for the column when the header is not words) }]\n * lead [{ key, width, header, cell(row, index), className? }]:\n * the columns before the pinned ones (a row number, a tick)\n * pinned how many of `columns` stay put beside `lead` (default 1)\n * rowKey a key or (row) => key (default \"id\")\n * rowHeight px (default 32); headerHeight px (default 50)\n * rowMark(row) { selected, highlighted, struck, dimmed }\n * active { row: key, column: key } the active cell, or null\n * onActiveChange(next) the keyboard or a click moved it\n * onCellClick(row, columnKey) a click on a cell (open it)\n * onOpen(row, columnKey) Enter on the active cell\n * after a node in a row of its own after the last row\n * empty what an empty grid says\n * label what a screen reader calls the grid\n * className layout around it\n * ref { scrollToRow(key), scrollToEnd(), focus() }\n */\nexport const RecordGrid = forwardRef(function RecordGrid(\n {\n rows,\n columns,\n lead = [],\n pinned = 1,\n rowKey = \"id\",\n rowHeight = 32,\n headerHeight = 50,\n overscan = 8,\n rowMark,\n active,\n onActiveChange,\n onCellClick,\n onOpen,\n after,\n empty,\n label = \"Records\",\n className,\n },\n ref,\n) {\n const box = useRef(null);\n const [scroll, setScroll] = useState({ top: 0, left: 0, height: 600 });\n\n const all = useMemo(() => [...lead.map((c) => ({ ...c, lead: true })), ...columns], [lead, columns]);\n const stuck = lead.length + Math.max(0, Math.min(pinned, columns.length));\n const offsets = useMemo(() => {\n const out = [];\n let x = 0;\n for (const c of all) {\n out.push(x);\n x += Number(c.width) || 120;\n }\n return { at: out, width: x };\n }, [all]);\n\n const onScroll = useCallback(() => {\n const el = box.current;\n if (!el) return;\n setScroll((s) => (s.top === el.scrollTop && s.left === el.scrollLeft && s.height === el.clientHeight ? s : { top: el.scrollTop, left: el.scrollLeft, height: el.clientHeight }));\n }, []);\n\n useEffect(() => {\n const el = box.current;\n if (!el) return;\n onScroll();\n const ro = typeof ResizeObserver === \"function\" ? new ResizeObserver(onScroll) : null;\n ro?.observe(el);\n return () => ro?.disconnect();\n }, [onScroll]);\n\n const total = rows.length;\n const first = Math.max(0, Math.floor((scroll.top - headerHeight) / rowHeight) - overscan);\n const last = Math.min(total, Math.ceil((scroll.top + scroll.height) / rowHeight) + overscan);\n const shown = rows.slice(first, last);\n\n const indexOf = useCallback((key) => rows.findIndex((r) => keyOf(r, rowKey) === key), [rows, rowKey]);\n\n const scrollToIndex = useCallback(\n (i, block = \"nearest\") => {\n const el = box.current;\n if (!el || i < 0) return;\n const top = headerHeight + i * rowHeight;\n const view = el.clientHeight;\n if (block === \"end\") el.scrollTop = Math.max(0, top + rowHeight - view);\n else if (top < el.scrollTop + headerHeight) el.scrollTop = Math.max(0, top - headerHeight);\n else if (top + rowHeight > el.scrollTop + view) el.scrollTop = top + rowHeight - view;\n },\n [headerHeight, rowHeight],\n );\n\n useImperativeHandle(\n ref,\n () => ({\n scrollToRow: (key) => scrollToIndex(indexOf(key)),\n scrollToEnd: () => {\n const el = box.current;\n if (el) el.scrollTop = el.scrollHeight;\n },\n focus: () => box.current?.focus({ preventScroll: true }),\n }),\n [indexOf, scrollToIndex],\n );\n\n // The active cell stays in view.\n useEffect(() => {\n if (active?.row != null) scrollToIndex(indexOf(active.row));\n }, [active?.row, indexOf, scrollToIndex]);\n\n const keys = columns.map((c) => c.key);\n const onKeyDown = (e) => {\n if (!rows.length || !keys.length) return;\n const at = active ? indexOf(active.row) : -1;\n const col = active ? Math.max(0, keys.indexOf(active.column)) : 0;\n let next = null;\n if (e.key === \"ArrowDown\") next = { i: Math.min(total - 1, at + 1), c: col };\n else if (e.key === \"ArrowUp\") next = { i: Math.max(0, at < 0 ? 0 : at - 1), c: col };\n else if (e.key === \"ArrowRight\") next = { i: Math.max(0, at), c: Math.min(keys.length - 1, col + 1) };\n else if (e.key === \"ArrowLeft\") next = { i: Math.max(0, at), c: Math.max(0, col - 1) };\n else if (e.key === \"Home\") next = { i: e.metaKey || e.ctrlKey ? 0 : Math.max(0, at), c: 0 };\n else if (e.key === \"End\") next = { i: e.metaKey || e.ctrlKey ? total - 1 : Math.max(0, at), c: keys.length - 1 };\n else if (e.key === \"PageDown\") next = { i: Math.min(total - 1, at + Math.max(1, Math.floor(scroll.height / rowHeight) - 1)), c: col };\n else if (e.key === \"PageUp\") next = { i: Math.max(0, at - Math.max(1, Math.floor(scroll.height / rowHeight) - 1)), c: col };\n else if (e.key === \"Enter\" && active) {\n e.preventDefault();\n const row = rows[at];\n if (row) onOpen?.(row, active.column);\n return;\n }\n if (!next) return;\n e.preventDefault();\n const row = rows[next.i];\n if (row) onActiveChange?.({ row: keyOf(row, rowKey), column: keys[next.c] });\n };\n\n const stick = (i, head = false) => {\n if (i >= stuck) return null;\n return { position: \"sticky\", left: offsets.at[i], zIndex: head ? 3 : 1 };\n };\n const edge = (i) => i === stuck - 1 && scroll.left > 0;\n\n return (\n <div\n ref={box}\n role=\"grid\"\n aria-label={label}\n aria-rowcount={total + 1}\n aria-colcount={all.length}\n tabIndex={0}\n onScroll={onScroll}\n onKeyDown={onKeyDown}\n className={cx(\"relative h-full min-h-0 overflow-auto overscroll-contain outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\", className)}\n >\n <div style={{ width: offsets.width, minWidth: \"100%\", height: headerHeight + total * rowHeight + (after ? 48 : 0) }} className=\"relative\">\n <div role=\"row\" aria-rowindex={1} className=\"sticky top-0 z-[2] flex bg-background\" style={{ height: headerHeight, width: offsets.width }}>\n {all.map((c, i) => (\n <div\n key={c.key}\n role=\"columnheader\"\n aria-label={c.label}\n className={cx(\n \"shrink-0 overflow-hidden border-b border-r border-border bg-background px-2 pb-1 pt-1.5 text-left align-top\",\n edge(i) && \"shadow-[6px_0_8px_-6px_var(--color-foreground)]\",\n c.headerClassName,\n )}\n style={{ width: Number(c.width) || 120, ...stick(i, true) }}\n >\n {c.header}\n </div>\n ))}\n </div>\n {total === 0 ? (\n <div className=\"px-3 py-6 text-sm text-muted-foreground\">{empty ?? \"Nothing here.\"}</div>\n ) : null}\n <div style={{ position: \"absolute\", top: headerHeight + first * rowHeight, left: 0, width: offsets.width }}>\n {shown.map((row, j) => {\n const index = first + j;\n const key = keyOf(row, rowKey);\n const mark = rowMark?.(row) ?? {};\n const tint = mark.selected ? \"bg-accent text-accent-foreground\" : \"bg-background\";\n return (\n <div\n key={key}\n role=\"row\"\n aria-rowindex={index + 2}\n aria-selected={mark.selected ? true : undefined}\n className={cx(\"flex\", mark.struck && \"text-muted-foreground\")}\n style={{ height: rowHeight }}\n >\n {all.map((c, i) => {\n const on = active && active.row === key && active.column === c.key;\n return (\n <div\n key={c.key}\n role=\"gridcell\"\n aria-selected={on ? true : undefined}\n onClick={c.lead ? undefined : () => {\n onActiveChange?.({ row: key, column: c.key });\n onCellClick?.(row, c.key);\n }}\n className={cx(\n \"flex shrink-0 items-center overflow-hidden whitespace-nowrap border-b border-r border-border px-2\",\n tint,\n !c.lead && onCellClick && \"cursor-pointer\",\n mark.dimmed && !c.lead && i >= stuck && \"opacity-55\",\n mark.struck && i === stuck - 1 && \"line-through decoration-muted-foreground\",\n mark.highlighted && i === 0 && \"border-l-2 border-l-primary\",\n edge(i) && \"shadow-[6px_0_8px_-6px_var(--color-foreground)]\",\n on && \"outline outline-2 -outline-offset-2 outline-ring\",\n c.className,\n )}\n style={{ width: Number(c.width) || 120, height: rowHeight, ...stick(i) }}\n >\n {c.cell ? c.cell(row, index) : row[c.key]}\n </div>\n );\n })}\n </div>\n );\n })}\n </div>\n {after && total ? (\n <div className=\"absolute left-0 flex items-center px-3 text-sm text-muted-foreground\" style={{ top: headerHeight + total * rowHeight, minHeight: 48, width: \"100%\" }}>\n <div className=\"sticky left-3\">{after}</div>\n </div>\n ) : null}\n </div>\n </div>\n );\n});\n"
23
+ },
24
+ {
25
+ "path": "registry/blocks/record-grid/README.md",
26
+ "type": "registry:file",
27
+ "target": "@components/makersclaw/blocks/record-grid/README.md",
28
+ "content": "# RecordGrid\n\nA dense grid for reading many records at once. Each row starts with what\nyou put in `lead` (a row number, a tick), then the pinned columns (the\nname), then the rest, which scroll sideways under a sticky header. Every\nheader and every cell is yours to draw: a column whose header carries its\nlive progress, its price and its own menu; a cell that draws an answer\nwith its mark. Whole rows take marks (selected, highlighted, struck,\ndimmed), one cell is active (an outline), the arrow keys move it, Enter\nopens it, a click opens it. Only the rows in the window are drawn, at a\nfixed height, so 2,000 rows scroll like twenty. It scrolls itself: put it\nin a bounded parent (`h-full`, `flex-1 min-h-0`, an AppLayout's content\nwith `contentMode=\"fill\"`) that can also shrink sideways: in a grid or\nflex row give that parent `min-w-0`, or the grid's columns widen the\nparent instead of scrolling inside it.\n\nBorn in Data Enrichment (0.5.0), moved into the template with the app kit: the list, with\neach agent column a job whose header says how far it got and what a row\ncosts, and the receipt of an answer opened beside it.\n\n```jsx\nimport { RecordGrid } from \"../components/blocks/record-grid\"; // a Makersclaw app (\"./blocks/record-grid\" from components/)\nimport { RecordGrid } from \"@/components/makersclaw/blocks/record-grid\"; // a shadcn project\n```\n\n## Props\n\n- `rows` — the records, in order. Each has a key (`rowKey`).\n- `columns` — `[{ key, width, header, cell(row, index), className?,\n headerClassName?, label? }]`. `width` in px (default 120). `header` is\n any node, drawn at the top of a `headerHeight` cell; `cell(row, index)`\n any node, in a one-line cell of `rowHeight` (it clips: truncate your\n text). `label` names the column for a screen reader when the header is\n not words.\n- `lead` — columns before the pinned ones, the same shape: a row number,\n a tick. They are pinned too, and a click on them is theirs (no active\n cell).\n- `pinned` — how many of `columns` stay put beside `lead` (default 1).\n When the grid has scrolled sideways the last pinned column casts an\n edge.\n- `rowKey` — a key or `(row) => key` (default `\"id\"`).\n- `rowHeight` (32) · `headerHeight` (50) · `overscan` (8 rows drawn past\n each end of the window).\n- `rowMark(row)` — `{ selected, highlighted, struck, dimmed }`: selected\n tints the row with `accent`; highlighted draws a bar in `primary` at\n its start (the rows a fill will work on); struck lines through the\n last pinned column and quiets the row (a skipped row); dimmed fades the\n unpinned cells.\n- `active` — `{ row, column }`, the cell with the outline, or null.\n- `onActiveChange(next)` — a click or a key moved it. Keep it in state.\n- `onCellClick(row, columnKey)` — a click on an unpinned or pinned\n (not `lead`) cell: open what it shows.\n- `onOpen(row, columnKey)` — Enter on the active cell.\n- `after` — a node in a row of its own after the last row (\"+ Add rows\").\n- `empty` — what an empty grid says.\n- `label` — what a screen reader calls the grid (`\"Records\"`).\n- `className` — layout around it. Never colour.\n- `ref` — `{ scrollToRow(key), scrollToEnd(), focus() }`.\n\nKeys, with the grid focused: ↑ ↓ ← → move the active cell · Home / End\nthe row's ends (⌘ for the grid's) · Page Up / Down · Enter opens. Your\nown keys (J and K for the next answer to check) go on a parent.\n\n## Example\n\n```jsx\n\"use client\";\nimport { useState } from \"react\";\nimport { RecordGrid } from \"./blocks/record-grid\";\n\nexport function Companies({ rows, open }) {\n const [active, setActive] = useState(null);\n return (\n <div className=\"h-full min-h-0\">\n <RecordGrid\n label=\"Companies\"\n rows={rows}\n lead={[{ key: \"n\", width: 44, header: \"#\", cell: (r, i) => i + 1 }]}\n columns={[\n { key: \"name\", width: 180, header: <b>Company</b>, cell: (r) => <span className=\"truncate\">{r.name}</span> },\n { key: \"headcount\", width: 120, header: <b>Headcount</b>, cell: (r) => r.headcount },\n ]}\n active={active}\n onActiveChange={setActive}\n onCellClick={(r, key) => open(r, key)}\n onOpen={(r, key) => open(r, key)}\n rowMark={(r) => ({ selected: active?.row === r.id })}\n />\n </div>\n );\n}\n```\n\n## What it will not do\n\n- Edit a cell, sort, filter or page: the rows come in as you want them\n shown, and your cells' buttons do the work.\n- Rows of their own heights: one height keeps 2,000 rows smooth.\n- Draw a phone layout: below 768px draw your own cards, or let it scroll\n sideways inside its box.\n"
29
+ }
30
+ ],
31
+ "categories": [
32
+ "grid",
33
+ "table",
34
+ "records",
35
+ "virtual",
36
+ "pinned column",
37
+ "header",
38
+ "dense",
39
+ "keyboard",
40
+ "rows",
41
+ "review",
42
+ "enrichment",
43
+ "list"
44
+ ],
45
+ "docs": "Installed to components/makersclaw/blocks/record-grid. Import: import { RecordGrid } from \"@/components/makersclaw/blocks/record-grid\". Props and an example: components/makersclaw/blocks/record-grid/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
46
+ "meta": {
47
+ "makersclaw": {
48
+ "version": "0.8.1",
49
+ "useWhen": "A list that is read and worked rather than typed into, where each column needs its own header (an agent's column with its progress and price, a column with a menu) and each cell its own drawing: a research list being enriched, a review queue across many records, a report of a long job's rows. Not for editing cells like a sheet (spreadsheet) or a few dozen rows with sort and filters built in (data-table)."
50
+ }
51
+ }
52
+ }
@@ -5,9 +5,9 @@
5
5
  "title": "RecordNotes",
6
6
  "description": "Dated rich-text notes on one record (a deal, a contact, a company), to sit beside record-form: a composer with @mentions and Cmd/Ctrl+Enter adds a note; the notes follow newest first (or oldest), grouped by day, each with author avatar, name and relative time; your own notes edit in place and delete after a confirm. Every save is an async callback answering { ok }; a failure keeps the words and says why where it happened.",
7
7
  "registryDependencies": [
8
- "https://cdn.jsdelivr.net/npm/makersclaw@0.7.0/r/core.json",
9
- "https://cdn.jsdelivr.net/npm/makersclaw@0.7.0/r/comment-box.json",
10
- "https://cdn.jsdelivr.net/npm/makersclaw@0.7.0/r/doc-editor.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/comment-box.json",
10
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/doc-editor.json"
11
11
  ],
12
12
  "dependencies": [],
13
13
  "files": [
@@ -48,7 +48,7 @@
48
48
  "docs": "Installed to components/makersclaw/blocks/record-notes. Import: import { RecordNotes } from \"@/components/makersclaw/blocks/record-notes\". Props and an example: components/makersclaw/blocks/record-notes/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
49
49
  "meta": {
50
50
  "makersclaw": {
51
- "version": "0.7.0",
51
+ "version": "0.8.1",
52
52
  "useWhen": "A record page keeps a running log of notes: call notes on a deal, context on a contact, a history on a customer or a candidate. Not a discussion thread without edit and delete (comment-box), not one long document per record (doc-editor or doc-page), not an automatic activity feed."
53
53
  }
54
54
  }