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,83 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "schedule-gantt",
4
+ "type": "registry:block",
5
+ "title": "ScheduleGantt",
6
+ "description": "A Gantt for dated work with dependencies: the task sheet beside a working-day time scale, one picture row for row. Outline numbers, phases that fold into a bracket over their work, milestones as diamonds, bars in steel grey with their progress, links of all four kinds with lag drawn as elbows (a broken one dashed with a cross), weekends shaded, today a line, the target a dashed line and the finish a solid one with the gap between them counted in working days in the header. The critical path turns red when switched on; slack, the start and finish, the length in working days, the owner, what a task waits on in the short form (2.4FS+2d) and its distance from a baseline are columns the page picks, and the divider between sheet and bars drags. Drag a bar to move it (several selected move together), an end to change its length, the dot past its end onto another bar to link them: every task that waits on it moves while you drag, the finish in the header slides off the target and a note by the bar says what it costs. Dashed outlines show where bars were and where a suggestion would put them; a thin bar under each shows a baseline; lanes under the rows show who is on what, on the same scale. Type into the sheet: names, lengths, percentages, what a task waits on, and a last row that adds a task (Enter) or a milestone (Shift+Enter). Built on schedule-engine.",
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/schedule-engine.json"
10
+ ],
11
+ "dependencies": [],
12
+ "files": [
13
+ {
14
+ "path": "registry/blocks/schedule-gantt/index.js",
15
+ "type": "registry:component",
16
+ "target": "@components/makersclaw/blocks/schedule-gantt/index.js",
17
+ "content": "// The schedule-gantt block — `makersclaw add schedule-gantt`. Fixed:\n// compose it through its props (README.md beside this file). The model's\n// helpers are plain functions; they work on the server too.\nexport { ScheduleGantt } from \"./schedule-gantt\";\nexport { TagChip, CellInput, ROW } from \"./sheet\";\nexport { ZOOMS, axisFor, rangeFor, routeLink, shortDate, isoToday } from \"./geometry\";\nexport {\n COLUMNS,\n DEFAULT_COLUMNS,\n MONTHS,\n WEEKDAYS,\n baselineDelta,\n barLabel,\n columnsFor,\n describeDrag,\n dayWords,\n dragLines,\n fixedWidth,\n gapOf,\n lengthWords,\n linkLines,\n loopWords,\n monthDay,\n selectionMoves,\n slackWords,\n} from \"./gantt-model\";\n"
18
+ },
19
+ {
20
+ "path": "registry/blocks/schedule-gantt/schedule-gantt.jsx",
21
+ "type": "registry:component",
22
+ "target": "@components/makersclaw/blocks/schedule-gantt/schedule-gantt.jsx",
23
+ "content": "\"use client\";\n\nimport { useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport { Icon, cx } from \"../../ui\";\nimport { datesOf, dayOf, isoOf, linkProblem, makeCalendar, schedule, shiftDependents, spanOf, taskTree, waitsWords } from \"../schedule-engine\";\nimport { ZOOMS, axisFor, rangeFor, routeLink } from \"./geometry\";\nimport { baselineDelta, barLabel, columnsFor, dayWords, describeDrag, dragLines, fixedWidth, gapOf, linkLines, selectionMoves, slackWords } from \"./gantt-model\";\nimport { AddRow, ROW, SheetHead, SheetRow } from \"./sheet\";\nimport { HEAD, Ghosts, LaneLabels, LaneTrack, Links, Paper, TrackHead } from \"./track\";\n\n// ScheduleGantt: the task sheet beside a working-day time scale, one picture\n// row for row. Bars are steel grey with their progress inside; the critical\n// path turns red when the page switches it on; a phase is a bracket over its\n// work; a milestone a diamond. The target is a dashed line, the finish a solid\n// one, and the gap between them is measured in working days in the header.\n// Dashed outlines show where bars were (`ghosts`) and where a suggestion\n// would put them (`pencil`).\n//\n// Drag a bar to move it, an end to change its length, the dot at its end onto\n// another bar to link them: everything that waits on it moves while you drag,\n// and a note by the bar says what it costs. README.md lists every prop.\n// Built from divs and one svg for the links, tokens only.\n\nconst BAR = 14;\nconst CAL = makeCalendar(\"weekdays\");\nconst EMPTY = [];\nconst NONE = new Set();\nconst KIND_OF = { \"finish:start\": \"finish_to_start\", \"start:start\": \"start_to_start\", \"finish:finish\": \"finish_to_finish\", \"start:finish\": \"start_to_finish\" };\nconst NUM = \"tabular-nums\";\n\nconst firstName = (name) => String(name ?? \"\").trim().split(/\\s+/)[0] ?? \"\";\n\nexport function ScheduleGantt({\n tasks: tasksProp,\n links: linksProp,\n people,\n today,\n showToday = true,\n target,\n zoom = \"week\",\n fitWidth,\n columns,\n vsLabel = \"vs baseline\",\n rowH = ROW,\n gridWidth = 470,\n onGridWidth,\n height = \"fill\",\n critical = false,\n selectedId,\n selectedIds,\n onSelect,\n collapsed,\n onToggle,\n onOpen,\n onMove,\n onLink,\n onEdit,\n onAdd,\n onPreview,\n onOpenLink,\n highlight,\n addUnder,\n editRequest,\n jump,\n ghosts,\n pencil,\n baseline,\n guesses,\n notes,\n tags,\n moving,\n rowMenu,\n lanes,\n lanesTitle,\n loading = false,\n error = null,\n emptyText = \"No tasks yet. Type the first one in the last row.\",\n label = \"Gantt chart\",\n className,\n}) {\n const tasks = tasksProp ?? EMPTY;\n const links = linksProp ?? EMPTY;\n const scroller = useRef(null);\n const track = useRef(null);\n const dragRef = useRef(null);\n const linkRef = useRef(null);\n const suppress = useRef(false);\n const addRef = useRef(null);\n const [boxW, setBoxW] = useState(null);\n const [preview, setPreview] = useState(null);\n const [linking, setLinking] = useState(null);\n const [editing, setEditing] = useState(null);\n const [draft, setDraft] = useState({ title: \"\", days: \"\", milestone: false, parentId: null, afterId: null });\n const [adding, setAdding] = useState(false);\n const [hovered, setHovered] = useState(null);\n const [busy, setBusy] = useState(false);\n const [note, setNote] = useState(null);\n const [gridState, setGridState] = useState(gridWidth);\n const selected = selectedId ? String(selectedId) : null;\n const picked = selectedIds ?? NONE;\n\n // Measured in the callback, applied in the next frame (an observer that sets what it watches loops).\n useLayoutEffect(() => {\n const el = scroller.current;\n if (!el) return undefined;\n let raf = 0;\n const apply = (w) => {\n cancelAnimationFrame(raf);\n raf = requestAnimationFrame(() => setBoxW(w));\n };\n setBoxW(el.clientWidth);\n const onResize = () => apply(el.clientWidth);\n window.addEventListener(\"resize\", onResize);\n const ro = typeof ResizeObserver === \"undefined\" ? null : new ResizeObserver(() => apply(el.clientWidth));\n ro?.observe(el);\n return () => {\n cancelAnimationFrame(raf);\n window.removeEventListener(\"resize\", onResize);\n ro?.disconnect();\n };\n }, []);\n\n // The page opens the add row: { id (a parent) | null, afterId, milestone, nonce }.\n useEffect(() => {\n if (!addUnder) return;\n setAdding(true);\n setDraft({ title: \"\", days: \"\", milestone: Boolean(addUnder.milestone), parentId: addUnder.id ? String(addUnder.id) : null, afterId: addUnder.afterId ? String(addUnder.afterId) : null });\n const t = setTimeout(() => {\n addRef.current?.focus();\n addRef.current?.scrollIntoView?.({ block: \"nearest\" });\n }, 20);\n return () => clearTimeout(t);\n }, [addUnder]);\n\n // The page opens a cell: { id, field, nonce } (the row menu's Rename).\n useEffect(() => {\n if (editRequest?.id) setEditing({ id: String(editRequest.id), field: editRequest.field ?? \"title\" });\n }, [editRequest]);\n\n useEffect(() => {\n if (jump?.date) scrollToDay(jump.date, true);\n }, [jump]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // ---- the model ----------------------------------------------------------\n const plan = useMemo(() => schedule(tasks, links), [tasks, links]);\n const live = preview?.plan ?? plan;\n const tree = useMemo(() => taskTree(tasks), [tasks]);\n const byId = useMemo(() => new Map(tasks.map((t) => [String(t.id), t])), [tasks]);\n const closed = collapsed ?? NONE;\n const visible = useMemo(() => {\n const out = [];\n for (const e of tree.order) {\n let hidden = false;\n for (let p = e.parentId; p; p = tree.byId.get(p)?.parentId) if (closed.has(p)) hidden = true;\n if (!hidden) out.push(e);\n }\n return out;\n }, [tree, closed]);\n const rowOf = useMemo(() => new Map(visible.map((e, i) => [e.id, i])), [visible]);\n const outlineOf = (id) => tree.byId.get(String(id))?.outline;\n // The add row sits under the row it adds after; everything below it moves down by its height.\n const ADD_H = ROW + 6;\n const addIndex = adding ? (draft.afterId && rowOf.has(draft.afterId) ? rowOf.get(draft.afterId) : draft.parentId && rowOf.has(draft.parentId) ? rowOf.get(draft.parentId) : visible.length - 1) : null;\n const rowTop = (i) => i * rowH + (addIndex != null && i > addIndex ? ADD_H : 0);\n const rowAt = (y) => {\n for (let i = 0; i < visible.length; i += 1) if (y >= rowTop(i) && y < rowTop(i) + rowH) return i;\n return -1;\n };\n const shownRow = (id) => {\n for (let cur = String(id); cur; cur = tree.byId.get(cur)?.parentId) if (rowOf.has(cur)) return rowOf.get(cur);\n return null;\n };\n\n const shown = useMemo(() => {\n const out = new Map();\n for (const t of tasks) {\n const id = String(t.id);\n const r = live.rows.get(id);\n if (!r || !r.dated || r.summary) continue;\n const p = preview?.dates?.get(id);\n out.set(id, { start: p?.start ?? r.start, due: p?.due ?? r.due });\n }\n for (const r of live.rows.values()) {\n if (!r.summary) continue;\n let s = null;\n let d = null;\n for (const leaf of r.leafIds) {\n const x = out.get(leaf);\n if (!x || !x.start) continue;\n if (!s || x.start < s) s = x.start;\n if (!d || x.due > d) d = x.due;\n }\n if (s) out.set(r.id, { start: s, due: d });\n }\n return out;\n }, [tasks, live, preview]);\n\n // ---- the sheet ----------------------------------------------------------\n const narrow = boxW != null && boxW < 760;\n const cols = useMemo(() => columnsFor(columns, { vsLabel, narrow, outlineWidth: tree.order.some((e) => e.depth >= 3) ? 78 : undefined }), [columns, vsLabel, narrow, tree]);\n const gridW = narrow ? Math.max(170, Math.min(260, Math.round((boxW ?? 400) * 0.46))) : Math.max(fixedWidth(cols) + 160, gridState);\n const cellsOf = useMemo(() => {\n const byTask = new Map();\n for (const l of links) {\n const k = String(l.task_id);\n if (!byTask.has(k)) byTask.set(k, []);\n byTask.get(k).push(l);\n }\n return (id) => {\n const t = byId.get(id);\n const r = live.rows.get(id);\n const mine = byTask.get(id) ?? [];\n const person = people?.get?.(String(t?.assignee_id ?? \"\"));\n const name = person?.name ?? t?.assignee_name ?? \"\";\n const out = {\n owner: { name: firstName(name), title: name, left: Boolean(person?.left) },\n waits: waitsWords(mine, id, outlineOf),\n waitsLinks: mine.map((l) => live.links.get(String(l.id))).filter(Boolean),\n waitsBroken: mine.some((l) => live.links.get(String(l.id))?.violated) || Boolean(r?.stuck),\n slack: slackWords(r),\n guessed: Boolean(guesses?.has?.(id)),\n };\n if (baseline) out.delta = baselineDelta(r, baseline.get?.(id));\n return out;\n };\n }, [links, byId, live, people, baseline, guesses, tree]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // ---- the axis -----------------------------------------------------------\n const z = ZOOMS[zoom] ? zoom : \"week\";\n const range = useMemo(() => {\n const dates = [];\n for (const r of plan.rows.values()) if (r.dated) dates.push(r.start, r.due, r.planDue);\n if (target) dates.push(target);\n if (ghosts) for (const g of ghosts.values()) dates.push(g.start, g.due);\n if (pencil) for (const g of pencil.values()) dates.push(g.start, g.due);\n if (baseline) for (const g of baseline.values()) dates.push(g.start, g.due);\n return rangeFor(z, dates, today);\n }, [plan, target, ghosts, pencil, baseline, z, today]);\n // Whole project: the span fits the width, with room on the right for the notes after the bars.\n const ppd = z === \"fit\" ? Math.max(2, ((fitWidth ?? boxW ?? 1200) - gridW - (notes?.size ? 200 : 16)) / (range.to - range.from + 1)) : ZOOMS[z].ppd;\n const axis = useMemo(() => axisFor(z, range.from, range.to), [z, range.from, range.to]);\n const trackW = Math.round((range.to - range.from + 1) * ppd);\n const X = (iso) => (dayOf(iso) - range.from) * ppd;\n const inRange = (iso) => iso && dayOf(iso) >= range.from && dayOf(iso) <= range.to;\n\n const boxOf = (id) => {\n const row = shownRow(id);\n const d = shown.get(String(id));\n if (row == null || !d) return null;\n const r = live.rows.get(String(id));\n const y = rowTop(row) + rowH / 2;\n const own = rowOf.get(String(id)) === row;\n if (r?.milestone) {\n const cx0 = X(d.due) + ppd / 2;\n return { x0: cx0 - 7, x1: cx0 + 7, y, top: y - 7, bottom: y + 7, milestone: true, row, own };\n }\n const h = r?.summary || !own ? 6 : BAR;\n return { x0: X(d.start), x1: X(d.due) + ppd, y, top: y - h / 2, bottom: y + h / 2, milestone: false, row, own };\n };\n\n const lines = useMemo(() => {\n const out = [];\n const moved = new Set((preview?.changes ?? []).map((c) => String(c.id)));\n for (const l of live.links.values()) {\n if (!l.valid) continue;\n const a = boxOf(l.dependsOnId);\n const b = boxOf(l.taskId);\n if (!a || !b || a.row === b.row || !a.own || !b.own) continue;\n const kind = { finish_to_start: \"FS\", start_to_start: \"SS\", finish_to_finish: \"FF\", start_to_finish: \"SF\" }[l.kind] ?? l.kind;\n const route = routeLink(a, b, kind, { rowH });\n out.push({ id: l.id, link: l, critical: l.critical, lag: l.lag, route, touches: selected === l.taskId || selected === l.dependsOnId, moving: moved.has(l.taskId) && moved.has(l.dependsOnId) });\n }\n return out;\n }, [live, shown, visible, ppd, range.from, selected, rowH, preview]); // eslint-disable-line react-hooks/exhaustive-deps\n\n function scrollToDay(iso, smooth) {\n const el = scroller.current;\n if (!el || !iso) return;\n const left = Math.max(0, X(iso) - (el.clientWidth - gridW) * 0.3);\n el.scrollTo({ left, behavior: smooth ? \"smooth\" : \"auto\" });\n }\n useLayoutEffect(() => {\n if (z !== \"fit\") scrollToDay(today, false);\n }, [z, range.from]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // ---- dragging bars ----------------------------------------------------------\n function movesFor(d, delta) {\n const t = byId.get(d.id);\n const r = plan.rows.get(d.id);\n if (!t || !r) return [];\n if (r.summary) return [{ id: d.id, start: isoOf(dayOf(d.start) + delta) }];\n const span = spanOf(t);\n if (d.mode === \"move\") {\n if (r.milestone) {\n const m = datesOf(CAL.lastIndex(dayOf(r.due) + delta) + 1, 0, true);\n return [{ id: d.id, start: m.start, due: m.due }];\n }\n const m = datesOf(CAL.startIndex(dayOf(r.start) + delta), span.d, false);\n return [{ id: d.id, start: m.start, due: m.due }];\n }\n if (d.mode === \"end\") return [{ id: d.id, start: r.start, due: isoOf(Math.max(dayOf(r.start), dayOf(r.due) + delta)) }];\n return [{ id: d.id, start: isoOf(Math.min(dayOf(r.due), dayOf(r.start) + delta)), due: r.due }];\n }\n\n function previewFor(moves, extra = {}) {\n const res = shiftDependents(tasks, links, moves, extra);\n const dates = new Map(res.changes.map((c) => [String(c.id), { start: c.start_date, due: c.due_date }]));\n const from = new Map(res.changes.filter((c) => c.from?.start_date).map((c) => [String(c.id), { start: c.from.start_date, due: c.from.due_date }]));\n return { dates, from, plan: res.after, finishBefore: res.finishBefore, finishAfter: res.finishAfter, clamped: res.clamped, moves, changes: res.changes };\n }\n\n const describe = (p, id) => describeDrag({ before: plan, after: p.plan, changes: p.changes, clamped: p.clamped, id, titleOf: (x) => byId.get(x)?.title ?? \"\" });\n\n function startDrag(e, id, modeName) {\n if (!onMove) return;\n if (e.pointerType === \"mouse\" && e.button !== 0) return;\n const r = plan.rows.get(String(id));\n if (!r?.dated || r.pinned) return;\n if (e.pointerType !== \"mouse\" && modeName === \"move\" && !r.summary && !r.milestone) return;\n e.stopPropagation();\n const together = modeName === \"move\" && picked.size > 1 && picked.has(String(id)) ? [...picked].map(String) : null;\n dragRef.current = { id: String(id), mode: modeName, x0: e.clientX, start: shown.get(String(id))?.start, delta: 0, moved: false, pointerId: e.pointerId, together };\n try {\n track.current?.setPointerCapture(e.pointerId);\n } catch {\n // the pointer is gone already\n }\n }\n\n function movesOfDrag(d, delta) {\n if (!d.together) return movesFor(d, delta);\n return selectionMoves(d.together, (id) => {\n const r = plan.rows.get(id);\n if (!r?.dated || r.pinned) return [];\n return movesFor({ id, mode: \"move\", start: shown.get(id)?.start ?? r.start }, delta);\n });\n }\n\n function onTrackMove(e) {\n if (linkRef.current) return moveLink(e);\n const d = dragRef.current;\n if (!d || e.pointerId !== d.pointerId) return;\n const dx = e.clientX - d.x0;\n if (!d.moved && Math.abs(dx) < 4) return;\n d.moved = true;\n const delta = Math.round(dx / ppd);\n if (delta === d.delta && preview) return;\n d.delta = delta;\n const p = previewFor(movesOfDrag(d, delta));\n p.info = describe(p, d.id);\n d.preview = p;\n setPreview(p);\n onPreview?.({ mode: d.mode === \"end\" ? \"stretch\" : \"move\", task: byId.get(d.id), ...p.info });\n }\n\n async function onTrackUp(e) {\n if (linkRef.current) return finishLink(e);\n const d = dragRef.current;\n if (!d || e.pointerId !== d.pointerId) return;\n dragRef.current = null;\n if (!d.moved) return;\n suppress.current = true;\n setTimeout(() => {\n suppress.current = false;\n }, 0);\n const p = d.preview;\n setPreview(null);\n onPreview?.(null);\n if (!p || d.delta === 0) return;\n const res = await onMove?.(p.moves, p);\n if (res && res.ok === false) setNote(res.error ?? \"That move was not saved.\");\n }\n\n function cancelAll() {\n dragRef.current = null;\n linkRef.current = null;\n setPreview(null);\n setLinking(null);\n onPreview?.(null);\n }\n\n // ---- drawing links ----------------------------------------------------------\n function trackPoint(e) {\n const rect = track.current?.getBoundingClientRect();\n return rect ? { x: e.clientX - rect.left, y: e.clientY - rect.top } : { x: 0, y: 0 };\n }\n\n function startLink(e, id, end) {\n if (!onLink) return;\n e.stopPropagation();\n e.preventDefault();\n const b = boxOf(id);\n if (!b) return;\n const from = { x: end === \"start\" ? b.x0 : b.x1, y: b.y };\n const l = { fromId: String(id), fromEnd: end, from, to: from, targetId: null, targetEnd: null, problem: null, pointerId: e.pointerId, preview: null };\n linkRef.current = l;\n setLinking(l);\n try {\n track.current?.setPointerCapture(e.pointerId);\n } catch {\n // the pointer is gone already\n }\n }\n\n function moveLink(e) {\n const cur = linkRef.current;\n if (!cur || e.pointerId !== cur.pointerId) return;\n const pt = trackPoint(e);\n const entry = visible[rowAt(pt.y)];\n let targetId = null;\n let targetEnd = null;\n let problem = null;\n let p = null;\n if (entry && entry.id !== cur.fromId) {\n const b = boxOf(entry.id);\n if (b && pt.x >= b.x0 - 12 && pt.x <= b.x1 + 12) {\n targetId = entry.id;\n targetEnd = b.milestone ? \"start\" : pt.x < (b.x0 + b.x1) / 2 ? \"start\" : \"finish\";\n problem = linkProblem(tasks, links, targetId, cur.fromId);\n if (!problem) {\n p = previewFor([], { nextLinks: [...links, { id: \"__new\", task_id: targetId, depends_on_id: cur.fromId, kind: KIND_OF[`${cur.fromEnd}:${targetEnd}`], lag_days: 0 }] });\n p.info = describe(p, targetId);\n }\n }\n }\n linkRef.current = { ...cur, to: pt, targetId, targetEnd, problem, preview: p };\n setLinking(linkRef.current);\n setPreview(p);\n onPreview?.(p ? { mode: \"link\", task: byId.get(targetId), ...p.info } : null);\n }\n\n async function finishLink(e) {\n const l = linkRef.current;\n if (!l || e.pointerId !== l.pointerId) return;\n linkRef.current = null;\n setLinking(null);\n const p = l.preview;\n setPreview(null);\n onPreview?.(null);\n if (!l.targetId) return;\n if (l.problem) return setNote(l.problem);\n const res = await onLink?.({ taskId: l.targetId, dependsOnId: l.fromId, kind: KIND_OF[`${l.fromEnd}:${l.targetEnd}`] }, p);\n if (res && res.ok === false) setNote(res.error ?? \"That link was not saved.\");\n }\n\n useEffect(() => {\n if (!preview && !linking) return undefined;\n const onKey = (e) => {\n if (e.key === \"Escape\") cancelAll();\n };\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [preview, linking]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // ---- keyboard on the chart ----------------------------------------------------\n function onKeyDown(e) {\n if (editing || e.target?.tagName === \"INPUT\") return;\n const i = selected ? rowOf.get(selected) : null;\n const task = selected ? byId.get(selected) : null;\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n const next = i == null ? 0 : Math.max(0, Math.min(visible.length - 1, i + (e.key === \"ArrowDown\" ? 1 : -1)));\n if (visible[next]) onSelect?.(visible[next].id);\n return;\n }\n if (!task) return;\n if (e.altKey && (e.key === \"ArrowRight\" || e.key === \"ArrowLeft\") && onMove && !e.shiftKey) {\n e.preventDefault();\n const r = plan.rows.get(String(task.id));\n if (!r?.dated || r.pinned) return;\n const step = e.key === \"ArrowRight\" ? 1 : -1;\n const from = dayOf(r.milestone ? r.due : r.start);\n const to = CAL.dayAt((r.milestone ? CAL.lastIndex(from) : CAL.startIndex(from)) + step);\n const p = previewFor(movesFor({ id: String(task.id), mode: \"move\", start: r.start }, to - from));\n onMove(p.moves, p).then((res) => res && res.ok === false && setNote(res.error ?? \"That move was not saved.\"));\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n onOpen?.(task);\n } else if (e.key === \"F2\") {\n e.preventDefault();\n setEditing({ id: String(task.id), field: \"title\" });\n } else if (e.key === \"Escape\") onSelect?.(null);\n }\n\n async function commitEdit(task, field, value) {\n setEditing(null);\n const res = await onEdit?.(task, field, value);\n if (res && res.ok === false) setNote(res.error ?? \"That change was not saved.\");\n }\n\n async function submitDraft(milestoneKey = false) {\n const title = draft.title.trim();\n if (!title || !onAdd || busy) return;\n setBusy(true);\n const milestone = draft.milestone || milestoneKey;\n const res = await onAdd({ title, days: Number(draft.days) || null, milestone, parentId: draft.parentId, afterId: draft.afterId });\n setBusy(false);\n if (res && res.ok === false) setNote(res.error ?? \"That task was not added.\");\n else setDraft((d) => ({ ...d, title: \"\", days: \"\", milestone: false, afterId: res?.id ? String(res.id) : d.afterId }));\n addRef.current?.focus();\n }\n\n // ---- the sheet's divider --------------------------------------------------------\n function startDivider(e) {\n e.preventDefault();\n const x0 = e.clientX;\n const w0 = gridW;\n let last = w0;\n const move = (ev) => {\n last = Math.max(fixedWidth(cols) + 140, Math.min(760, w0 + ev.clientX - x0));\n setGridState(last);\n };\n const up = () => {\n window.removeEventListener(\"pointermove\", move);\n window.removeEventListener(\"pointerup\", up);\n onGridWidth?.(last);\n };\n window.addEventListener(\"pointermove\", move);\n window.addEventListener(\"pointerup\", up);\n }\n const divider = narrow ? null : (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label=\"Resize the task sheet\"\n aria-valuenow={gridW}\n tabIndex={0}\n onPointerDown={startDivider}\n onKeyDown={(e) => {\n if (e.key === \"ArrowLeft\" || e.key === \"ArrowRight\") {\n e.preventDefault();\n setGridState(Math.max(fixedWidth(cols) + 140, Math.min(760, gridW + (e.key === \"ArrowRight\" ? 16 : -16))));\n }\n }}\n className=\"absolute inset-y-0 -right-[3px] z-50 w-[6px] cursor-col-resize outline-none hover:bg-ring/30 focus-visible:bg-ring/40\"\n />\n );\n\n // ---- render -------------------------------------------------------------------\n const finish = preview?.finishAfter ?? plan.finish;\n const gap = gapOf(finish, target);\n const marks = {\n today: showToday && inRange(today) ? X(today) + ppd / 2 : null,\n target: target && inRange(target) ? X(target) + ppd : null,\n targetIso: target,\n finish: finish && inRange(finish) ? X(finish) + ppd : null,\n finishFrom: preview && plan.finish && inRange(plan.finish) ? X(plan.finish) + ppd : null,\n moving: Boolean(preview && !linking),\n gap,\n };\n const bodyH = Math.max(visible.length, 1) * rowH + (addIndex != null ? ADD_H : 0);\n const addAfter = addIndex != null && addIndex < visible.length - 1 ? addIndex : null;\n const addParent = draft.parentId ? byId.get(String(draft.parentId)) : null;\n const ghostMap = preview?.from?.size ? preview.from : ghosts;\n const dragInfo = preview?.info ?? null;\n const laneH = 20;\n const laneHead = 28;\n const hasLanes = Boolean(lanes?.length);\n const lanesH = hasLanes ? laneHead + lanes.length * laneH : 0;\n const rowTops = visible.map((_, i) => rowTop(i));\n const selectedRows = new Set([...(selected ? [selected] : []), ...picked].map((id) => rowOf.get(String(id))).filter((i) => i != null));\n const addAt = addIndex != null && addIndex < visible.length ? rowTop(addIndex) + rowH : bodyH;\n\n const addRow = onAdd ? (\n <AddRow\n open={adding}\n onOpen={() => {\n setAdding(true);\n setDraft({ title: \"\", days: \"\", milestone: false, parentId: null, afterId: null });\n setTimeout(() => addRef.current?.focus(), 20);\n }}\n draft={draft}\n setDraft={setDraft}\n onSubmit={submitDraft}\n busy={busy}\n addRef={addRef}\n parentTitle={addParent?.title}\n aboveTitle={draft.afterId ? byId.get(draft.afterId)?.title : null}\n onIndent={() => setDraft((d) => ({ ...d, parentId: d.afterId, afterId: null }))}\n height={rowH}\n />\n ) : null;\n const addExtra = onAdd && !adding ? rowH : 0;\n\n const rootStyle = height === \"fill\" ? undefined : { height };\n\n return (\n <div className={cx(\"flex min-h-0 flex-col\", height === \"fill\" && \"h-full\", className)} style={rootStyle}>\n {error ? (\n <p role=\"alert\" className=\"shrink-0 border-b border-destructive px-3 py-2 text-[13px] text-destructive\">\n {typeof error === \"string\" ? error : \"The plan could not be read.\"}\n </p>\n ) : null}\n {note ? (\n <p role=\"alert\" className=\"flex shrink-0 items-start justify-between gap-3 border-b border-border bg-card px-3 py-2 text-[13px]\">\n <span>{note}</span>\n <button type=\"button\" onClick={() => setNote(null)} className=\"shrink-0 text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\" aria-label=\"Dismiss the message\">\n <Icon name=\"x\" size={16} />\n </button>\n </p>\n ) : null}\n <div\n ref={scroller}\n role=\"region\"\n aria-label={label}\n aria-busy={loading || undefined}\n tabIndex={0}\n onKeyDown={onKeyDown}\n className=\"relative min-h-0 flex-1 overflow-auto bg-card outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\"\n >\n <div className=\"relative\" style={{ width: gridW + trackW, minHeight: HEAD + bodyH + addExtra + lanesH }}>\n <div className=\"sticky top-0 z-30 flex border-b border-border bg-background\" style={{ height: HEAD }}>\n <div className=\"sticky left-0 z-40 shrink-0 border-r border-border bg-background\" style={{ width: gridW }}>\n <SheetHead cols={cols} />\n {divider}\n </div>\n <div className=\"relative\" style={{ width: trackW }}>\n <TrackHead axis={axis} range={range} ppd={ppd} z={z} gridW={gridW} marks={marks} />\n </div>\n </div>\n\n <div className=\"flex\">\n <div className=\"sticky left-0 z-20 shrink-0 border-r border-border bg-card\" style={{ width: gridW }}>\n {loading\n ? Array.from({ length: 8 }, (_, i) => (\n <div key={i} className=\"flex items-center gap-2 border-b border-border px-3\" style={{ height: rowH }}>\n <span className=\"h-2.5 w-8 animate-pulse rounded-[2px] bg-muted\" />\n <span className=\"h-2.5 flex-1 animate-pulse rounded-[2px] bg-muted\" style={{ maxWidth: `${40 + ((i * 17) % 50)}%` }} />\n </div>\n ))\n : visible.map((e, i) => {\n const t = byId.get(e.id);\n const r = live.rows.get(e.id);\n const d = shown.get(e.id);\n const late = r && !r.summary && !r.pinned && r.dated && !r.milestone && d?.due && d.due < today && r.progress < 100;\n const lateMs = r?.milestone && !r.pinned && d?.due && d.due < today;\n return (\n <div key={e.id}>\n <SheetRow\n e={e}\n t={t}\n r={r}\n cols={cols}\n rowH={rowH}\n critical={critical}\n selected={selected === e.id || picked.has(e.id)}\n closed={closed.has(e.id)}\n cells={cellsOf(e.id)}\n foldedCount={r?.summary ? `${r.leafIds.length} ${r.leafIds.length === 1 ? \"task\" : \"tasks\"}` : null}\n tags={tags?.get?.(e.id)}\n late={late || lateMs}\n dim={highlight && !highlight.has(e.id) && !r?.summary}\n moving={moving}\n editing={editing?.id === e.id ? editing : null}\n onSelect={onSelect}\n onOpen={onOpen}\n onToggle={onToggle}\n onOpenLink={onOpenLink}\n onStartEdit={(field) => {\n onSelect?.(e.id);\n setEditing({ id: e.id, field });\n }}\n onCommit={(field, v) => commitEdit(t, field, v)}\n onCancel={() => setEditing(null)}\n onHover={() => setHovered(e.id)}\n menu={rowMenu && (hovered === e.id || selected === e.id) ? rowMenu(t, { row: e, plan: r }) : null}\n />\n {addAfter === i && adding ? addRow : null}\n </div>\n );\n })}\n {addAfter == null ? addRow : null}\n {hasLanes ? (\n <div>\n <div className=\"flex items-center gap-2 border-b border-t border-border bg-muted px-3 text-[12px] font-semibold\" style={{ height: laneHead }}>\n {lanesTitle}\n </div>\n <LaneLabels lanes={lanes} laneH={laneH} width={gridW - 1} />\n </div>\n ) : null}\n </div>\n\n <div\n ref={track}\n className={cx(\"relative\", linking && \"cursor-crosshair\")}\n style={{ width: trackW, height: bodyH + addExtra + lanesH, touchAction: dragRef.current || linking ? \"none\" : \"pan-x pan-y\" }}\n onPointerMove={onTrackMove}\n onPointerUp={onTrackUp}\n onPointerCancel={cancelAll}\n >\n <Paper axis={axis} range={range} ppd={ppd} z={z} marks={marks} rowTops={rowTops} rowH={rowH} selectedRows={selectedRows} />\n <Links lines={lines} width={trackW} height={bodyH} critical={critical} onOpenLink={onOpenLink} />\n <Ghosts ghosts={ghostMap} visible={visible} X={X} ppd={ppd} rowH={rowH} rowTop={rowTop} live={live} kind=\"was\" bar={BAR} />\n <Ghosts ghosts={pencil} visible={visible} X={X} ppd={ppd} rowH={rowH} rowTop={rowTop} live={live} kind=\"pencil\" bar={BAR} />\n {addIndex != null && !draft.milestone ? (\n <span aria-hidden=\"true\" className=\"absolute h-3.5 rounded-[2px] border border-dashed border-info bg-info/15\" style={{ left: today ? X(today) : 0, width: Math.max(1, Number(draft.days) || 3) * ppd * 1.4, top: rowTop(addIndex) + rowH + (ADD_H - 14) / 2 }} />\n ) : null}\n\n {visible.map((e, i) => {\n const t = byId.get(e.id);\n const r = live.rows.get(e.id);\n const d = shown.get(e.id);\n const y = rowTop(i) + rowH / 2;\n const n = notes?.get?.(e.id);\n if (!r || !d) {\n return n ? (\n <span key={e.id} className={cx(\"absolute -translate-y-1/2 whitespace-nowrap text-[12px]\", n.tone === \"problem\" ? \"font-medium text-destructive\" : \"text-muted-foreground\")} style={{ left: 12, top: y }}>\n {n.text}\n </span>\n ) : null;\n }\n const isSel = selected === e.id || picked.has(e.id);\n const dim = highlight && !highlight.has(e.id) && !r.summary;\n const done = r.pinned;\n const late = !r.summary && !done && !r.milestone && d.due < today && r.progress < 100;\n const title = barLabel(t, r);\n const common = {\n tabIndex: -1,\n onClick: (ev) => {\n ev.stopPropagation();\n if (suppress.current) return;\n onSelect?.(e.id, { additive: ev.metaKey || ev.ctrlKey || ev.shiftKey });\n },\n onDoubleClick: () => onOpen?.(t),\n title,\n \"aria-label\": title,\n };\n const crit = critical && r.critical && !done;\n const labelEl = (x, extra = 0) => {\n const person = people?.get?.(String(t.assignee_id ?? \"\"));\n const who = firstName(person?.name ?? t.assignee_name);\n const text = n?.text ?? who;\n return text || late ? (\n <span className=\"pointer-events-none absolute top-1/2 flex -translate-y-1/2 items-center gap-1.5 whitespace-nowrap text-[11px]\" style={{ left: x + 6 + extra }}>\n {late ? <span className=\"rounded-[3px] border border-warning px-1 text-[10px] font-semibold leading-4 text-warning\">Late</span> : null}\n {text ? <span className={n?.tone === \"problem\" ? \"font-medium text-destructive\" : n?.tone === \"crit\" ? \"font-medium text-destructive\" : \"text-muted-foreground\"}>{text}</span> : null}\n </span>\n ) : null;\n };\n const handles =\n onLink && !r.summary && !done ? (\n <>\n <button type=\"button\" tabIndex={-1} aria-label={`Link from the start of ${t.title}`} onPointerDown={(ev) => startLink(ev, e.id, \"start\")} className={cx(\"absolute top-1/2 z-20 size-2 -translate-y-1/2 rounded-full border-2 border-ring bg-background opacity-0 outline-none group-hover:opacity-100\", isSel && \"opacity-100\")} style={{ left: -11, touchAction: \"none\" }} />\n <button type=\"button\" tabIndex={-1} aria-label={`Link from the end of ${t.title}`} onPointerDown={(ev) => startLink(ev, e.id, \"finish\")} className={cx(\"absolute top-1/2 z-20 size-2 -translate-y-1/2 rounded-full border-2 border-ring bg-background opacity-0 outline-none group-hover:opacity-100\", isSel && \"opacity-100\")} style={{ right: -11, touchAction: \"none\" }} />\n </>\n ) : null;\n const target = linking?.targetId === e.id;\n\n if (r.milestone) {\n const cx0 = X(d.due) + ppd / 2;\n return (\n <div key={e.id} className={cx(\"group absolute z-10\", dim && \"opacity-40\")} style={{ left: cx0 - 9, top: y - 9, width: 18, height: 18 }}>\n <button type=\"button\" {...common} onPointerDown={(ev) => startDrag(ev, e.id, \"move\")} className={cx(\"absolute inset-0 grid place-items-center rounded-[2px] outline-none focus-visible:ring-2 focus-visible:ring-ring\", onMove && !done && \"cursor-grab\", target && (linking.problem ? \"ring-2 ring-destructive\" : \"ring-2 ring-ring\"))}>\n <span className={cx(\"block size-3 rotate-45\", done ? \"bg-chart-2\" : crit ? \"bg-destructive\" : \"bg-foreground\", isSel && \"ring-2 ring-ring ring-offset-1 ring-offset-card\")} />\n </button>\n {handles}\n <span className={cx(NUM, \"pointer-events-none absolute left-full top-1/2 ml-2 -translate-y-1/2 whitespace-nowrap text-[11px]\", crit ? \"font-semibold text-destructive\" : \"text-muted-foreground\")}>{n?.text ?? dayWords(d.due)}</span>\n </div>\n );\n }\n\n const x0 = X(d.start);\n const w = Math.max(ppd, X(d.due) + ppd - x0);\n if (r.summary) {\n const folded = closed.has(e.id);\n const kids = folded ? r.leafIds.map((id) => ({ id, row: live.rows.get(id), d: shown.get(id) })).filter((k) => k.row?.milestone && k.d) : [];\n return (\n <div key={e.id} className=\"group absolute z-10\" style={{ left: x0, top: y - 4, width: w, height: 12 }}>\n <button type=\"button\" {...common} onPointerDown={(ev) => startDrag(ev, e.id, \"move\")} className={cx(\"absolute inset-x-0 top-0 h-[6px] outline-none focus-visible:ring-2 focus-visible:ring-ring\", done ? \"bg-chart-2\" : \"bg-foreground\", onMove && !done && \"cursor-grab\")}>\n <span className={cx(\"absolute left-0 top-[6px] size-0 border-r-[5px] border-t-[5px] border-r-transparent\", done ? \"border-t-chart-2\" : \"border-t-foreground\")} />\n <span className={cx(\"absolute right-0 top-[6px] size-0 border-l-[5px] border-t-[5px] border-l-transparent\", done ? \"border-t-chart-2\" : \"border-t-foreground\")} />\n </button>\n {kids.map((k) => (\n <span key={k.id} title={byId.get(k.id)?.title} className={cx(\"absolute -top-[10px] size-[10px] -translate-x-1/2 rotate-45 border-[1.5px] border-card\", k.row.pinned ? \"bg-chart-2\" : critical && k.row.critical ? \"bg-destructive\" : \"bg-foreground\")} style={{ left: X(k.d.due) + ppd / 2 - x0 }} />\n ))}\n {n ? (\n <span className=\"pointer-events-none absolute -top-[4px] whitespace-nowrap text-[11px] text-muted-foreground\" style={{ left: w + 8 }}>\n {n.text}\n </span>\n ) : null}\n </div>\n );\n }\n const loop = r.stuck;\n const guess = guesses?.has?.(e.id);\n const barTop = y - BAR / 2 - (baseline ? 2 : 0);\n const base = baseline?.get?.(e.id);\n const dragging = preview && dragRef.current?.id === e.id;\n const movedByDrag = preview?.dates?.has(e.id);\n const trackClass = loop ? \"border border-dashed border-destructive bg-destructive/15\" : guess ? \"border border-dashed border-chart-2 bg-transparent\" : crit ? \"bg-destructive\" : done ? \"bg-chart-2\" : \"bg-chart-1\";\n const fill = crit ? \"bg-foreground/35\" : \"bg-chart-2\";\n const mark = tags?.get?.(e.id)?.some((g) => g.kind === \"unsaved\");\n return (\n <div key={e.id} className={cx(\"group absolute z-10\", dim && \"opacity-40\")} style={{ left: x0, top: barTop, width: w, height: BAR }}>\n <button\n type=\"button\"\n {...common}\n onPointerDown={(ev) => startDrag(ev, e.id, \"move\")}\n className={cx(\n \"absolute inset-0 overflow-hidden rounded-[2px] outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n trackClass,\n isSel && \"ring-2 ring-ring\",\n (dragging || movedByDrag) && !isSel && \"ring-[1.5px] ring-ring\",\n mark && \"ring-2 ring-destructive\",\n target && (linking.problem ? \"ring-2 ring-destructive\" : \"ring-2 ring-ring\"),\n onMove && !done && \"cursor-grab\",\n )}\n >\n {loop ? <span className=\"absolute inset-0 opacity-40\" style={{ backgroundImage: \"repeating-linear-gradient(135deg, currentColor 0 2px, transparent 2px 6px)\" }} /> : null}\n {!loop && !guess && !done && r.progress > 0 ? <span className={cx(\"absolute inset-y-0 left-0\", fill)} style={{ width: `${r.progress}%` }} /> : null}\n </button>\n {base ? <span aria-hidden=\"true\" className=\"pointer-events-none absolute h-1 rounded-[1px] bg-chart-3\" style={{ left: X(base.start) - x0, width: Math.max(ppd, X(base.due) + ppd - X(base.start)), top: BAR + 2 }} /> : null}\n {onMove && !done ? (\n <>\n <span onPointerDown={(ev) => startDrag(ev, e.id, \"start\")} aria-hidden=\"true\" className={cx(\"absolute z-10 cursor-ew-resize\", dragging || isSel ? \"-inset-y-[2px] -left-[2px] w-1.5 rounded-[2px] bg-ring\" : \"inset-y-0 left-0 w-2\")} style={{ touchAction: \"none\" }} />\n <span onPointerDown={(ev) => startDrag(ev, e.id, \"end\")} aria-hidden=\"true\" className={cx(\"absolute z-10 cursor-ew-resize\", dragging || isSel ? \"-inset-y-[2px] -right-[2px] w-1.5 rounded-[2px] bg-ring\" : \"inset-y-0 right-0 w-2\")} style={{ touchAction: \"none\" }} />\n </>\n ) : null}\n {handles}\n {labelEl(w, isSel ? 8 : 0)}\n </div>\n );\n })}\n\n {dragInfo && dragRef.current ? <DragTip info={dragInfo} task={byId.get(dragInfo.id)} box={boxOf(dragInfo.id)} tasks={tasks} plan={plan} /> : null}\n\n {linking ? (\n (() => {\n const dx = linking.to.x - linking.from.x;\n const dy = linking.to.y - linking.from.y;\n const lines2 = linking.targetId\n ? linkLines({ task: byId.get(linking.targetId), predecessor: byId.get(linking.fromId), kind: KIND_OF[`${linking.fromEnd}:${linking.targetEnd}`], info: linking.preview?.info, problem: linking.problem })\n : null;\n return (\n <>\n <span aria-hidden=\"true\" className={cx(\"pointer-events-none absolute z-30 h-0 origin-left border-t-[1.5px] border-dashed\", linking.problem ? \"border-destructive\" : \"border-ring\")} style={{ left: linking.from.x, top: linking.from.y, width: Math.sqrt(dx * dx + dy * dy), transform: `rotate(${(Math.atan2(dy, dx) * 180) / Math.PI}deg)` }} />\n {lines2 ? (\n <div role=\"status\" className=\"pointer-events-none absolute z-40 max-w-80 rounded-[4px] border border-border bg-popover px-2.5 py-2 text-[12px] leading-snug text-popover-foreground shadow-lg\" style={{ left: linking.to.x + 12, top: linking.to.y + 10 }}>\n <div className={lines2.tone === \"problem\" ? \"font-medium text-destructive\" : undefined}>{lines2.text}</div>\n {lines2.tail ? <div className={lines2.tone === \"late\" ? \"font-semibold text-warning\" : \"text-muted-foreground\"}>{lines2.tail}</div> : null}\n </div>\n ) : null}\n </>\n );\n })()\n ) : null}\n\n {visible.length === 0 && !loading ? (\n <div className=\"absolute inset-x-0 top-6 grid place-items-center px-6 text-center\">\n <p className=\"max-w-sm text-[13px] text-muted-foreground\">{emptyText}</p>\n </div>\n ) : null}\n\n {hasLanes ? <LaneTrack lanes={lanes} X={X} ppd={ppd} laneH={laneH} top={bodyH + addExtra + laneHead} /> : null}\n </div>\n </div>\n </div>\n </div>\n </div>\n );\n}\n\n/** The note by a bar while it moves: what it is, where it ends, what it costs, in the canvas's words. */\nfunction DragTip({ info, task, box, tasks, plan }) {\n if (!box || !task) return null;\n const next = (() => {\n const l = [...plan.links.values()].find((x) => x.valid && x.dependsOnId === info.id);\n return l ? tasks.find((t) => String(t.id) === l.taskId)?.title : null;\n })();\n const lines = dragLines(info, task, { nextTitle: next });\n return (\n <div role=\"status\" className=\"pointer-events-none absolute z-40 grid w-max max-w-80 gap-0.5 rounded-[4px] border border-border bg-popover px-2.5 py-2 text-[12px] leading-snug text-popover-foreground shadow-lg\" style={{ left: box.x1 + 14, top: Math.max(4, box.y - 64) }}>\n <span className=\"text-[13px] font-semibold\">{lines.title}</span>\n <span className={NUM}>{lines.first}</span>\n <span>{lines.second}</span>\n {lines.slack ? <span className=\"text-muted-foreground\">{lines.slack}</span> : null}\n <span className={cx(NUM, lines.third.tone === \"late\" ? \"font-semibold text-warning\" : lines.third.tone === \"early\" ? \"font-semibold text-success\" : \"text-muted-foreground\")}>{lines.third.text}</span>\n <span className=\"text-muted-foreground\">{lines.last}</span>\n </div>\n );\n}\n\n"
24
+ },
25
+ {
26
+ "path": "registry/blocks/schedule-gantt/sheet.jsx",
27
+ "type": "registry:component",
28
+ "target": "@components/makersclaw/blocks/schedule-gantt/sheet.jsx",
29
+ "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { Icon, Menu, cx } from \"../../ui\";\nimport { monthDay } from \"./gantt-model\";\n\n// The task sheet on the chart's left, one row for one row of bars: the\n// outline number, the task (a phase folds, a milestone is a diamond, its\n// tags in words), and whichever columns the page asked for: start, finish,\n// length in working days, how much is done, the owner, what it waits on,\n// slack, and how far it sits from a baseline. A row's \"…\" holds every\n// change; double-click a name, a length, a % or a \"waits on\" to type it.\n// The last row adds a task in place.\n\nexport const ROW = 30;\n\nconst NUM = \"tabular-nums\";\n\nfunction Chevron({ open }) {\n return <Icon name={open ? \"chevron-down\" : \"chevron-right\"} size={14} />;\n}\n\nconst TAG = {\n late: \"border border-warning text-warning\",\n crit: \"border border-destructive text-destructive\",\n pole: \"border border-destructive text-destructive\",\n unsaved: \"border border-destructive text-destructive\",\n blue: \"border border-info text-info\",\n ink: \"bg-foreground text-background\",\n quiet: \"text-muted-foreground\",\n};\n\n/** A tag in a row: words in a small outlined pill; the agent's is a filled badge with its reason on hover. */\nexport function TagChip({ tag }) {\n if (tag.kind === \"agent\")\n return (\n <span title={tag.title ?? tag.label ?? \"The agent did this\"} className=\"inline-flex size-[14px] shrink-0 items-center justify-center rounded-[3px] bg-info text-background\">\n <Icon name=\"sparkles\" size={9} label={tag.title ?? \"The agent did this\"} />\n </span>\n );\n if (tag.kind === \"dot\") return <span title={tag.title ?? tag.label} aria-label={tag.title ?? tag.label} className=\"size-1.5 shrink-0 rounded-full bg-warning\" />;\n return <span className={cx(\"inline-flex h-4 shrink-0 items-center whitespace-nowrap rounded-[3px] px-1 text-[10px] font-semibold\", tag.kind === \"quiet\" ? \"font-normal\" : \"\", TAG[tag.kind] ?? TAG.quiet)}>{tag.label}</span>;\n}\n\n/** A dense cell editor: a 30 px row needs its own. */\nexport function CellInput({ type = \"text\", value, onCommit, onCancel, label, align = \"left\", min, max, className, placeholder, quiet = false, autoFocus = true }) {\n const ref = useRef(null);\n const [v, setV] = useState(value ?? \"\");\n const settled = useRef(false);\n useEffect(() => {\n if (!autoFocus) return;\n ref.current?.focus();\n ref.current?.select?.();\n }, []); // eslint-disable-line react-hooks/exhaustive-deps\n const commit = () => {\n if (settled.current) return;\n settled.current = true;\n onCommit(v);\n };\n return (\n <input\n ref={ref}\n type={type}\n aria-label={label}\n value={v}\n min={min}\n max={max}\n placeholder={placeholder}\n onChange={(e) => setV(e.target.value)}\n onBlur={commit}\n onKeyDown={(e) => {\n e.stopPropagation();\n if (e.key === \"Enter\") {\n e.preventDefault();\n commit();\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n settled.current = true;\n onCancel();\n }\n }}\n className={cx(\"h-6 w-full min-w-0 rounded-[3px] bg-background px-1.5 text-[12px] text-foreground outline-none\", quiet ? \"border border-border focus-visible:border-ring\" : \"border border-ring ring-1 ring-ring\", align === \"right\" && \"text-right\", type !== \"text\" && NUM, className)}\n />\n );\n}\n\nexport function SheetHead({ cols }) {\n return (\n <div className=\"flex h-full items-end px-0 pb-1.5\">\n {cols.map((c) => (\n <div key={c.key} className={cx(\"shrink-0 truncate px-1.5 text-[11px] font-semibold text-muted-foreground\", c.key === \"title\" && \"min-w-0 flex-1\", c.right && \"text-right\")} style={c.key === \"title\" ? undefined : { width: c.w }}>\n {c.label}\n </div>\n ))}\n </div>\n );\n}\n\nconst tone = { late: \"text-warning\", early: \"text-success\", same: \"text-muted-foreground\", new: \"text-info\" };\n\n/**\n * One row of the sheet. `r` is the engine's row; `cells` carries what the\n * page worked out (owner words, the waits-on text, the baseline delta).\n * `editing` { field } when a cell is open.\n */\nexport function SheetRow({ e, t, r, cols, selected, closed, cells, tags, late, editing, rowH, critical, onSelect, onOpen, onToggle, onStartEdit, onCommit, onCancel, onOpenLink, menu, dim, onHover, foldedCount, moving }) {\n const summary = r?.summary;\n const done = r?.pinned;\n const crit = critical && r?.critical && !done;\n const editableLength = !summary && !r?.milestone && !done;\n return (\n <div\n data-row={e.id}\n onMouseEnter={onHover}\n onClick={(ev) => onSelect?.(e.id, { additive: ev.metaKey || ev.ctrlKey || ev.shiftKey })}\n onDoubleClick={() => onOpen?.(t)}\n className={cx(\"group/row relative flex items-center border-b border-border text-[13px]\", selected ? \"bg-accent\" : \"hover:bg-muted/50\", dim && \"opacity-50\")}\n style={{ height: rowH }}\n >\n {cols.map((c) => {\n if (c.key === \"outline\")\n return (\n <div key=\"outline\" className={cx(NUM, \"shrink-0 truncate px-1.5 text-[12px]\", crit ? \"text-destructive\" : \"text-muted-foreground\")} style={{ width: c.w }}>\n {e.outline}\n </div>\n );\n if (c.key === \"title\") {\n const isEditing = editing?.field === \"title\";\n return (\n <div key=\"title\" className=\"flex min-w-0 flex-1 items-center gap-1 px-1.5\" style={{ paddingLeft: 6 + e.depth * 14 }}>\n {summary ? (\n <button\n type=\"button\"\n aria-label={closed ? `Open ${t.title}` : `Fold ${t.title}`}\n aria-expanded={!closed}\n onClick={(ev) => {\n ev.stopPropagation();\n onToggle?.(e.id);\n }}\n className=\"grid size-5 shrink-0 place-items-center rounded-[3px] text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <Chevron open={!closed} />\n </button>\n ) : r?.milestone ? (\n <span className=\"grid size-5 shrink-0 place-items-center\">\n <span aria-hidden=\"true\" className={cx(\"size-[9px] rotate-45 bg-foreground\", crit && \"bg-destructive\")} />\n </span>\n ) : (\n <span className=\"size-5 shrink-0\" aria-hidden=\"true\" />\n )}\n {isEditing ? (\n <CellInput value={t.title} label={`Name of ${t.title}`} onCancel={onCancel} onCommit={(v) => onCommit(\"title\", v)} />\n ) : (\n <span\n onDoubleClick={(ev) => {\n ev.stopPropagation();\n onStartEdit?.(\"title\");\n }}\n title={t.title}\n className={cx(\"min-w-0 truncate\", summary ? \"font-semibold\" : done ? \"text-muted-foreground\" : \"text-foreground\")}\n >\n {t.title}\n </span>\n )}\n {closed && summary && foldedCount ? <span className=\"shrink-0 text-[11px] text-muted-foreground\">{foldedCount}</span> : null}\n {late ? <TagChip tag={{ label: \"Late\", kind: \"late\" }} /> : null}\n {(tags ?? []).map((g, i) => (\n <TagChip key={`${g.kind}${g.label ?? i}`} tag={g} />\n ))}\n {menu ? (\n <span className={cx(\"ml-auto shrink-0 opacity-0 group-hover/row:opacity-100 focus-within:opacity-100\", selected && \"opacity-100\")} onClickCapture={() => !selected && onSelect?.(e.id)} onClick={(ev) => ev.stopPropagation()}>\n <Menu label={`${t.title}: change it`} items={menu} />\n </span>\n ) : null}\n </div>\n );\n }\n if (c.key === \"start\" || c.key === \"due\") {\n const iso = c.key === \"start\" ? r?.start : r?.due;\n const shown = c.key === \"due\" && r?.milestone ? \"\" : iso ? monthDay(iso) : null;\n return (\n <div key={c.key} className={cx(NUM, \"shrink-0 truncate px-1.5 text-[12px]\", moving?.has?.(e.id) && \"text-ring\")} style={{ width: c.w }}>\n {shown === null ? <span className=\"text-muted-foreground\">no date</span> : shown}\n </div>\n );\n }\n if (c.key === \"who\") {\n const o = cells?.owner;\n return (\n <div key=\"who\" className=\"shrink-0 truncate px-1.5 text-[12px]\" style={{ width: c.w }} title={o?.title}>\n {summary || r?.milestone ? null : o?.name ? (\n <span className={o.left ? \"text-muted-foreground\" : undefined}>{o.name}{o.left ? \" (left)\" : \"\"}</span>\n ) : (\n <span className=\"text-warning\">no owner</span>\n )}\n </div>\n );\n }\n if (c.key === \"days\") {\n const undated = !r?.dated;\n if (editing?.field === \"days\" || (undated && !summary && editableLength))\n return (\n <div key=\"days\" className=\"shrink-0 px-1\" style={{ width: c.w }} onClick={(ev) => ev.stopPropagation()}>\n <CellInput\n type=\"number\"\n min={1}\n align=\"right\"\n placeholder=\"none\"\n value={cells?.lengthGuess != null ? String(cells.lengthGuess) : undated ? \"\" : String(r?.duration ?? \"\")}\n label={`Length of ${t.title} in working days`}\n onCancel={onCancel}\n onCommit={(v) => (String(v) === String(r?.duration ?? \"\") || v === \"\" ? onCancel() : onCommit(\"days\", v))}\n quiet={undated && editing?.field !== \"days\"}\n autoFocus={!undated || editing?.field === \"days\"}\n />\n </div>\n );\n const text = summary ? (r?.dated && r.duration != null ? `${r.duration} wd` : \"\") : r?.milestone ? \"0 wd\" : r?.duration != null ? `${r.duration} wd${cells?.guessed ? \"?\" : \"\"}` : \"\";\n return (\n <div\n key=\"days\"\n onDoubleClick={(ev) => {\n if (!editableLength) return;\n ev.stopPropagation();\n onStartEdit?.(\"days\");\n }}\n className={cx(NUM, \"shrink-0 truncate px-1.5 text-right text-[12px]\", cells?.guessed ? \"text-info\" : summary || r?.milestone ? \"text-muted-foreground\" : \"text-foreground\")}\n style={{ width: c.w }}\n title={cells?.guessed ? \"A guess from the title\" : undefined}\n >\n {text}\n </div>\n );\n }\n if (c.key === \"done\") {\n if (editing?.field === \"progress\")\n return (\n <div key=\"done\" className=\"shrink-0 px-1\" style={{ width: c.w }} onClick={(ev) => ev.stopPropagation()}>\n <CellInput type=\"number\" min={0} max={100} align=\"right\" value={String(r?.progress ?? 0)} label={`Done, in per cent, of ${t.title}`} onCancel={onCancel} onCommit={(v) => onCommit(\"progress\", v)} />\n </div>\n );\n return (\n <div\n key=\"done\"\n onDoubleClick={(ev) => {\n if (summary || r?.milestone || done) return;\n ev.stopPropagation();\n onStartEdit?.(\"progress\");\n }}\n className={cx(NUM, \"flex shrink-0 justify-end px-1.5 text-[12px] text-muted-foreground\")}\n style={{ width: c.w }}\n >\n {done && !summary ? <Icon name=\"check\" size={14} label=\"Done\" /> : r?.milestone || !r ? \"\" : `${r.progress}%`}\n </div>\n );\n }\n if (c.key === \"waits\") {\n const text = cells?.waits ?? \"\";\n if (editing?.field === \"waits\")\n return (\n <div key=\"waits\" className=\"shrink-0 px-1\" style={{ width: c.w }} onClick={(ev) => ev.stopPropagation()}>\n <CellInput value={text} label={`What ${t.title} waits on`} placeholder=\"2.2FS+2d\" onCancel={onCancel} onCommit={(v) => (v === text ? onCancel() : onCommit(\"waits\", v))} />\n </div>\n );\n return (\n <div\n key=\"waits\"\n onDoubleClick={(ev) => {\n ev.stopPropagation();\n onStartEdit?.(\"waits\");\n }}\n onClick={(ev) => {\n if (cells?.waitsLinks?.length === 1 && onOpenLink && (ev.metaKey || ev.ctrlKey)) {\n ev.stopPropagation();\n onOpenLink(cells.waitsLinks[0]);\n }\n }}\n title={text ? `${text} (double-click to change)` : \"Double-click to say what it waits on\"}\n className={cx(NUM, \"shrink-0 truncate px-1.5 text-[12px]\", cells?.waitsBroken ? \"text-destructive\" : \"text-foreground\")}\n style={{ width: c.w }}\n >\n {text}\n </div>\n );\n }\n if (c.key === \"slack\") {\n const text = cells?.slack ?? \"\";\n return (\n <div key=\"slack\" className={cx(NUM, \"shrink-0 truncate px-1.5 text-right text-[12px]\", crit ? \"font-semibold text-destructive\" : \"text-muted-foreground\")} style={{ width: c.w }}>\n {text}\n </div>\n );\n }\n if (c.key === \"vs\") {\n const d = cells?.delta;\n return (\n <div key=\"vs\" className={cx(NUM, \"shrink-0 truncate px-1.5 text-right text-[12px]\", d?.tone ? tone[d.tone] : \"text-muted-foreground\")} style={{ width: c.w }}>\n {d?.text ?? \"\"}\n </div>\n );\n }\n return null;\n })}\n </div>\n );\n}\n\n/**\n * The row that adds in place: a title and a length. Enter adds a task,\n * Shift+Enter a milestone; Tab makes it a sub-task of the row above; Esc\n * cancels. Closed, it is one quiet line that says so.\n */\nexport function AddRow({ open, onOpen, draft, setDraft, onSubmit, busy, addRef, parentTitle, aboveTitle, onIndent, height = ROW }) {\n if (!open)\n return (\n <button type=\"button\" onClick={onOpen} className=\"flex w-full items-center gap-2 border-b border-border px-3 text-left text-[13px] text-muted-foreground outline-none hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\" style={{ height }}>\n <Icon name=\"plus\" size={14} />\n <span>Add task</span>\n <kbd className=\"rounded-[3px] border border-border px-1 text-[10px]\">Enter</kbd>\n <span>milestone</span>\n <kbd className=\"rounded-[3px] border border-border px-1 text-[10px]\">Shift+Enter</kbd>\n </button>\n );\n return (\n <div className=\"border-b border-border bg-accent\">\n <div className=\"flex items-center gap-2 px-3\" style={{ height: height + 6 }}>\n <span className=\"w-9 shrink-0\">\n <Icon name={draft.milestone ? \"diamond\" : \"plus\"} size={14} className=\"text-muted-foreground\" />\n </span>\n <input\n ref={addRef}\n value={draft.title}\n onChange={(ev) => setDraft((d) => ({ ...d, title: ev.target.value }))}\n onKeyDown={(ev) => {\n ev.stopPropagation();\n if (ev.key === \"Enter\") {\n ev.preventDefault();\n onSubmit(ev.shiftKey);\n } else if (ev.key === \"Tab\" && !ev.shiftKey && aboveTitle && !draft.milestone) {\n ev.preventDefault();\n onIndent?.();\n } else if (ev.key === \"Escape\") {\n setDraft({ title: \"\", days: \"\", milestone: false, parentId: null, afterId: null });\n ev.currentTarget.blur();\n }\n }}\n placeholder={parentTitle ? `A sub-task of ${parentTitle}` : draft.milestone ? \"Name the milestone\" : \"Add a task\"}\n aria-label={parentTitle ? `Add a sub-task of ${parentTitle}` : draft.milestone ? \"Milestone name\" : \"Task title\"}\n disabled={busy}\n className=\"h-6 min-w-0 flex-1 rounded-[3px] border border-ring bg-background px-1.5 text-[13px] text-foreground outline-none ring-1 ring-ring placeholder:text-muted-foreground\"\n />\n {!draft.milestone ? (\n <input\n value={draft.days}\n onChange={(ev) => setDraft((d) => ({ ...d, days: ev.target.value.replace(/[^0-9]/g, \"\").slice(0, 3) }))}\n onKeyDown={(ev) => {\n ev.stopPropagation();\n if (ev.key === \"Enter\") {\n ev.preventDefault();\n onSubmit(ev.shiftKey);\n }\n }}\n inputMode=\"numeric\"\n placeholder=\"5\"\n aria-label=\"Length in working days\"\n className={cx(NUM, \"h-6 w-10 shrink-0 rounded-[3px] border border-ring bg-background px-1 text-right text-[13px] outline-none ring-1 ring-ring\")}\n />\n ) : null}\n </div>\n </div>\n );\n}\n"
30
+ },
31
+ {
32
+ "path": "registry/blocks/schedule-gantt/track.jsx",
33
+ "type": "registry:component",
34
+ "target": "@components/makersclaw/blocks/schedule-gantt/track.jsx",
35
+ "content": "\"use client\";\n\nimport { Fragment } from \"react\";\nimport { cx } from \"../../ui\";\nimport { monthDay } from \"./gantt-model\";\n\n// The time scale's paper and lines: the header with its flags (today, the\n// target and the finish, with the gap between them as a line you can read),\n// weekends and the grid, the target as a dashed rule, the finish as a solid\n// one, today in the accent, the link lines (elbows between the bars, the\n// critical ones red when the page asks for them, a broken one dashed with a\n// cross), ghosts of where bars were, pencil for where a suggestion would put\n// them, and the lanes under the rows (who is on what, on the same scale).\n\nconst NUM = \"tabular-nums\";\nexport const HEAD = 56;\n\n/** The header: the axis tiers, then a band with Today, Target and Finish. */\nexport function TrackHead({ axis, range, ppd, z, gridW, marks }) {\n const late = marks.gap?.state === \"late\";\n const early = marks.gap?.state === \"early\";\n const finishText = marks.gap?.text ?? \"\";\n const targetText = marks.target != null ? `Target ${monthDay(marks.targetIso)}` : \"\";\n const same = marks.target != null && marks.finish != null && marks.target === marks.finish;\n return (\n <>\n {axis.top.map((s) => (\n <div key={`t${s.start}`} className=\"absolute top-0 h-[18px] border-l border-border\" style={{ left: (s.start - range.from) * ppd, width: (s.end - s.start + 1) * ppd }}>\n <span className=\"sticky block w-max px-1.5 pt-0.5 text-[11px] font-semibold text-foreground\" style={{ left: gridW }}>\n {s.label}\n </span>\n </div>\n ))}\n {axis.bottom.map((s) => (\n <div key={`b${s.start}`} className={cx(NUM, \"absolute top-[18px] h-[18px] overflow-hidden whitespace-nowrap pt-0.5 text-[11px]\", z === \"day\" ? \"text-center\" : \"pl-1\", s.dim ? \"text-muted-foreground/70\" : \"text-muted-foreground\")} style={{ left: (s.start - range.from) * ppd, width: (s.end - s.start + 1) * ppd }}>\n {s.label}\n </div>\n ))}\n <div className=\"absolute inset-x-0 top-[36px] border-t border-border\" />\n {marks.target != null && marks.finish != null && !same ? (\n <span aria-hidden=\"true\" className={cx(\"absolute top-[45px] h-0 border-t-2\", late ? \"border-warning\" : \"border-success\")} style={{ left: Math.min(marks.target, marks.finish), width: Math.abs(marks.finish - marks.target) }} />\n ) : null}\n {marks.finishFrom != null && marks.finishFrom !== marks.finish ? <span aria-hidden=\"true\" className=\"absolute top-[40px] h-3 w-0.5 bg-chart-3\" style={{ left: marks.finishFrom }} /> : null}\n {marks.today != null ? (\n <span className=\"absolute top-[38px] z-10 h-4 -translate-x-1/2 whitespace-nowrap rounded-[3px] bg-primary px-1.5 text-[11px] font-semibold leading-4 text-primary-foreground\" style={{ left: marks.today }}>\n Today\n </span>\n ) : null}\n {marks.target != null && !same ? (\n <span className={cx(\"absolute top-[38px] z-10 h-4 whitespace-nowrap rounded-[3px] border border-dashed border-foreground bg-background px-1.5 text-[11px] font-semibold leading-[14px] text-foreground\", late ? \"-translate-x-full -ml-px\" : \"ml-px\")} style={{ left: marks.target }}>\n {targetText}\n </span>\n ) : null}\n {marks.finish != null ? (\n <span className={cx(\"absolute top-[38px] z-10 h-4 whitespace-nowrap rounded-[3px] px-1.5 text-[11px] font-semibold leading-4\", marks.moving ? \"bg-ring text-primary-foreground\" : \"bg-foreground text-background\", early ? \"-translate-x-full -ml-px\" : \"ml-px\")} style={{ left: marks.finish }}>\n {finishText}\n </span>\n ) : null}\n </>\n );\n}\n\n/** Weekends, grid lines, the selected rows, target, finish and today, under everything. */\nexport function Paper({ axis, range, ppd, z, marks, rowTops, rowH, selectedRows }) {\n return (\n <div aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0\">\n {z === \"month\" || z === \"fit\" || ppd < 4 ? null : axis.shade.map((s) => <div key={`s${s.start}`} className=\"absolute inset-y-0 bg-muted\" style={{ left: (s.start - range.from) * ppd, width: (s.end - s.start + 1) * ppd }} />)}\n {axis.lines.map((l) => (\n <div key={`l${l.day}${l.strong ? \"s\" : \"\"}`} className={cx(\"absolute inset-y-0 w-px\", l.strong ? \"bg-border\" : \"bg-border/50\")} style={{ left: (l.day - range.from) * ppd }} />\n ))}\n {rowTops.map((top, i) => (\n <div key={i} className={cx(\"absolute inset-x-0 border-b border-border\", selectedRows.has(i) && \"bg-accent\")} style={{ top, height: rowH }} />\n ))}\n {marks.target != null ? <div className=\"absolute inset-y-0 border-l-[1.5px] border-dashed border-foreground\" style={{ left: marks.target }} /> : null}\n {marks.finish != null ? <div className={cx(\"absolute inset-y-0 w-0.5 -translate-x-px\", marks.moving ? \"bg-ring\" : \"bg-foreground\")} style={{ left: marks.finish }} /> : null}\n {marks.today != null ? <div className=\"absolute inset-y-0 w-0.5 -translate-x-px bg-primary\" style={{ left: marks.today }} /> : null}\n </div>\n );\n}\n\nconst HEAD_PATH = {\n down: (x, y) => `${x - 3.5},${y - 5} ${x + 3.5},${y - 5} ${x},${y}`,\n up: (x, y) => `${x - 3.5},${y + 5} ${x + 3.5},${y + 5} ${x},${y}`,\n right: (x, y) => `${x - 5},${y - 3.5} ${x - 5},${y + 3.5} ${x},${y}`,\n left: (x, y) => `${x + 5},${y - 3.5} ${x + 5},${y + 3.5} ${x},${y}`,\n};\n\n/**\n * Link lines on one svg: hairline in muted ink, the critical ones red (when\n * the page switches the critical path on), a broken one dashed with a\n * cross, the ones that move with a drag in the ring colour, the lag beside\n * the first leg. Each is pressable (double-click) when `onOpenLink` is given.\n */\nexport function Links({ lines, width, height, critical, onOpenLink }) {\n return (\n <svg aria-hidden=\"true\" className=\"pointer-events-none absolute left-0 top-0 z-[5] overflow-visible\" width={width} height={height}>\n {lines.map((l) => {\n const tone = l.link.violated ? \"broken\" : l.moving ? \"moving\" : critical && l.critical ? \"crit\" : l.touches ? \"strong\" : \"quiet\";\n const stroke = { broken: \"stroke-destructive\", crit: \"stroke-destructive\", moving: \"stroke-ring\", strong: \"stroke-foreground\", quiet: \"stroke-muted-foreground\" }[tone];\n const fill = { broken: \"fill-destructive\", crit: \"fill-destructive\", moving: \"fill-ring\", strong: \"fill-foreground\", quiet: \"fill-muted-foreground\" }[tone];\n const w = tone === \"crit\" || tone === \"broken\" || tone === \"moving\" ? 1.5 : 1;\n const segs = l.route.segments;\n if (!segs.length) return null;\n const d = `M${segs[0].x1 + 0.5} ${segs[0].y1 + 0.5}${segs.map((s) => ` L${s.x2 + 0.5} ${s.y2 + 0.5}`).join(\"\")}`;\n const first = segs.find((s) => s.x1 === s.x2);\n const mid = segs[Math.floor(segs.length / 2)];\n return (\n <Fragment key={l.id}>\n <path d={d} fill=\"none\" className={stroke} strokeWidth={w} strokeDasharray={tone === \"broken\" ? \"4 3\" : undefined} />\n {l.route.head ? <polygon points={HEAD_PATH[l.route.head.dir](l.route.head.x + 0.5, l.route.head.y + 0.5)} className={fill} /> : null}\n {tone === \"broken\" && mid ? (\n <path d={`M${(mid.x1 + mid.x2) / 2 - 4} ${(mid.y1 + mid.y2) / 2 - 4}l8 8M${(mid.x1 + mid.x2) / 2 + 4} ${(mid.y1 + mid.y2) / 2 - 4}l-8 8`} fill=\"none\" className=\"stroke-destructive\" strokeWidth={2} />\n ) : null}\n {l.lag && first ? (\n <text x={first.x1 + 4} y={(first.y1 + first.y2) / 2} dominantBaseline=\"middle\" className={cx(\"text-[10px]\", tone === \"crit\" ? \"fill-destructive\" : \"fill-muted-foreground\")}>\n {l.lag > 0 ? `+${l.lag}` : l.lag}\n </text>\n ) : null}\n {onOpenLink ? (\n <path d={d} fill=\"none\" stroke=\"transparent\" strokeWidth={10} className=\"pointer-events-auto cursor-pointer\" onDoubleClick={() => onOpenLink(l.link)}>\n <title>{`Double-click to change this link`}</title>\n </path>\n ) : null}\n </Fragment>\n );\n })}\n </svg>\n );\n}\n\n/** Where bars were (dashed grey) or where a suggestion would put them (dashed in the agent's colour). */\nexport function Ghosts({ ghosts, visible, X, ppd, rowH, rowTop, live, kind = \"was\", bar = 14 }) {\n if (!ghosts?.size) return null;\n const ring = kind === \"pencil\" ? \"border-info\" : \"border-chart-3\";\n return (\n <div aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0 z-[6]\">\n {visible.map((e, i) => {\n const g = ghosts.get(e.id);\n const r = live.rows.get(e.id);\n if (!g || !r || r.summary) return null;\n const y = (rowTop ? rowTop(i) : i * rowH) + rowH / 2;\n if (r.milestone) {\n const cx0 = X(g.due ?? g.start) + ppd / 2;\n return <span key={e.id} className={cx(\"absolute size-3 -translate-x-1/2 -translate-y-1/2 rotate-45 border-[1.5px] border-dashed\", ring)} style={{ left: cx0, top: y }} />;\n }\n const x0 = X(g.start);\n const w = Math.max(ppd, X(g.due) + ppd - x0);\n return <span key={e.id} className={cx(\"absolute -translate-y-1/2 rounded-[2px] border-[1.5px] border-dashed\", ring)} style={{ left: x0, width: w, top: y, height: bar }} />;\n })}\n </div>\n );\n}\n\n/**\n * Lanes under the rows, on the same scale: one thin row per lane (a person)\n * with a bar for each of its items and a box where two or more overlap\n * (the count in it). The label side is drawn by the sheet's twin\n * (LaneLabels); both take the same lane list.\n */\nexport function LaneTrack({ lanes, X, ppd, laneH = 20, top }) {\n return (\n <div className=\"absolute inset-x-0\" style={{ top }}>\n {lanes.map((lane, i) => (\n <div key={lane.id} className=\"absolute inset-x-0 border-b border-border\" style={{ top: i * laneH, height: laneH }}>\n {(lane.items ?? []).map((it, k) => (\n <span key={`${it.id ?? k}`} title={it.title} className=\"absolute h-1.5 rounded-[2px] bg-chart-1\" style={{ left: X(it.start), width: Math.max(ppd, X(it.due) + ppd - X(it.start)), top: 7 }} />\n ))}\n {(lane.marks ?? []).map((m, k) => (\n <span key={`m${k}`} title={m.title ?? `${m.count} at once`} className=\"absolute rounded-[2px] border-[1.5px] border-warning pl-0.5 text-[10px] font-bold leading-[11px] text-warning\" style={{ left: X(m.from), width: Math.max(ppd, X(m.to) + ppd - X(m.from)), top: 3, height: 14 }}>\n {m.count}\n </span>\n ))}\n </div>\n ))}\n </div>\n );\n}\n\nexport function LaneLabels({ lanes, laneH = 20, width }) {\n return (\n <div style={{ width }}>\n {lanes.map((lane) => (\n <div key={lane.id} className=\"flex items-center gap-1.5 border-b border-border px-3 text-[12px]\" style={{ height: laneH }}>\n <span className=\"truncate\">{lane.label}</span>\n <span className=\"ml-auto shrink-0 text-muted-foreground\">{lane.sub}</span>\n </div>\n ))}\n </div>\n );\n}\n"
36
+ },
37
+ {
38
+ "path": "registry/blocks/schedule-gantt/geometry.js",
39
+ "type": "registry:component",
40
+ "target": "@components/makersclaw/blocks/schedule-gantt/geometry.js",
41
+ "content": "// Gantt geometry: the axis and the link lines, as plain numbers. Pure, so\n// the chart stays readable and the routing can be tested on its own.\n\nimport { dayOf, isoOf, weekdayOf } from \"../schedule-engine\";\n\nexport const ZOOMS = {\n day: { label: \"Days\", ppd: 28, pad: 5 },\n week: { label: \"Weeks\", ppd: 11, pad: 7 },\n month: { label: \"Months\", ppd: 4, pad: 10 },\n // The whole project in the width there is: the block works the ppd out.\n fit: { label: \"Fit\", ppd: 6, pad: 6 },\n};\n\nexport const MONTHS = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\nconst DAY_MS = 86400000;\nconst dateOf = (day) => new Date(day * DAY_MS);\nconst monthStart = (day) => {\n const d = dateOf(day);\n return Math.round(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1) / DAY_MS);\n};\nconst addMonths = (day, n) => {\n const d = dateOf(day);\n return Math.round(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + n, 1) / DAY_MS);\n};\n\n/** \"Oct 28\" */\nexport function shortDate(iso) {\n const d = dayOf(iso);\n if (d == null) return \"\";\n return `${MONTHS[dateOf(d).getUTCMonth()]} ${dateOf(d).getUTCDate()}`;\n}\n\n/** The span the chart covers: every date in `dates`, padded, snapped to the zoom's grid. */\nexport function rangeFor(zoom, dates, today) {\n const { pad } = ZOOMS[zoom] ?? ZOOMS.week;\n let lo = dayOf(today) ?? Math.floor(Date.now() / DAY_MS);\n let hi = lo;\n for (const iso of dates) {\n const d = dayOf(iso);\n if (d == null) continue;\n lo = Math.min(lo, d);\n hi = Math.max(hi, d);\n }\n lo -= pad;\n hi += pad * 2;\n if (zoom === \"month\" || zoom === \"fit\") {\n lo = monthStart(lo);\n hi = zoom === \"fit\" ? hi : addMonths(hi, 1) - 1;\n } else {\n lo -= weekdayOf(lo);\n hi += 6 - weekdayOf(hi);\n }\n const min = zoom === \"day\" ? 35 : zoom === \"week\" ? 91 : zoom === \"fit\" ? 60 : 120;\n if (hi - lo < min) hi = lo + min;\n return { from: lo, to: hi };\n}\n\n/**\n * The axis over [from, to]: `top` and `bottom` tiers of { start, end,\n * label, dim? }, grid `lines` { day, strong }, and weekend `shade` spans.\n */\nexport function axisFor(zoom, from, to) {\n const top = [];\n const bottom = [];\n const lines = [];\n const shade = [];\n if (zoom === \"month\" || zoom === \"fit\") {\n for (let y = dateOf(from).getUTCFullYear(); y <= dateOf(to).getUTCFullYear(); y += 1) {\n const a = Math.round(Date.UTC(y, 0, 1) / DAY_MS);\n const b = Math.round(Date.UTC(y + 1, 0, 1) / DAY_MS) - 1;\n top.push({ start: Math.max(a, from), end: Math.min(b, to), label: String(y) });\n }\n for (let m = monthStart(from); m <= to; m = addMonths(m, 1)) {\n bottom.push({ start: Math.max(m, from), end: Math.min(addMonths(m, 1) - 1, to), label: MONTHS[dateOf(m).getUTCMonth()] });\n lines.push({ day: m, strong: true });\n }\n for (let d = from; d <= to; d += 1) if (weekdayOf(d) === 0 && dateOf(d).getUTCDate() !== 1) lines.push({ day: d, strong: false });\n return { top, bottom, lines, shade };\n }\n for (let m = monthStart(from); m <= to; m = addMonths(m, 1)) {\n const d = dateOf(m);\n top.push({ start: Math.max(m, from), end: Math.min(addMonths(m, 1) - 1, to), label: `${MONTHS[d.getUTCMonth()]} ${d.getUTCFullYear()}` });\n }\n for (let d = from; d <= to; d += 1) {\n const w = weekdayOf(d);\n if (w === 5) shade.push({ start: d, end: d + 1 });\n if (zoom === \"day\") {\n bottom.push({ start: d, end: d, label: String(dateOf(d).getUTCDate()), dim: w >= 5 });\n lines.push({ day: d, strong: w === 0 });\n } else if (w === 0) {\n bottom.push({ start: d, end: Math.min(d + 6, to), label: String(dateOf(d).getUTCDate()) });\n lines.push({ day: d, strong: true });\n }\n }\n return { top, bottom, lines, shade };\n}\n\n/**\n * The line for one link, as orthogonal segments. Boxes are the bars on\n * screen: { x0, x1, y (centre), top, bottom, milestone }. Finish-to-start\n * drops into the successor from above or below at its start, the way a\n * scheduler draws it; the other kinds run beside the bars. `bold` routes\n * into the bar's centre line instead of its edge, so the critical path\n * reads as one continuous line.\n *\n * → { segments: [{ x1, y1, x2, y2 }], head: { x, y, dir } | null }\n * dir: \"down\" | \"up\" | \"left\" | \"right\"\n */\nexport function routeLink(predBox, succ, kind, { rowH = 32, bold = false } = {}) {\n const STUB = 6;\n // A line leaves a milestone from its centre, so it drops straight out\n // of the diamond instead of jogging round its tip.\n const pred = predBox.milestone ? { ...predBox, x0: (predBox.x0 + predBox.x1) / 2, x1: (predBox.x0 + predBox.x1) / 2 } : predBox;\n const below = succ.y > pred.y;\n const boundary = pred.y + (below ? rowH / 2 : -rowH / 2);\n const segs = [];\n const add = (x1, y1, x2, y2) => {\n if (x1 === x2 && y1 === y2) return;\n segs.push({ x1, y1, x2, y2 });\n };\n const edgeY = below ? succ.top : succ.bottom;\n const vHead = below ? \"down\" : \"up\";\n\n if (kind === \"SS\") {\n const xv = Math.min(pred.x0, succ.x0) - STUB;\n add(pred.x0, pred.y, xv, pred.y);\n add(xv, pred.y, xv, succ.y);\n add(xv, succ.y, succ.x0, succ.y);\n return { segments: segs, head: bold ? null : { x: succ.x0, y: succ.y, dir: \"right\" } };\n }\n if (kind === \"FF\") {\n const xv = Math.max(pred.x1, succ.x1) + STUB;\n add(pred.x1, pred.y, xv, pred.y);\n add(xv, pred.y, xv, succ.y);\n add(xv, succ.y, succ.x1, succ.y);\n return { segments: segs, head: bold ? null : { x: succ.x1, y: succ.y, dir: \"left\" } };\n }\n if (kind === \"SF\") {\n const xa = pred.x0 - STUB;\n if (succ.x1 + STUB <= xa) {\n add(pred.x0, pred.y, xa, pred.y);\n add(xa, pred.y, xa, succ.y);\n add(xa, succ.y, succ.x1, succ.y);\n } else {\n const xb = succ.x1 + STUB;\n add(pred.x0, pred.y, xa, pred.y);\n add(xa, pred.y, xa, boundary);\n add(xa, boundary, xb, boundary);\n add(xb, boundary, xb, succ.y);\n add(xb, succ.y, succ.x1, succ.y);\n }\n return { segments: segs, head: bold ? null : { x: succ.x1, y: succ.y, dir: \"left\" } };\n }\n // Finish to start.\n const drop = succ.milestone ? (succ.x0 + succ.x1) / 2 : bold ? succ.x0 : succ.x0 + 4;\n const endY = bold ? succ.y : edgeY;\n if (drop >= pred.x1) {\n add(pred.x1, pred.y, drop, pred.y);\n add(drop, pred.y, drop, endY);\n } else {\n const xa = pred.x1 + STUB;\n add(pred.x1, pred.y, xa, pred.y);\n add(xa, pred.y, xa, boundary);\n add(xa, boundary, drop, boundary);\n add(drop, boundary, drop, endY);\n }\n return { segments: segs, head: bold ? null : { x: drop, y: endY, dir: vHead } };\n}\n\n/** Today at the server, then the browser's own day once mounted. */\nexport function isoToday() {\n const d = new Date();\n return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, \"0\")}-${String(d.getDate()).padStart(2, \"0\")}`;\n}\n\nexport { dayOf, isoOf };\n"
42
+ },
43
+ {
44
+ "path": "registry/blocks/schedule-gantt/gantt-model.js",
45
+ "type": "registry:component",
46
+ "target": "@components/makersclaw/blocks/schedule-gantt/gantt-model.js",
47
+ "content": "// The chart's arithmetic and words, with no React: the columns of the sheet,\n// how a date is written, the gap between the finish and the target, what a\n// baseline says about a row, and what a drag costs. Pure, so it is tested\n// under node and the chart only draws what it says.\n\nimport { dayOf, linkKind, workdaysBetween } from \"../schedule-engine\";\n\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\"];\nconst DAY_MS = 86400000;\n\n/** \"Oct 23\" */\nexport function monthDay(iso) {\n const d = dayOf(iso);\n if (d == null) return \"\";\n const x = new Date(d * DAY_MS);\n return `${MONTHS[x.getUTCMonth()]} ${x.getUTCDate()}`;\n}\n\n/** \"Fri Oct 23\" */\nexport function dayWords(iso) {\n const d = dayOf(iso);\n if (d == null) return \"\";\n const x = new Date(d * DAY_MS);\n return `${WEEKDAYS[(((d + 3) % 7) + 7) % 7]} ${MONTHS[x.getUTCMonth()]} ${x.getUTCDate()}`;\n}\n\n/** The sheet's columns: label, width in px (the title takes the rest), and which side its text sits. */\nexport const COLUMNS = {\n outline: { label: \"#\", w: 50 },\n title: { label: \"Task\" },\n start: { label: \"Start\", w: 56 },\n due: { label: \"Finish\", w: 56 },\n days: { label: \"Length\", w: 50, right: true },\n done: { label: \"Done\", w: 44, right: true },\n who: { label: \"Owner\", w: 62 },\n waits: { label: \"Waits on\", w: 88 },\n slack: { label: \"Slack\", w: 50, right: true },\n vs: { label: \"vs baseline\", w: 66, right: true },\n};\nexport const DEFAULT_COLUMNS = [\"outline\", \"title\", \"who\", \"days\", \"done\"];\n\n/**\n * The columns to draw: the keys asked for that exist, the title always\n * there (after the outline number), a narrow container only the number and\n * the title.\n */\nexport function columnsFor(keys, { vsLabel = \"vs baseline\", narrow = false, outlineWidth } = {}) {\n const want = (Array.isArray(keys) && keys.length ? keys : DEFAULT_COLUMNS).filter((k) => COLUMNS[k]);\n const list = want.includes(\"title\") ? want : [\"outline\", \"title\", ...want.filter((k) => k !== \"outline\")];\n const out = (narrow ? list.filter((k) => k === \"outline\" || k === \"title\") : list).map((key) => ({ key, ...COLUMNS[key], ...(key === \"vs\" ? { label: vsLabel } : {}), ...(key === \"outline\" && outlineWidth ? { w: outlineWidth } : {}) }));\n return out;\n}\n\n/** The width the fixed columns take, so the sheet knows what is left for the title. */\nexport const fixedWidth = (cols) => cols.reduce((n, c) => n + (c.w ?? 0), 0);\n\n/**\n * The finish against the target: { state: \"late\" | \"early\" | \"on\" | null,\n * n (working days), text }. Null with no finish. \"Finish Nov 3 · 7 wd late\".\n */\nexport function gapOf(finish, target) {\n if (!finish) return { state: null, n: 0, text: \"\" };\n const head = `Finish ${monthDay(finish)}`;\n if (!target) return { state: null, n: 0, text: head };\n const a = dayOf(finish);\n const b = dayOf(target);\n if (a > b) {\n const n = workdaysBetween(target, finish);\n return { state: \"late\", n, text: `${head} · ${n} wd late` };\n }\n if (a < b) {\n const n = workdaysBetween(finish, target);\n return { state: \"early\", n, text: `${head} · ${n} wd to spare` };\n }\n return { state: \"on\", n: 0, text: `${head} · on the target` };\n}\n\n/**\n * A row against its baseline: days later (+) or earlier (-) at the finish,\n * { text, tone: \"late\" | \"early\" | \"same\" | \"new\" | null }. `base` is\n * { start, due } or undefined (the task is new since); a summary with no\n * baseline row says nothing.\n */\nexport function baselineDelta(row, base) {\n if (!row?.dated || row.summary) return { text: row?.summary ? \"\" : \"\", tone: null };\n if (!base) return { text: \"new\", tone: \"new\" };\n const a = dayOf(row.due);\n const b = dayOf(base.due);\n if (a == null || b == null) return { text: \"\", tone: null };\n if (a === b) return { text: \"0\", tone: \"same\" };\n const n = a > b ? workdaysBetween(base.due, row.due) : -workdaysBetween(row.due, base.due);\n return { text: `${n > 0 ? \"+\" : n < 0 ? \"-\" : \"\"}${Math.abs(n)} wd`, tone: n > 0 ? \"late\" : n < 0 ? \"early\" : \"same\" };\n}\n\n/** A row's length words: \"5 wd\", \"3 wd?\" for a guess, \"\" for a phase or milestone, null when undated. */\nexport function lengthWords(row, guessed = false) {\n if (!row || row.summary || row.milestone) return \"\";\n if (row.duration == null) return null;\n return `${row.duration} wd${guessed ? \"?\" : \"\"}`;\n}\n\n/** The slack words: \"0 wd\" on a critical row, \"\" for finished work and phases. */\nexport function slackWords(row) {\n if (!row || row.summary || row.pinned || row.slack == null) return \"\";\n return `${row.slack} wd`;\n}\n\n/** What a task is called to a screen reader. */\nexport function barLabel(task, row) {\n if (!task || !row) return \"\";\n const when = row.start ? `${dayWords(row.start)}${row.due && row.due !== row.start ? ` to ${dayWords(row.due)}` : \"\"}` : \"no dates\";\n const len = row.milestone || row.summary || row.duration == null ? \"\" : `, ${row.duration} working ${row.duration === 1 ? \"day\" : \"days\"}`;\n const state = row.pinned ? \", done\" : row.critical ? \", on the critical path\" : row.slack > 0 ? `, ${row.slack} days of slack` : \"\";\n return `${task.title}: ${when}${len}${state}`;\n}\n\n/**\n * What a drag costs, for the note by the bar and the page's top bar.\n * `before` / `after` are plans (engine `schedule` results), `changes` the\n * engine's shifted dates, `id` the dragged row.\n * → { id, cost (working days the finish moves), followers, startShift,\n * lengthShift, slack, critical, duration, oldDuration, start, due,\n * finishBefore, finishAfter, finishTitle, clamped }\n */\nexport function describeDrag({ before, after, changes, clamped = [], id, titleOf = () => \"\" }) {\n const b = before.rows.get(id);\n const a = after.rows.get(id);\n const cost = before.finish && after.finish ? workdaysBetween(before.finish, after.finish) : 0;\n const followers = (changes ?? []).filter((c) => String(c.id) !== id && !c.summary).length;\n const startShift = b?.start && a?.planStart ? workdaysBetween(b.start, a.planStart) : 0;\n const lengthShift = b?.duration != null && a?.duration != null ? a.duration - b.duration : 0;\n // The row the finish waits on: a milestone that ends on it, else the first task that does.\n let finishTitle = \"\";\n if (after.finish) {\n let hit = null;\n for (const r of after.rows.values()) {\n if (r.summary || !r.dated || r.planDue !== after.finish) continue;\n if (!hit || (r.milestone && !hit.milestone)) hit = r;\n }\n finishTitle = hit ? titleOf(hit.id) : \"\";\n }\n return {\n id,\n cost,\n followers,\n startShift,\n lengthShift,\n slack: b?.slack ?? null,\n critical: Boolean(b?.critical),\n duration: a?.duration,\n oldDuration: b?.duration,\n start: a?.planStart,\n due: a?.planDue,\n finishBefore: before.finish,\n finishAfter: after.finish,\n finishTitle,\n clamped,\n };\n}\n\n/** The lines of the note by a moving bar, in the canvas's words. */\nexport function dragLines(info, task, { nextTitle = null } = {}) {\n const shift = info.lengthShift || info.startShift;\n const signed = shift ? `${shift > 0 ? \"+\" : \"-\"}${Math.abs(shift)}` : \"\";\n const first = info.lengthShift\n ? `Finish ${dayWords(info.due)} · ${info.duration} wd, was ${info.oldDuration}`\n : `${dayWords(info.start)}${info.due !== info.start ? ` to ${dayWords(info.due)}` : \"\"}${shift ? ` · moved ${signed} wd` : \"\"}`;\n const second = info.followers ? `Moves ${info.followers} ${info.followers === 1 ? \"task\" : \"tasks\"} with it` : \"Nothing else moves\";\n const left = info.slack != null ? Math.max(0, info.slack - Math.max(0, info.startShift + info.lengthShift)) : null;\n const slackLine = !info.followers && left != null ? `${left} wd of slack left${nextTitle ? ` before ${nextTitle} needs it` : \"\"}` : null;\n const third =\n info.cost > 0\n ? { text: `${info.finishTitle ? `${info.finishTitle} ` : \"Finish \"}${dayWords(info.finishAfter)} · +${info.cost} wd`, tone: \"late\" }\n : info.cost < 0\n ? { text: `${info.finishTitle ? `${info.finishTitle} ` : \"Finish \"}${dayWords(info.finishAfter)} · ${info.cost} wd`, tone: \"early\" }\n : { text: \"The finish holds\", tone: null };\n return { title: task?.title ?? \"\", first, second, slack: slackLine, third, last: \"Let go to save · Esc puts it back\" };\n}\n\n/** The note by a link being pulled: what it would say, and what it costs. */\nexport function linkLines({ task, predecessor, kind, info, problem }) {\n if (problem) return { text: problem, tone: \"problem\" };\n const k = linkKind(kind);\n const clause = { FS: \"finish to start\", SS: \"start to start\", FF: \"finish to finish\", SF: \"start to finish\" }[k];\n const head = `Let go to link: ${task?.title ?? \"it\"} waits on ${predecessor?.title ?? \"it\"}, ${clause}`;\n const cost = info?.cost ?? 0;\n const tail = cost > 0 ? `Finish moves ${cost} wd later` : info?.followers ? `${info.followers} ${info.followers === 1 ? \"task moves\" : \"tasks move\"}; the finish holds` : `Finish unchanged${predecessor && info?.slack != null ? `: ${predecessor.title} has ${info.slack} wd of slack` : \"\"}`;\n return { text: head, tail, tone: cost > 0 ? \"late\" : null };\n}\n\n/**\n * The moves for a drag of one bar or of every selected bar by `delta`\n * calendar days of the track (`movesOf(id)` makes one bar's move). A row\n * that is finished, undated or a summary of a selected child is left out.\n */\nexport function selectionMoves(ids, movesOf) {\n const out = [];\n const seen = new Set();\n for (const id of ids) {\n for (const m of movesOf(id)) {\n if (seen.has(m.id)) continue;\n seen.add(m.id);\n out.push(m);\n }\n }\n return out;\n}\n\n/** \"Not placed: waits on 5, which waits on 4\" for a row a loop holds. */\nexport function loopWords(row, plan, outlineOf) {\n if (!row?.stuck) return \"\";\n const waits = [];\n for (const l of plan.links.values()) if (l.taskId === row.id && l.valid) waits.push(l);\n const first = waits[0];\n if (!first) return \"Not placed: a loop\";\n const out = outlineOf(first.dependsOnId);\n return out ? `Not placed: waits on ${out}, which is in a loop` : \"Not placed: a loop\";\n}\n"
48
+ },
49
+ {
50
+ "path": "registry/blocks/schedule-gantt/README.md",
51
+ "type": "registry:file",
52
+ "target": "@components/makersclaw/blocks/schedule-gantt/README.md",
53
+ "content": "# ScheduleGantt\n\nA Gantt for dated work with dependencies, where the question is \"when do we\nfinish, and what decides it?\". The task sheet on the left and a working-day\ntime scale on the right are one picture, row for row.\n\n- **Bars** in steel grey with their progress inside; finished work full; a\n phase a bracket over its work (it folds); a milestone a diamond.\n- **Links of all four kinds** (finish to start, start to start, finish to\n finish, start to finish) with lag, drawn as elbows. A link the dates break\n is dashed with a cross. The critical path turns red when the page switches\n it on.\n- **The header says the news**: today, the target (dashed), the finish\n (solid), and the gap between them in working days (\"Finish Nov 3 · 7 wd\n late\").\n- **Drag a bar** to move it (several selected move together), **an end** to\n change its length, **the dot past its end onto another bar** to link them.\n Everything that waits on it moves with it while you drag, the finish slides\n away from the target, and a note by the bar says what it costs. Let go and\n one `onMove` carries every changed date.\n- **Dashed outlines** show where a bar was (`ghosts`) or where a suggestion\n would put it (`pencil`); a thin bar under each shows a baseline.\n- **Lanes under the rows** (who is on what) share the scale.\n- **Type into the sheet**: double-click a name, a length, a percentage or\n what a task waits on (`2.4FS+2d`); the last row adds a task (Enter) or a\n milestone (Shift+Enter, Tab makes it a sub-task).\n\nBorn in a Gantt planner's app, which drew all of it by hand beside its own\nengine. The template's `gantt` block is the neighbour for calendar-day work\nwith finish-to-start links only; this one is for working days, four link\nkinds, lag, slack, a critical path and a baseline.\n\n```jsx\nimport { ScheduleGantt } from \"../components/blocks/schedule-gantt\"; // a Makersclaw app (\"./blocks/schedule-gantt\" from components/)\n// shadcn project: import { ScheduleGantt } from \"@/components/makersclaw/blocks/schedule-gantt\"\n```\n\nA client component; it needs the `schedule-engine` block (added with it). The\nsize is the parent's: put it where a height is set (the app's `AppLayout`\ncontent) and it fills it, the sheet and the bars scrolling together, the time\nscale scrolling inside the chart.\n\n## Rows in\n\n- `tasks`: `{ id, parent_id, title, start_date, due_date, is_milestone,\n progress, status, position, assignee_id, assignee_name, created_at }`\n (dates `'YYYY-MM-DD'`). A parent with children is a phase.\n- `links`: `{ id, task_id, depends_on_id, kind, lag_days }`: `task_id` waits\n on `depends_on_id`.\n- `people`: `Map(id → { name, left? })`; `left: true` writes \"(left)\" after\n the name.\n\n## Props\n\n- `today`, `target`: `'YYYY-MM-DD'`. `showToday` (true).\n- `zoom`: `\"day\" | \"week\" | \"month\" | \"fit\"` (`fit`: the whole span in the\n width). `fitWidth`: the width to fit, default the container's.\n- `columns`: the sheet's columns, in order, from `outline`, `title`,\n `start`, `due`, `days`, `done`, `who`, `waits`, `slack`, `vs`. Default\n `[\"outline\",\"title\",\"who\",\"days\",\"done\"]`. `vsLabel`: the baseline\n column's heading (\"vs Kickoff\"). Under 760px of its own width the sheet\n keeps the number and the title.\n- `gridWidth`: the sheet's width, 470 (the divider drags it);\n `onGridWidth(w)` when it is let go. `rowH`: 30.\n- `height`: `\"fill\"` (default; the parent sets it) or a pixel number.\n- `critical`: draw the critical path in red.\n- `selectedId`, `selectedIds` (a Set: with more than one, dragging one moves\n them all), `onSelect(id | null, { additive })`; `collapsed` (a Set of phase\n ids), `onToggle(id)`; `highlight` (a Set of ids; the rest fade).\n- `ghosts`: `Map(id → { start, due })` where bars were. `pencil`: the same\n for where a suggestion would put them. `baseline`: the same for a saved\n baseline, a thin bar under each. `guesses`: a Set of ids whose length is a\n guess (\"5 wd?\", in blue). `moving`: a Set of ids whose dates are changing.\n- `notes`: `Map(id → { text, tone? })` words after a bar (`tone: \"crit\"` red,\n `\"problem\"` red and bold); on a row with no bar, the words stand where\n the bar would. `tags`: `Map(id → [{ label, kind }])` in the row, `kind`:\n `late`, `pole`, `unsaved`, `crit`, `blue`, `ink`, `quiet`, or\n `agent` (a small filled badge, `title` on hover) and `dot` (a warning dot).\n- `lanes`: `[{ id, label, sub, items: [{ id, title, start, due }], marks:\n [{ from, to, count, title }] }]` under the rows; `lanesTitle`: the heading\n node of that section.\n- `rowMenu(task, { row, plan })` → Menu items for the row's \"…\" (built for\n the hovered or selected row only).\n- `addUnder`: `{ id (a parent) | null, afterId, milestone, nonce }` opens\n the add row under a row; `editRequest`: `{ id, field, nonce }` opens a\n cell; `jump`: `{ date, nonce }` scrolls to a day.\n- `loading` (skeleton rows), `error` (a line above the chart), `emptyText`,\n `label` (a screen reader's name for the chart), `className`.\n\n## Callbacks\n\nEvery write answers `{ ok: true }` or `{ ok: false, error }`; a failure puts\nthe chart back and says so above it. Without a callback that part is\nread-only: no drag, no link, no typing.\n\n- `onMove(moves, preview)`: a finished drag or an Alt+arrow. `moves` is\n `[{ id, start, due }]` (what was asked); `preview.changes` is every date\n the engine shifted, `[{ id, start_date, due_date, from, summary? }]`, so\n the page can show them at once and save them in one write.\n- `onLink({ taskId, dependsOnId, kind }, preview)`: the dot dropped on a bar.\n The kind comes from which ends were joined. A link that would loop is\n refused in words before the drop.\n- `onEdit(task, field, value)`: `title`, `days`, `progress`, `waits` (the\n short form; parse it with the engine's `parseWaits`).\n- `onAdd({ title, days, milestone, parentId, afterId })` → `{ ok, id }`.\n- `onOpen(task)`: Enter or a double-click. `onOpenLink(link)`: a double-click\n on a link line, or Ctrl-click on a waits-on cell.\n- `onPreview(info | null)`: while a drag or a pulled link runs: `{ mode:\n \"move\" | \"stretch\" | \"link\", task, cost (working days the finish moves),\n followers, startShift, lengthShift, slack, critical, duration, start, due,\n finishBefore, finishAfter, finishTitle, clamped }`. The page shows the\n finish counting up in its top bar.\n\n## Keys\n\n↑ ↓ move the selection; Enter opens; F2 renames; Alt+← → moves the task a\nworking day with everything that waits on it; Esc drops a drag or clears the\nselection; in the add row Enter adds a task, Shift+Enter a milestone, Tab\nmakes it a sub-task of the row above.\n\n## Example\n\n```jsx\n<ScheduleGantt\n tasks={tasks}\n links={links}\n people={people}\n today=\"2026-09-24\"\n target=\"2026-10-23\"\n zoom=\"week\"\n columns={[\"outline\", \"title\", \"start\", \"due\", \"days\", \"done\", \"who\", \"waits\"]}\n critical={showCritical}\n selectedId={selectedId}\n onSelect={setSelectedId}\n onOpen={(t) => open(t.id)}\n onMove={(moves, preview) => save(preview.changes)}\n onLink={(link) => addLink(link)}\n onEdit={(task, field, value) => edit(task, field, value)}\n onAdd={(draft) => addTask(draft)}\n onPreview={setLive}\n/>\n```\n\n## States drawn in the demo\n\nA plan with every kind of row; the critical path on; a baseline and its\ncolumn; ghosts and pencil; a loop; a plan with no dates; lanes; the whole\nproject at once; loading; failed; empty; read-only; a narrow (320px)\ncontainer.\n\n## Notes\n\n- Dates are days, never instants, so the server and the browser draw the same\n picture. Weekends are shaded and skipped; there are no holidays.\n- Positions are measured in pixels per day by zoom; a very long plan scrolls\n inside the chart. 300 rows is fine; virtualise past a few thousand.\n- Colour is a signal: red is the critical path or a broken link, amber is\n late or the gap past the target, green the gap in front of it, blue the\n agent (a guess, a suggestion), the accent today and the selection.\n"
54
+ }
55
+ ],
56
+ "categories": [
57
+ "gantt",
58
+ "schedule",
59
+ "project plan",
60
+ "critical path",
61
+ "slack",
62
+ "dependencies",
63
+ "working days",
64
+ "baseline",
65
+ "lag",
66
+ "milestones",
67
+ "drag",
68
+ "slip",
69
+ "ripple",
70
+ "ms project",
71
+ "teamgantt",
72
+ "workload",
73
+ "target",
74
+ "timeline"
75
+ ],
76
+ "docs": "Installed to components/makersclaw/blocks/schedule-gantt. Import: import { ScheduleGantt, TagChip, CellInput, ROW, ZOOMS, axisFor, rangeFor, routeLink, shortDate, isoToday, COLUMNS, DEFAULT_COLUMNS, MONTHS, WEEKDAYS, baselineDelta, barLabel, columnsFor, describeDrag, dayWords, dragLines, fixedWidth, gapOf, lengthWords, linkLines, loopWords, monthDay, selectionMoves, slackWords } from \"@/components/makersclaw/blocks/schedule-gantt\". Props and an example: components/makersclaw/blocks/schedule-gantt/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
77
+ "meta": {
78
+ "makersclaw": {
79
+ "version": "0.8.1",
80
+ "useWhen": "A project plan where the question is 'when do we finish and what decides it': tasks with lengths in working days, links of four kinds with lag, slack, a critical path, a target date, a saved baseline, and dates that must move when something slips. Not for calendar-day work with finish-to-start links only (gantt), phases against gates (schedule-ruler), stage columns (board) or undated to-dos (task-list)."
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,64 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "schedule-ruler",
4
+ "type": "registry:block",
5
+ "title": "Schedule ruler",
6
+ "description": "A dated chain drawn to scale on a day axis (a tick each Monday, a taller one on the 1st): the segments of the chain as bars with their names and status (done, in progress, planned, late, draft), a gate post at the end of a segment built from its exit criteria as pips (met are filled, with a 2/4 count, and the date turns to the warning tone once it has passed with criteria open), milestone diamonds with their names, a solid target line, a dashed forecast line, today as a line in the accent, and between target and forecast the gap hatched: green slack or a red overrun, counted in working days and labelled \"+3 d\" or \"3 d slack\". Earlier positions can show as dashed ghosts. GapRuler is the target against the forecast alone, for a sentence to sit beside; ScheduleStrip is the chain in one small row for a list. Positions are percentages, so it draws the same on the server; under 36rem of its own width the chain scrolls and is also listed in words. Loading, failed and empty states are drawn.",
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/schedule-ruler/index.js",
14
+ "type": "registry:component",
15
+ "target": "@components/makersclaw/blocks/schedule-ruler/index.js",
16
+ "content": "// The ScheduleRuler block — `makersclaw add schedule-ruler`. Fixed: compose\n// it through its props (README.md beside this file).\nexport { ScheduleRuler, GapRuler, ScheduleStrip } from \"./schedule-ruler\";\nexport {\n axisOf,\n dayName,\n dayNumber,\n daysBetween,\n frameOf,\n gapOf,\n gapWords,\n gateOf,\n isoOf,\n layoutLanes,\n placeLabel,\n readMilestones,\n readSegments,\n shortDay,\n summaryOf,\n WORKWEEK,\n} from \"./ruler-model\";\n"
17
+ },
18
+ {
19
+ "path": "registry/blocks/schedule-ruler/schedule-ruler.jsx",
20
+ "type": "registry:component",
21
+ "target": "@components/makersclaw/blocks/schedule-ruler/schedule-ruler.jsx",
22
+ "content": "\"use client\";\n\nimport { Button, Empty, Icon, Notice, Skeleton, cx } from \"../../ui\";\nimport {\n dayName,\n dayNumber,\n gapOf,\n gapWords,\n gateOf,\n axisOf,\n frameOf,\n labelPct,\n layoutLanes,\n placeLabel,\n readMilestones,\n readSegments,\n shortDay,\n summaryOf,\n} from \"./ruler-model\";\n\n// ScheduleRuler: a dated chain drawn to scale, like a surveyor's chain.\n// A baseline of days (a tick each Monday, a taller one on the 1st); the\n// segments of the chain as bars with their names; a gate post at the end of\n// a segment, built from its exit criteria as pips; milestone diamonds; the\n// target as a solid line and the forecast as a dashed one, and between them\n// the gap hatched: green when the forecast lands early (slack), red when it\n// lands late (an overrun), with its size in working days (\"+3 d\"). Today is\n// a line in the accent.\n//\n// GapRuler is the target against the forecast and nothing else, for a\n// sentence to sit beside. ScheduleStrip is the chain small, one row, for a\n// row in a list.\n//\n// Positions are percentages of the chain's width: nothing measures the page,\n// so the server's HTML and the browser agree. The chain is never narrower\n// than `minWidth`; a container narrower than that scrolls it sideways and,\n// under 36rem of its own width, also lists the chain in words below it.\n// Props in, one callback out (`onOpen`); it imports only React and the kit.\n// README.md beside this file lists every prop.\n\nconst AXIS = 30;\nconst MILE_H = 20;\nconst GHOST_H = 14;\nconst GATE_H = 38;\nconst FLAG_H = 22;\n\n// The hatch is drawn in currentColor, so a text-* token paints it.\nconst HATCH = { backgroundImage: \"repeating-linear-gradient(135deg, currentColor 0 1.5px, transparent 1.5px 7px)\" };\nconst HATCH_FINE = { backgroundImage: \"repeating-linear-gradient(135deg, currentColor 0 1.2px, transparent 1.2px 5px)\" };\n\n// A segment's status: done (solid, success) · active (solid ink) · planned\n// (an ink outline) · late (solid accent: past its gate) · draft (dashed,\n// breathing: still being written).\nconst TONE = {\n done: \"bg-success text-background border border-success\",\n active: \"bg-foreground text-background border border-foreground\",\n planned: \"bg-background text-foreground border border-foreground\",\n late: \"bg-primary text-primary-foreground border border-primary\",\n draft: \"border-[1.5px] border-dashed border-muted-foreground bg-background text-muted-foreground motion-safe:animate-pulse\",\n};\nconst STRIP = {\n done: \"bg-success\",\n active: \"bg-foreground\",\n late: \"bg-primary\",\n planned: \"border border-foreground bg-background\",\n draft: \"border border-dashed border-muted-foreground\",\n};\nconst STATUS_WORDS = { done: \"done\", active: \"in progress\", planned: \"planned\", late: \"late\", draft: \"draft\" };\n\nconst GAP_TEXT = { over: \"text-destructive\", slack: \"text-success\", on: \"text-foreground\" };\nconst GAP_LINE = { over: \"border-destructive\", slack: \"border-success\", on: \"border-foreground\" };\n\nconst WORDS = {\n target: \"Target\",\n forecast: \"Forecast\",\n today: \"Today\",\n on: \"On target\",\n over: (n) => `+${n} d`,\n slack: (n) => `${n} d slack`,\n undated: (n) => `${n} without dates, left off`,\n early: \"Earlier plan\",\n};\n\nconst pct = (n) => `${n}%`;\n\n/** The things every form of the ruler reads the same way. */\nfunction readProps({ segments, milestones, target, forecast, today, count, workweek }) {\n const { segs, skipped } = readSegments(segments);\n const miles = readMilestones(milestones);\n const tgt = dayNumber(target);\n const t = dayNumber(today);\n // forecast: undefined → the chain's own last day; null or \"\" → none.\n const derived = forecast === undefined && segs.length ? Math.max(...segs.map((p) => p.e)) : null;\n const fc = forecast === undefined ? derived : dayNumber(forecast);\n const gap = gapOf({ target: tgt, forecast: fc, count, workweek });\n return { segs, skipped, miles, tgt, fc, t, gap };\n}\n\n/**\n * The full chain.\n *\n * Props: see README.md. In short: `segments` [{ id, label, start, end,\n * status, sub, gate }], `milestones` [{ id, date, label, done }], `target`,\n * `forecast`, `today`, `ghost`, `count`, `words`, `compact`, `minWidth`,\n * `onOpen`, `loading`, `error`, `onRetry`, `empty`, `emptyAction`.\n */\nexport function ScheduleRuler({\n segments,\n milestones,\n start,\n target,\n forecast,\n today,\n ghost,\n count = \"working\",\n workweek,\n words: wordsIn,\n compact = false,\n minWidth = 560,\n fitToday = true,\n from,\n to,\n onOpen,\n loading = false,\n error,\n onRetry,\n empty,\n emptyAction,\n label = \"Schedule\",\n className,\n}) {\n const words = { ...WORDS, ...wordsIn };\n\n if (loading) {\n return (\n <div aria-busy=\"true\" aria-label={`${label}, loading`} className={cx(\"@container grid min-w-0 gap-3\", className)}>\n <Skeleton shape=\"rect\" className=\"h-4 w-full\" />\n <Skeleton shape=\"rect\" className=\"h-12 w-full\" />\n <Skeleton shape=\"rect\" className=\"h-4 w-2/3\" />\n </div>\n );\n }\n if (error) {\n return (\n <div className={cx(\"@container min-w-0\", className)}>\n <Notice\n tone=\"danger\"\n title={`${label} did not load`}\n action={onRetry ? <Button size=\"sm\" onClick={onRetry}>Try again</Button> : null}\n >\n {typeof error === \"string\" ? error : \"Something went wrong reading the dates.\"}\n </Notice>\n </div>\n );\n }\n\n const { segs, skipped, miles, tgt, fc, t, gap } = readProps({ segments, milestones, target, forecast, today, count, workweek });\n const startDay = dayNumber(start);\n const ghosts = (ghost ?? [])\n .map((g) => ({ s: dayNumber(g?.start), e: dayNumber(g?.end) }))\n .filter((g) => g.s != null && g.e != null && g.e >= g.s);\n const days = [\n startDay,\n ...segs.flatMap((p) => [p.s, p.e + 1]),\n ...miles.flatMap((m) => [m.day, m.day + 1]),\n tgt != null ? tgt + 1 : null,\n fc != null ? fc + 1 : null,\n ...ghosts.flatMap((g) => [g.s, g.e + 1]),\n fitToday && t != null ? t : null,\n ].filter((d) => d != null);\n const frame = segs.length || miles.length || tgt != null ? frameOf({ days, from: dayNumber(from), to: dayNumber(to) }) : null;\n\n if (!frame) {\n return (\n <div className={cx(\"@container min-w-0\", className)}>\n <Empty\n icon=\"calendar\"\n title={empty ?? \"Nothing is dated yet\"}\n body={skipped ? words.undated(skipped) : null}\n action={emptyAction}\n />\n </div>\n );\n }\n\n const { r0, r1, x } = frame;\n const axis = axisOf(r0, r1);\n const inWindow = (d) => d != null && d >= r0 && d <= r1;\n const tIn = inWindow(t) ? t : null;\n\n // ---- rows, top to bottom, in px\n const mileItems = miles.map((m) => {\n const text = `${m.label} ${shortDay(m.day)}`;\n const w = labelPct(text.length, minWidth, 26);\n const px = x(m.day) + (100 / (r1 - r0)) / 2;\n return { id: m.id, m, px, ...placeLabel({ x: px, width: w, prefer: m.side ?? \"right\" }) };\n });\n const mileLay = layoutLanes(mileItems);\n const mileZone = miles.length ? mileLay.count * MILE_H + 6 : 0;\n const ghostZone = ghosts.length ? GHOST_H + 4 : 0;\n const bandTop = AXIS + mileZone + ghostZone + 8;\n const bandH = compact ? 36 : 48;\n const bandBottom = bandTop + bandH;\n\n const gateItems = compact\n ? []\n : segs\n .map((p) => ({ p, g: gateOf(p.gate, p.e, t) }))\n .filter((r) => r.g)\n .map(({ p, g }) => {\n const gx = x(g.day + 1);\n // The date and the count share a line; the pips (12px each) are the other.\n const chars = Math.max(shortDay(g.day).length + (g.total ? 5 : 0), g.total * 1.7);\n return { id: p.id, p, g, gx, ...placeLabel({ x: gx, width: labelPct(chars, minWidth, 8), prefer: \"left\" }) };\n });\n const gateLay = layoutLanes(gateItems);\n const gateZone = gateItems.length ? 8 + gateLay.count * GATE_H : 0;\n\n const gapTop = bandBottom + gateZone + 4;\n const gapRow = gap ? 20 : 0;\n\n const flags = [];\n if (tgt != null) flags.push({ id: \"target\", day: tgt + 1, text: `${words.target} ${dayName(tgt)}`, kind: \"target\" });\n if (fc != null && gap?.state !== \"on\" && (tgt != null || forecast !== undefined)) flags.push({ id: \"forecast\", day: fc + 1, text: `${words.forecast} ${dayName(fc)}`, kind: \"forecast\" });\n if (tIn != null) flags.push({ id: \"today\", day: tIn, text: `${words.today} ${shortDay(tIn)}`, kind: \"today\" });\n const flagItems = compact\n ? []\n : flags.map((f) => {\n const fx = x(f.day);\n return { ...f, fx, ...placeLabel({ x: fx, width: labelPct(f.text.length, minWidth, 18), prefer: \"right\" }) };\n });\n const flagLay = layoutLanes(flagItems);\n const flagTop = gapTop + gapRow + 6;\n const flagZone = flagItems.length ? flagLay.count * FLAG_H : 0;\n const height = (compact ? bandBottom + 6 + gapRow : flagTop + flagZone) + 4;\n const bottomOf = (lane) => flagTop + lane * FLAG_H + 18;\n // A line runs down to its own flag; with no flag drawn (compact) it stops under the band.\n const lineBottom = (id) => (flagLay.lanes[id] != null ? bottomOf(flagLay.lanes[id]) : bandBottom + 8);\n\n const summary = summaryOf({\n label,\n segs: segs.map((p) => ({ ...p, status: STATUS_WORDS[p.status] ?? p.status, gate: gateOf(p.gate, p.e, t) })),\n milestones: miles,\n target: tgt,\n forecast: fc,\n gap,\n today: t,\n words,\n });\n const open = (kind, item) => (onOpen ? () => onOpen({ kind, id: item.id, item }) : undefined);\n\n return (\n <div role=\"group\" aria-label={label} className={cx(\"@container grid min-w-0 gap-3\", className)}>\n <p className=\"sr-only\">{summary}</p>\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 <div className=\"relative w-full\" style={{ minWidth, height }}>\n {/* the axis */}\n <span aria-hidden=\"true\" className=\"absolute inset-x-0 h-px bg-foreground\" style={{ top: AXIS - 4 }} />\n {axis.weeks.map((d) => (\n <span key={`w${d}`} aria-hidden=\"true\" className=\"absolute h-[5px] w-px bg-foreground\" style={{ left: pct(x(d)), top: AXIS - 9 }} />\n ))}\n {axis.months.map((m) => (\n <span key={`m${m.day}`} aria-hidden=\"true\">\n {m.tick ? <span className=\"absolute h-[17px] w-px bg-foreground\" style={{ left: pct(x(m.day)), top: AXIS - 21 }} /> : null}\n <span className=\"absolute top-0 whitespace-nowrap pl-1.5 font-mono text-xs uppercase tracking-[0.06em] text-muted-foreground\" style={{ left: pct(x(m.day)) }}>\n {m.label}\n </span>\n </span>\n ))}\n\n {/* milestones */}\n {mileItems.map((m) => {\n const lane = mileLay.lanes[m.id];\n const top = AXIS + 4 + lane * MILE_H;\n const Mark = onOpen ? \"button\" : \"span\";\n return (\n <Mark\n key={m.id}\n {...(onOpen ? { type: \"button\", onClick: open(\"milestone\", m.m), \"aria-label\": `${m.m.label}${m.m.done ? \", done\" : \"\"}, ${dayName(m.m.day)}` } : { \"aria-hidden\": \"true\" })}\n className={cx(\"absolute flex h-[18px] items-center gap-1.5 whitespace-nowrap rounded-sm bg-background pr-1 text-left font-mono text-xs outline-none\", onOpen && \"focus-visible:ring-2 focus-visible:ring-ring\")}\n style={m.side === \"right\" ? { left: `calc(${pct(m.px)} - 6px)`, top } : { right: `calc(${pct(100 - m.px)} - 6px)`, top, flexDirection: \"row-reverse\" }}\n >\n <span aria-hidden=\"true\" className={cx(\"block size-[11px] shrink-0 rotate-45 border-[1.5px] border-foreground\", m.m.done ? \"bg-foreground\" : \"bg-background\")} />\n <span className=\"font-sans text-xs font-medium text-foreground\">{m.m.label}</span>\n <span className=\"text-muted-foreground\">{shortDay(m.m.day)}</span>\n </Mark>\n );\n })}\n\n {/* where the chain sat before */}\n {ghosts.map((g, i) => (\n <span\n key={`g${i}`}\n aria-hidden=\"true\"\n title={words.early}\n className=\"absolute border border-dashed border-muted-foreground\"\n style={{ left: pct(x(g.s)), width: `calc(${x(g.e + 1) - x(g.s)}% - 3px)`, top: bandTop - GHOST_H - 3, height: GHOST_H - 4 }}\n />\n ))}\n\n {/* the segments */}\n {segs.map((p, i) => {\n const x0 = x(p.s);\n const x1 = x(p.e + 1);\n const Seg = onOpen ? \"button\" : \"span\";\n const status = STATUS_WORDS[p.status] ?? p.status ?? \"planned\";\n return (\n <Seg\n key={p.id}\n {...(onOpen\n ? { type: \"button\", onClick: open(\"segment\", p), \"aria-label\": `${p.label}, ${status}, ${shortDay(p.s)} to ${shortDay(p.e)}` }\n : { \"aria-hidden\": \"true\" })}\n title={`${p.label}, ${status}, ${shortDay(p.s)} to ${shortDay(p.e)}`}\n className={cx(\n \"absolute flex min-w-[3px] flex-col justify-between overflow-hidden px-2.5 py-2 text-left outline-none\",\n TONE[p.status] ?? TONE.planned,\n onOpen && \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1\",\n )}\n style={{ left: pct(x0), width: `calc(${x1 - x0}% - 3px)`, top: bandTop, height: bandH }}\n >\n <span className=\"truncate text-sm font-semibold leading-tight\">{p.label}</span>\n {compact ? null : <span className=\"truncate font-mono text-xs leading-tight\">{p.sub ?? i + 1}</span>}\n </Seg>\n );\n })}\n\n {/* the gap: slack in green, an overrun in red, hatched */}\n {gap && gap.state !== \"on\" ? (\n <span\n aria-hidden=\"true\"\n className={cx(\"absolute border-2 border-current bg-background/70\", GAP_TEXT[gap.state])}\n style={{ ...HATCH, left: `calc(${x(gap.from)}% + 1px)`, width: `max(3px, calc(${x(gap.to + 1) - x(gap.from)}% - 3px))`, top: bandTop, height: bandH }}\n />\n ) : null}\n {gap ? (\n <span\n aria-hidden=\"true\"\n className={cx(\"absolute whitespace-nowrap font-mono text-xs font-semibold\", GAP_TEXT[gap.state])}\n style={\n gap.state === \"on\"\n ? { left: pct(x(gap.target + 1)), top: gapTop, transform: x(gap.target + 1) > 78 ? \"translateX(-100%)\" : undefined }\n : x(gap.from) > 72\n ? { right: pct(100 - x(gap.to + 1)), top: gapTop }\n : { left: pct(x(gap.from)), top: gapTop }\n }\n >\n {gapWords(gap, words)}\n </span>\n ) : null}\n\n {/* gate posts */}\n {gateItems.map(({ id, p, g, gx, side }) => {\n const lane = gateLay.lanes[id];\n const top = bandBottom + 8 + lane * GATE_H;\n const stateTone = g.state === \"overdue\" ? \"text-destructive\" : \"text-muted-foreground\";\n const Post = onOpen && g.total ? \"button\" : \"span\";\n const said = `${p.label} gate, ${shortDay(g.day)}${g.total ? `, ${g.met} of ${g.total} criteria met` : \"\"}${g.state === \"overdue\" ? \", overdue\" : \"\"}`;\n return (\n <span key={`gate-${id}`}>\n <span aria-hidden=\"true\" className=\"absolute w-px bg-foreground\" style={{ left: `calc(${pct(gx)} - 3.5px)`, top: bandBottom, height: 8 + lane * GATE_H }} />\n <span\n aria-hidden=\"true\"\n className={cx(\"absolute size-[9px] border border-foreground\", g.state === \"met\" ? \"bg-success border-success\" : \"bg-background\")}\n style={{ left: `calc(${pct(gx)} - 8px)`, top: bandBottom - 4 }}\n />\n <Post\n {...(Post === \"button\" ? { type: \"button\", onClick: open(\"gate\", p), \"aria-label\": said } : { \"aria-hidden\": \"true\" })}\n className={cx(\n \"absolute grid gap-1 rounded-sm bg-background px-1 text-left outline-none\",\n Post === \"button\" && \"focus-visible:ring-2 focus-visible:ring-ring\",\n side === \"left\" ? \"justify-items-end\" : \"justify-items-start\",\n )}\n style={side === \"left\" ? { right: `calc(${pct(100 - gx)} + 6px)`, top } : { left: `calc(${pct(gx)} + 3px)`, top }}\n >\n <span className={cx(\"font-mono text-xs leading-none\", stateTone)}>\n {shortDay(g.day)}\n {g.total ? <span className=\"ml-1.5 tabular-nums\">{g.met}/{g.total}</span> : null}\n </span>\n {g.total ? (\n <span className=\"flex items-center gap-[3px]\">\n {g.pips.map((pip, k) => (\n <span\n key={pip.id ?? k}\n title={`${pip.label ?? \"Criterion\"}${pip.met ? \", met\" : \", not met\"}`}\n className={cx(\"block size-[9px] rounded-full border\", pip.met ? \"border-success bg-success\" : \"border-foreground bg-background\")}\n />\n ))}\n </span>\n ) : null}\n </Post>\n </span>\n );\n })}\n\n {/* the target, the forecast, today */}\n {tgt != null ? (\n <span aria-hidden=\"true\" className=\"absolute w-0.5 bg-foreground\" style={{ left: `calc(${pct(x(tgt + 1))} - 2px)`, top: AXIS - 14, height: lineBottom(\"target\") - (AXIS - 14) }} />\n ) : null}\n {fc != null && gap?.state !== \"on\" && flags.some((f) => f.id === \"forecast\") ? (\n <span\n aria-hidden=\"true\"\n className={cx(\"absolute w-0 border-l-2 border-dashed\", gap ? GAP_LINE[gap.state] : \"border-foreground\")}\n style={{ left: `calc(${pct(x(fc + 1))} - 2px)`, top: AXIS - 14, height: lineBottom(\"forecast\") - (AXIS - 14) }}\n />\n ) : null}\n {tIn != null ? (\n <span aria-hidden=\"true\" className=\"absolute w-0.5 bg-primary\" style={{ left: `calc(${pct(x(tIn))} - 1px)`, top: AXIS - 4, height: (compact ? bandBottom + 6 : lineBottom(\"today\")) - (AXIS - 4) }} />\n ) : null}\n {flagItems.map((f) => {\n const lane = flagLay.lanes[f.id];\n const top = flagTop + lane * FLAG_H;\n const place = f.side === \"left\" ? { right: `calc(${pct(100 - f.fx)} + 6px)` } : { left: `calc(${pct(f.fx)} + 6px)` };\n const tone = f.kind === \"forecast\" ? (gap ? GAP_TEXT[gap.state] : \"text-foreground\") : \"text-foreground\";\n return f.kind === \"today\" ? (\n <span\n key={f.id}\n aria-hidden=\"true\"\n className=\"absolute whitespace-nowrap bg-primary px-1.5 py-0.5 font-mono text-xs font-medium text-primary-foreground\"\n style={f.side === \"left\" ? { right: `calc(${pct(100 - f.fx)} - 1px)`, top } : { left: `calc(${pct(f.fx)} - 1px)`, top }}\n >\n {f.text}\n </span>\n ) : (\n <span key={f.id} aria-hidden=\"true\" className={cx(\"absolute flex items-center gap-1 whitespace-nowrap bg-background px-1 font-mono text-xs font-medium\", tone)} style={{ ...place, top }}>\n <Icon name=\"flag\" size={12} className=\"shrink-0\" />\n {f.text}\n </span>\n );\n })}\n </div>\n </div>\n\n {skipped ? <p className=\"text-xs text-muted-foreground\">{words.undated(skipped)}</p> : null}\n\n {/* under 36rem the same chain in words, because a scrolled picture hides its ends */}\n <ol aria-hidden=\"true\" className=\"grid min-w-0 gap-1.5 @xl:hidden\">\n {segs.map((p, i) => {\n const g = gateOf(p.gate, p.e, t);\n return (\n <li key={p.id} className=\"flex min-w-0 items-baseline justify-between gap-3 border-t border-border pt-1.5 text-sm\">\n <span className=\"min-w-0 truncate font-medium\">{i + 1}. {p.label}</span>\n <span className=\"shrink-0 text-right font-mono text-xs tabular-nums text-muted-foreground\">\n {shortDay(p.s)} to {shortDay(p.e)}\n {g?.total ? ` · ${g.met}/${g.total} met` : \"\"}\n </span>\n </li>\n );\n })}\n {miles.map((m) => (\n <li key={m.id} className=\"flex min-w-0 items-baseline justify-between gap-3 border-t border-border pt-1.5 text-sm\">\n <span className=\"min-w-0 truncate font-medium\">{m.label}</span>\n <span className=\"shrink-0 font-mono text-xs tabular-nums text-muted-foreground\">{shortDay(m.day)}{m.done ? \" · done\" : \"\"}</span>\n </li>\n ))}\n {tgt != null || gap ? (\n <li className=\"flex min-w-0 flex-col gap-0.5 border-t border-border pt-1.5 font-mono text-xs tabular-nums\">\n {tgt != null ? <span className=\"text-foreground\">{words.target} {dayName(tgt)}</span> : null}\n {fc != null && gap?.state !== \"on\" ? <span className={gap ? GAP_TEXT[gap.state] : \"text-foreground\"}>{words.forecast} {dayName(fc)}</span> : null}\n {gap ? <span className={cx(\"font-semibold\", GAP_TEXT[gap.state])}>{gapWords(gap, words)}</span> : null}\n </li>\n ) : null}\n </ol>\n </div>\n );\n}\n\n/**\n * The target against the forecast, drawn and nothing else: a dashed line\n * for the target, a solid bar for the forecast, the gap between them\n * hatched (green early, red late) and its size in words underneath. For a\n * sentence to sit beside (\"Public launch lands Tue 3 Nov\").\n */\nexport function GapRuler({ target, forecast, count = \"working\", workweek, words: wordsIn, label, width = 380, className }) {\n const words = { ...WORDS, ...wordsIn };\n const gap = gapOf({ target, forecast, count, workweek });\n if (!gap) return null;\n const lo = Math.min(gap.target, gap.forecast);\n const hi = Math.max(gap.target, gap.forecast);\n const total = Math.max(hi - lo + 6, 14);\n const r0 = lo - Math.floor((total - (hi - lo)) / 2);\n const at = (d) => ((d - r0) / total) * 100;\n const tx = at(gap.target);\n const fx = at(gap.forecast);\n const a = Math.min(tx, fx);\n const b = Math.max(tx, fx);\n const tone = GAP_TEXT[gap.state];\n const earlierIsTarget = gap.target <= gap.forecast;\n const said = label ?? gapWords(gap, words);\n return (\n <div\n role=\"img\"\n aria-label={`${words.target} ${dayName(gap.target)}, ${words.forecast.toLowerCase()} ${dayName(gap.forecast)}: ${said}`}\n className={cx(\"@container relative h-[92px] w-full min-w-0\", className)}\n style={{ maxWidth: width }}\n >\n <span aria-hidden=\"true\" className=\"absolute inset-x-0 top-[46px] h-px bg-foreground\" />\n {gap.state !== \"on\" ? (\n <span aria-hidden=\"true\" className={cx(\"absolute top-[34px] h-6 border-y-2 border-current\", tone)} style={{ ...HATCH, left: pct(a), width: pct(b - a) }} />\n ) : null}\n <span aria-hidden=\"true\" className=\"absolute top-6 h-11 w-0 border-l-[1.5px] border-dashed border-foreground\" style={{ left: pct(tx) }} />\n <span aria-hidden=\"true\" className={cx(\"absolute top-5 h-[52px] w-[3px] -translate-x-px bg-current\", tone)} style={{ left: pct(fx) }} />\n <span aria-hidden=\"true\" className={cx(\"absolute top-0 whitespace-nowrap font-mono text-xs leading-tight text-muted-foreground\", earlierIsTarget ? \"-translate-x-full pr-1.5 text-right\" : \"pl-1.5\")} style={{ left: pct(tx) }}>\n {words.target.toUpperCase()}\n <br />\n {shortDay(gap.target)}\n </span>\n <span aria-hidden=\"true\" className={cx(\"absolute top-0 whitespace-nowrap font-mono text-xs font-semibold leading-tight\", tone, earlierIsTarget ? \"pl-1.5\" : \"-translate-x-full pr-1.5 text-right\")} style={{ left: pct(fx) }}>\n {words.forecast.toUpperCase()}\n <br />\n {shortDay(gap.forecast)}\n </span>\n <span aria-hidden=\"true\" className={cx(\"absolute top-[72px] whitespace-nowrap font-mono text-xs font-semibold\", tone)} style={{ left: pct((a + b) / 2), transform: \"translateX(-50%)\" }}>\n {said}\n </span>\n </div>\n );\n}\n\n/**\n * The chain small: one row of bars with the gap hatched, the target in ink\n * and today in the accent. For a row in a list; put the numbers in words\n * beside it.\n */\nexport function ScheduleStrip({ segments, target, forecast, today, count = \"working\", workweek, height = 16, label = \"Schedule\", className }) {\n const { segs, tgt, fc, t, gap } = readProps({ segments, target, forecast, today, count, workweek });\n const days = [...segs.flatMap((p) => [p.s, p.e + 1]), tgt != null ? tgt + 1 : null, fc != null ? fc + 1 : null].filter((d) => d != null);\n const frame = frameOf({ days, padBefore: 0, padAfter: 1 });\n if (!frame || (!segs.length && tgt == null)) return <div aria-hidden=\"true\" className={cx(\"w-full border-b border-border\", className)} style={{ height: height + 6 }} />;\n const { x, r0, r1 } = frame;\n const summary = segs.map((p) => `${p.label} ${STATUS_WORDS[p.status] ?? p.status ?? \"planned\"}`).join(\", \");\n return (\n <div\n role=\"img\"\n aria-label={`${label}: ${summary || \"no dated items\"}${gap ? `; ${gapWords(gap, { ...WORDS, over: (n) => `${n} days over`, slack: (n) => `${n} days of slack` })}` : \"\"}`}\n className={cx(\"relative w-full min-w-0 border-b border-border\", className)}\n style={{ height: height + 6 }}\n >\n {segs.map((p) => (\n <span key={p.id} aria-hidden=\"true\" className={cx(\"absolute top-0\", STRIP[p.status] ?? STRIP.planned)} style={{ left: pct(x(p.s)), width: `calc(${x(p.e + 1) - x(p.s)}% - 2px)`, height }} />\n ))}\n {gap && gap.state !== \"on\" ? (\n <span aria-hidden=\"true\" className={cx(\"absolute top-0 border border-current bg-background/70\", GAP_TEXT[gap.state])} style={{ ...HATCH_FINE, left: pct(x(gap.from)), width: `max(2px, calc(${x(gap.to + 1) - x(gap.from)}% - 1px))`, height }} />\n ) : null}\n {tgt != null ? <span aria-hidden=\"true\" className=\"absolute -top-1 w-0.5 bg-foreground\" style={{ left: `calc(${pct(x(tgt + 1))} - 1px)`, height: height + 10 }} /> : null}\n {t != null && t >= r0 && t <= r1 ? <span aria-hidden=\"true\" className=\"absolute w-0.5 bg-primary\" style={{ left: `calc(${pct(x(t))} - 1px)`, top: -5, height: height + 11 }} /> : null}\n </div>\n );\n}\n"
23
+ },
24
+ {
25
+ "path": "registry/blocks/schedule-ruler/ruler-model.js",
26
+ "type": "registry:component",
27
+ "target": "@components/makersclaw/blocks/schedule-ruler/ruler-model.js",
28
+ "content": "// The schedule ruler's arithmetic: dates, working days, the gap between a\n// target and a forecast, the window, the axis ticks, and where labels go\n// so none sits on another. Pure: no React, no DOM, no clock. The component\n// (schedule-ruler.jsx) draws what this returns; test/schedule-ruler.test.mjs\n// pins it.\n//\n// A \"day\" here is a whole number: days since 1970-01-01 in UTC, so a date\n// string means the same day on the server and in the browser.\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/** Monday to Friday, as weekdayOf() numbers it (0 is Monday). */\nexport const WORKWEEK = [0, 1, 2, 3, 4];\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 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 const d = new Date(t);\n // 2026-02-31 is not a day: Date rolls it over, and the roll-over shows.\n if (d.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();\nexport const yearOf = (day) => new Date(day * DAY_MS).getUTCFullYear();\n\n/** \"11 Dec\" */\nexport const shortDay = (day) => `${dateOfMonth(day)} ${MONTHS[monthOf(day)]}`;\n/** \"Fri 11 Dec\" */\nexport const dayName = (day) => `${WEEKDAYS[weekdayOf(day)]} ${shortDay(day)}`;\n\n/** Does this day count? `count` is \"working\" (the workweek only) or \"calendar\" (every day). */\nexport function isCounted(day, count = \"working\", workweek = WORKWEEK) {\n return count === \"calendar\" ? true : workweek.includes(weekdayOf(day));\n}\n\n/**\n * Signed days from `from` to `to`, counting the days after `from` up to and\n * including `to`: Mon to Wed is 2 working days, Wed to Mon is -2. The same\n * rule everywhere, so \"+3 d\" means the same on the ruler, in a sentence and\n * in a table.\n */\nexport function daysBetween(from, to, { count = \"working\", workweek = WORKWEEK } = {}) {\n if (from == null || to == null) return null;\n if (to === from) return 0;\n const [a, b, sign] = to > from ? [from + 1, to, 1] : [to + 1, from, -1];\n let n = 0;\n for (let d = a; d <= b; d += 1) if (isCounted(d, count, workweek)) n += 1;\n return sign * n;\n}\n\n/**\n * The gap between a target and a forecast finish.\n * over the forecast is later than the target: the days between are an overrun\n * slack the forecast is earlier: the days between are room to spare\n * on no counted day between them\n * `from` and `to` are the first and last day of the hatched stretch.\n * Null when either date is missing.\n */\nexport function gapOf({ target, forecast, count = \"working\", workweek = WORKWEEK } = {}) {\n const t = typeof target === \"number\" ? target : dayNumber(target);\n const f = typeof forecast === \"number\" ? forecast : dayNumber(forecast);\n if (t == null || f == null) return null;\n const days = Math.abs(daysBetween(t, f, { count, workweek }));\n if (days === 0) return { state: \"on\", days: 0, from: null, to: null, target: t, forecast: f };\n return f > t\n ? { state: \"over\", days, from: t + 1, to: f, target: t, forecast: f }\n : { state: \"slack\", days, from: f + 1, to: t, target: t, forecast: f };\n}\n\n/** \"+3 d\" for an overrun, \"3 d slack\" for room to spare, \"On target\" for none. */\nexport function gapWords(gap, words = {}) {\n if (!gap) return \"\";\n if (gap.state === \"over\") return (words.over ?? ((n) => `+${n} d`))(gap.days);\n if (gap.state === \"slack\") return (words.slack ?? ((n) => `${n} d slack`))(gap.days);\n return words.on ?? \"On target\";\n}\n\n/**\n * The segments that can be drawn: both dates real and the end not before\n * the start. `skipped` counts the rest, so a screen can say so.\n */\nexport function readSegments(segments) {\n const out = [];\n let skipped = 0;\n for (const [i, seg] of (segments ?? []).entries()) {\n const s = dayNumber(seg?.start);\n const e = dayNumber(seg?.end);\n if (s == null || e == null || e < s) {\n skipped += 1;\n continue;\n }\n out.push({ ...seg, id: seg.id ?? `segment-${i}`, s, e });\n }\n return { segs: out, skipped };\n}\n\n/** Milestones with a real date, in date order. */\nexport function readMilestones(milestones) {\n const out = [];\n for (const [i, m] of (milestones ?? []).entries()) {\n const d = dayNumber(m?.date);\n if (d != null) out.push({ ...m, id: m.id ?? `milestone-${i}`, day: d });\n }\n return out.sort((a, b) => a.day - b.day);\n}\n\n/**\n * How a gate stands: how many of its exit criteria are met, and\n * met every criterion met (a gate with criteria)\n * open some are not, and the gate day is today or ahead\n * overdue some are not, and the gate day has passed\n * bare no criteria listed: just a date\n */\nexport function gateOf(gate, endDay, today) {\n if (!gate) return null;\n const day = dayNumber(gate.date) ?? endDay;\n const pips = Array.isArray(gate.pips) ? gate.pips : [];\n const total = pips.length;\n const met = pips.filter((p) => p?.met).length;\n const t = typeof today === \"number\" ? today : dayNumber(today);\n let state = \"bare\";\n if (total) state = met === total ? \"met\" : t != null && day < t ? \"overdue\" : \"open\";\n return { day, pips, met, total, state };\n}\n\n/**\n * The window of days the ruler shows. `days` are day numbers (a segment\n * contributes its start and the day after its end). The window gets a little\n * air either side; `from` and `to` (day numbers, `to` exclusive) override.\n * x(day) is the day's left edge as a percentage of the window.\n */\nexport function frameOf({ days = [], from = null, to = null, padBefore = null, padAfter = null } = {}) {\n const real = days.filter((d) => Number.isFinite(d));\n if (!real.length && from == null && to == null) return null;\n const lo = Math.min(...(real.length ? real : [from ?? to]));\n const hi = Math.max(...(real.length ? real : [to ?? from]));\n const span0 = Math.max(1, hi - lo);\n const air = Math.max(2, Math.round(span0 * 0.025));\n const r0 = from ?? lo - (padBefore ?? air);\n const r1 = Math.max(r0 + 1, to ?? hi + (padAfter ?? air));\n const span = r1 - r0;\n return { r0, r1, span, x: (day) => ((day - r0) / span) * 100 };\n}\n\n/**\n * The axis: a tick each Monday while the window is short enough to read\n * them, a taller one on each month's 1st (each quarter's, past about two\n * years), and the month names. The window's first day names its month\n * unless the next month starts right after it.\n */\nexport function axisOf(r0, r1) {\n const span = r1 - r0;\n const quarters = span > 800;\n const weeks = [];\n const months = [];\n if (span <= 150) for (let d = r0; d < r1; d += 1) if (weekdayOf(d) === 0) weeks.push(d);\n let nextFirst = null;\n for (let d = r0; d < r1; d += 1) {\n if (dateOfMonth(d) === 1 && (!quarters || monthOf(d) % 3 === 0)) {\n nextFirst ??= d;\n months.push({ day: d, label: MONTHS[monthOf(d)], year: monthOf(d) === 0, tick: true });\n }\n }\n const startsOnFirst = dateOfMonth(r0) === 1;\n const crowded = !startsOnFirst && nextFirst != null && ((nextFirst - r0) / span) * 100 < 9;\n if (!startsOnFirst && !crowded && !quarters) months.unshift({ day: r0, label: MONTHS[monthOf(r0)], year: true, tick: false });\n else if (crowded && months.length) months[0] = { ...months[0], year: true };\n if (!months.length) months.push({ day: r0, label: MONTHS[monthOf(r0)], year: true, tick: false });\n return { weeks, months: months.map((m) => ({ ...m, label: m.year ? `${m.label} ${yearOf(m.day)}` : m.label })) };\n}\n\n/** Pixels a character of the small mono face takes (about); a label's width is a guess from its text. */\nexport const CHAR_PX = 7.4;\n\n/** A label's width as a percentage of a chain `minWidth` px wide (the narrowest it is ever drawn). */\nexport const labelPct = (chars, minWidth, extraPx = 12) => ((chars * CHAR_PX + extraPx) / minWidth) * 100;\n\n/**\n * Where a label sits against the line it belongs to. It starts at the line\n * and runs right; when it would run past the window's right edge it ends at\n * the line and runs left instead (and the reverse at the left edge).\n * Returns the side and the percentage interval the label covers.\n */\nexport function placeLabel({ x, width, prefer = \"right\" }) {\n let side = prefer;\n if (side === \"right\" && x + width > 100) side = \"left\";\n if (side === \"left\" && x - width < 0 && x + width <= 100) side = \"right\";\n const x0 = side === \"right\" ? x : x - width;\n return { side, x0, x1: x0 + width };\n}\n\n/**\n * Lanes for labels that would overlap: sorted left to right, each item takes\n * the first lane whose last label ended at least `gap` percent before it\n * starts. `items` are { id, x0, x1 } in percent; returns { lanes: {id: n},\n * count } (count is the number of lanes used, 0 for none).\n */\nexport function layoutLanes(items, gap = 1) {\n const ends = [];\n const lanes = {};\n const sorted = [...items].sort((a, b) => a.x0 - b.x0 || String(a.id).localeCompare(String(b.id)));\n for (const it of sorted) {\n let lane = ends.findIndex((end) => end + gap <= it.x0);\n if (lane === -1) lane = ends.length;\n ends[lane] = it.x1;\n lanes[it.id] = lane;\n }\n return { lanes, count: ends.length };\n}\n\n/** The same line a screen reader hears for the whole ruler, built from what is drawn. */\nexport function summaryOf({ label = \"Schedule\", segs = [], milestones = [], target = null, forecast = null, gap = null, today = null, words = {} }) {\n const parts = segs.map((p) => {\n const g = p.gate;\n const crit = g?.total ? `, gate ${g.met} of ${g.total} met` : \"\";\n return `${p.label}${p.status ? `, ${p.status}` : \"\"}, ${shortDay(p.s)} to ${shortDay(p.e)}${crit}`;\n });\n for (const m of milestones) parts.push(`${m.label}${m.done ? \", done\" : \"\"}, ${shortDay(m.day)}`);\n const head = parts.length ? parts.join(\"; \") : \"nothing dated\";\n const tail = [];\n if (target != null) tail.push(`${words.target ?? \"Target\"} ${dayName(target)}`);\n if (forecast != null && gap?.state !== \"on\") tail.push(`${words.forecast ?? \"Forecast\"} ${dayName(forecast)}`);\n if (gap) tail.push(gap.state === \"on\" ? (words.on ?? \"On target\") : `${gap.days} ${gap.days === 1 ? \"day\" : \"days\"} ${gap.state === \"over\" ? \"over\" : \"of slack\"}`);\n if (today != null) tail.push(`${words.today ?? \"Today\"} ${dayName(today)}`);\n return `${label}: ${head}${tail.length ? `; ${tail.join(\"; \")}` : \"\"}`;\n}\n"
29
+ },
30
+ {
31
+ "path": "registry/blocks/schedule-ruler/README.md",
32
+ "type": "registry:file",
33
+ "target": "@components/makersclaw/blocks/schedule-ruler/README.md",
34
+ "content": "# ScheduleRuler\n\nA dated chain drawn to scale, and how it stands against its promise.\n\nOne picture answers \"when does this finish, and against what?\". A day axis\n(a tick each Monday, a taller one on the 1st). The chain's segments as\nbars. A gate post at the end of a segment, built from its exit criteria as\npips. Milestones as diamonds. The target as a solid line, the forecast as a\ndashed one, today as a line in the accent. Between target and forecast the\ngap, hatched: **green when the forecast lands early (slack), red when it\nlands late (an overrun)**, counted in working days and labelled \"+3 d\" or\n\"3 d slack\".\n\nIt draws a result the app has already worked out. It does not schedule,\ndrag or link anything: for bars you edit, with dependencies, use `gantt`.\n\nBorn in three store apps that each drew it by hand: a phase planner's phase\nruler with gate dates, a Gantt planner's gap ruler beside the sentence, a\nroadmap's forecast against its target.\n\n```jsx\nimport { ScheduleRuler, GapRuler, ScheduleStrip } from \"../components/blocks/schedule-ruler\"; // a Makersclaw app (\"./blocks/schedule-ruler\" from components/)\n// shadcn project: import { ScheduleRuler } from \"@/components/makersclaw/blocks/schedule-ruler\"\n```\n\nA client component. No npm packages, no other block. Dates are `'YYYY-MM-DD'`\nstrings and are days, never times, so the server and the browser draw the\nsame picture. Positions are percentages of the chain; nothing is measured.\n\n## Props\n\n`ScheduleRuler`:\n\n- `segments` — `[{ id, label, start, end, status, sub, gate }]`, in order.\n - `start`, `end`: 'YYYY-MM-DD', both days included. A segment without\n both, or ending before it starts, is left off; a line below the picture\n says how many.\n - `status`: `done` (solid, success) · `active` (solid ink) · `planned`\n (an ink outline, the default) · `late` (solid accent) · `draft` (dashed,\n breathing).\n - `sub`: the small line under the name; default the segment's number.\n - `gate`: `{ date?, pips: [{ id, label, met }] }`. The post stands at\n `date` (default the segment's end). The pips are the exit criteria:\n met is filled green, not met is a ring, with \"2/4\" beside the date.\n A gate whose date has passed with criteria open shows its date in the\n destructive tone; one with every criterion met has a green post. A gate\n with no `pips` is just a dated post.\n- `milestones` — `[{ id, date, label, done, side }]`. `done` fills the\n diamond. `side` (\"right\" default, or \"left\") is where the name goes; it\n flips when there is no room.\n- `target` — the promise, 'YYYY-MM-DD'. Drawn as a solid line at the end of\n that day.\n- `forecast` — where the chain will finish. **Left out, it is the chain's\n last day.** `null` for none. Drawn as a dashed line, in the gap's tone.\n- `today` — 'YYYY-MM-DD' or null for no line.\n- `start` — the chain's start, when it begins before its first segment.\n- `ghost` — `[{ start, end }]`: where the segments sat in an earlier\n version, dashed above the bars.\n- `count` — `\"working\"` (default; Monday to Friday) or `\"calendar\"`: which\n days the gap counts. `workweek` — weekday numbers that count, 0 being\n Monday (default `[0,1,2,3,4]`).\n- `words` — the app's vocabulary: `{ target: \"Deadline\", forecast, today,\n on: \"On target\", over: (n) => \"+\" + n + \" d\", slack: (n) => n + \" d slack\",\n undated: (n) => ..., early }`. Every key is optional.\n- `compact` — no gate posts and no flag words; the picture only.\n- `minWidth` — the least width in px the chain is drawn at (560). A\n narrower container scrolls it sideways.\n- `from`, `to` — 'YYYY-MM-DD' to fix the window (`to` is its last day).\n Default: everything drawn, with a little air.\n- `fitToday` — widen the window to take in today (default true). `false`:\n today is drawn only when it already falls inside.\n- `onOpen({ kind, id, item })` — makes bars, gates and diamonds buttons\n (`kind` is \"segment\", \"gate\" or \"milestone\"; `item` is what you passed,\n with `s`/`e` day numbers added on a segment). Leave it out for a\n read-only picture: nothing is pressable.\n- `loading`, `error`, `onRetry`, `empty`, `emptyAction` — the other states:\n a skeleton; a danger notice with a \"Try again\" button when `onRetry` is\n given (`error` may be a string); and with nothing dated, an empty panel\n titled `empty` (default \"Nothing is dated yet\") with `emptyAction` in it.\n- `label` — what a screen reader hears first (\"Website relaunch\").\n `className` — layout only.\n\n`GapRuler` — the target against the forecast and nothing else, about 380px\nwide, for a sentence to sit beside: `target`, `forecast` (both required),\n`count`, `workweek`, `words`, `label` (replaces the words under the ruler),\n`width` (the most px it takes), `className`.\n\n`ScheduleStrip` — the chain in one small row for a list: `segments`,\n`target`, `forecast`, `today`, `count`, `workweek`, `height` (16), `label`,\n`className`. Put the numbers in words beside it.\n\nAlso exported, pure and without React, for a screen that wants the same\nnumbers in a sentence: `gapOf({ target, forecast, count })` (`{ state:\n\"over\" | \"slack\" | \"on\", days, from, to }`), `gapWords`, `daysBetween`,\n`dayNumber`, `isoOf`, `shortDay`, `dayName`, `gateOf`, `frameOf`, `axisOf`,\n`layoutLanes`, `placeLabel`, `readSegments`, `readMilestones`,\n`summaryOf`, `WORKWEEK`.\n\n## Example\n\n```jsx\n<ScheduleRuler\n label=\"Website relaunch\"\n segments={[\n { id: \"d\", label: \"Discovery\", start: \"2026-09-07\", end: \"2026-09-25\", status: \"done\",\n gate: { pips: [{ id: \"a\", met: true }, { id: \"b\", met: true }] } },\n { id: \"x\", label: \"Design\", start: \"2026-09-28\", end: \"2026-10-23\", status: \"active\", sub: \"1 of 3 met\",\n gate: { pips: [{ id: \"a\", met: true }, { id: \"b\", met: false }, { id: \"c\", met: false }] } },\n ]}\n milestones={[{ id: \"k\", date: \"2026-10-02\", label: \"Kickoff held\", done: true }]}\n target=\"2026-10-16\"\n today=\"2026-10-12\"\n words={{ target: \"Deadline\" }}\n onOpen={({ kind, id }) => router.push(`?${kind}=${id}`)}\n/>\n\n<GapRuler target={project.target} forecast={plan.finish} />\n```\n\n## States drawn\n\n- **Empty**: nothing dated. A panel with what to do (`empty`, `emptyAction`).\n- **One segment**, **many segments**: the window fits what is drawn.\n- **Very long names**: a bar's name is cut to one line (its full text is\n the bar's title and, with `onOpen`, its accessible name); milestones and\n gates that would touch take another row instead of sitting on one\n another.\n- **Slack, overrun, on target**: green hatch and \"3 d slack\"; red hatch and\n \"+3 d\"; no hatch and \"On target\".\n- **Narrow** (a 320px container): the chain keeps its `minWidth` and scrolls\n sideways (focusable, so the keyboard can scroll it); under 36rem of its\n own width the chain is also listed in words below it.\n- **Wide**: the chain stretches; the labels have more room than they need.\n- **Loading**, **failed** (with Try again), **read-only** (no `onOpen`).\n- Reduced motion: only a `draft` bar breathes, and it holds still.\n\n## Notes for the app that places it\n\n- It is a picture. Always put the numbers in words beside it as well (the\n sentence \"Public launch lands Tue 3 Nov, seven working days after the\n target\"). A screen reader hears one summary line of the whole chain, built\n from what is drawn.\n- The block counts the gap in days; it does not know your holidays. If your\n finish date already allows for them, pass `count=\"calendar\"` or a\n `workweek`, or draw the gap words yourself with `words`.\n- `forecast` left out is the chain's last day. To draw a promise with no\n forecast, pass `forecast={null}`.\n- Colour is a signal here and nowhere else: green is early, red is late.\n"
35
+ }
36
+ ],
37
+ "categories": [
38
+ "schedule",
39
+ "timeline",
40
+ "ruler",
41
+ "phases",
42
+ "gates",
43
+ "milestones",
44
+ "target",
45
+ "deadline",
46
+ "forecast",
47
+ "slack",
48
+ "overrun",
49
+ "slip",
50
+ "working days",
51
+ "finish",
52
+ "today line",
53
+ "exit criteria",
54
+ "roadmap",
55
+ "project"
56
+ ],
57
+ "docs": "Installed to components/makersclaw/blocks/schedule-ruler. Import: import { ScheduleRuler, GapRuler, ScheduleStrip, axisOf, dayName, dayNumber, daysBetween, frameOf, gapOf, gapWords, gateOf, isoOf, layoutLanes, placeLabel, readMilestones, readSegments, shortDay, summaryOf, WORKWEEK } from \"@/components/makersclaw/blocks/schedule-ruler\". Props and an example: components/makersclaw/blocks/schedule-ruler/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
58
+ "meta": {
59
+ "makersclaw": {
60
+ "version": "0.8.1",
61
+ "useWhen": "A screen that answers 'when does this finish, and against what promise': a project's phases and gates, a launch plan's finish against its target date, a roadmap row's forecast against its deadline, a delivery chain, a renewal or audit timeline. Draws a result the app has already worked out; it does not schedule. Not for editable bars with dependencies (gantt), days on a month grid (calendar-view), or the age of one thing against a limit (day-ruler)."
62
+ }
63
+ }
64
+ }
@@ -5,7 +5,7 @@
5
5
  "title": "SearchDialog",
6
6
  "description": "One ⌘K search over every kind of record — chats, documents, images, projects, people. Before typing it shows your sections (Last opened, Recent); while typing, ranked matches with the typed text marked, a date, image thumbnails, and kind chips (All, Chats, Images…) to narrow them. Searches your list in the browser, or your server through an async onSearch. Built on the kit's Command; SearchPanel is the same box inline.",
7
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/search-dialog. Import: import { SearchDialog, SearchPanel } from \"@/components/makersclaw/blocks/search-dialog\". Props and an example: components/makersclaw/blocks/search-dialog/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
45
45
  "meta": {
46
46
  "makersclaw": {
47
- "version": "0.7.0",
47
+ "version": "0.8.1",
48
48
  "useWhen": "An app has more than one kind of record and wants one global search box or ⌘K palette to find any of them, or a search page with type filters."
49
49
  }
50
50
  }
@@ -5,7 +5,7 @@
5
5
  "title": "SettingsList",
6
6
  "description": "A card of settings, one row each — icon, label, description, meta lines, a badge, and its control: a Switch, a Select, a Button, a remove button or a menu of actions, with optional details that open under the row. Every change saves on its own through an async callback; the row shows the new value at once, a spinner while it saves, and on failure the old value back with the reason and Try again. variant=\"matrix\" is the notification grid: events by group down the side, channels (email, push, SMS) across, a switch per cell, stacked on a phone.",
7
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": [
@@ -59,7 +59,7 @@
59
59
  "docs": "Installed to components/makersclaw/blocks/settings-list. Import: import { SettingsList } from \"@/components/makersclaw/blocks/settings-list\". Props and an example: components/makersclaw/blocks/settings-list/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
60
60
  "meta": {
61
61
  "makersclaw": {
62
- "version": "0.7.0",
62
+ "version": "0.8.1",
63
63
  "useWhen": "An account or workspace settings screen where each setting applies the moment it is changed: preferences, two-step sign-in methods, signed-in devices, connected integrations, billing profiles, notification preferences per channel. Not for a form of fields saved together with one button (record-form)."
64
64
  }
65
65
  }
@@ -5,7 +5,7 @@
5
5
  "title": "SetupChecklist",
6
6
  "description": "The steps to get set up: which are done, how far along the whole is (a ring, a bar, or \"Step 2 of 4\"), and the button for the next one, sent out through an async onAction that can fail on the step. Four shapes: folding rows in a card with a dismiss menu, numbered step cards, a plain step list with a help panel, and bordered accordion rows that can be locked until the step before is done.",
7
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/setup-checklist. Import: import { SetupChecklist } from \"@/components/makersclaw/blocks/setup-checklist\". Props and an example: components/makersclaw/blocks/setup-checklist/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
45
45
  "meta": {
46
46
  "makersclaw": {
47
- "version": "0.7.0",
47
+ "version": "0.8.1",
48
48
  "useWhen": "A new account, workspace or shop has a few things to do before it is useful (complete the profile, invite the team, connect a tool, launch) and the founder should see what is left. Not for a running job's stages (kit Steps or job-panel), and not for a multi-page form wizard."
49
49
  }
50
50
  }
@@ -5,7 +5,7 @@
5
5
  "title": "ShareDialog",
6
6
  "description": "Who can see a record and how to let more people in: invite by email (comma separated, with an access level), each person with their access in a menu (can view, can edit, remove), sharing switches, and a link with a copy button that turns to a check. A dialog, or the same body inline (SharePanel). Every change is an async callback; a failure shows and the old value comes back.",
7
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": [
@@ -45,7 +45,7 @@
45
45
  "docs": "Installed to components/makersclaw/blocks/share-dialog. Import: import { ShareDialog, SharePanel } from \"@/components/makersclaw/blocks/share-dialog\". Props and an example: components/makersclaw/blocks/share-dialog/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
46
46
  "meta": {
47
47
  "makersclaw": {
48
- "version": "0.7.0",
48
+ "version": "0.8.1",
49
49
  "useWhen": "A record, project, document or workspace can be shared: an \"Invite\" or \"Share\" button, a members list with roles, a public link to copy."
50
50
  }
51
51
  }