makersclaw 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (475) hide show
  1. package/THIRD-PARTY-NOTICES.md +180 -0
  2. package/package.json +3 -2
  3. package/r/annotate.json +62 -0
  4. package/r/approval-request.json +52 -0
  5. package/r/audio-player.json +74 -0
  6. package/r/auth-form.json +55 -0
  7. package/r/board.json +10 -6
  8. package/r/calendar-view.json +105 -0
  9. package/r/cart.json +62 -0
  10. package/r/chart.json +83 -0
  11. package/r/chat.json +95 -0
  12. package/r/code-editor.json +94 -0
  13. package/r/comment-box.json +64 -0
  14. package/r/contact-section.json +53 -0
  15. package/r/core.json +205 -8
  16. package/r/csv-import.json +63 -0
  17. package/r/data-table.json +69 -0
  18. package/r/diagram.json +74 -0
  19. package/r/doc-editor.json +82 -0
  20. package/r/doc-page.json +61 -0
  21. package/r/email-render.json +73 -0
  22. package/r/faq.json +50 -0
  23. package/r/file-upload.json +54 -0
  24. package/r/flow-canvas.json +93 -0
  25. package/r/footer.json +52 -0
  26. package/r/gantt.json +70 -0
  27. package/r/job-panel.json +12 -7
  28. package/r/json-editor.json +66 -0
  29. package/r/map.json +89 -0
  30. package/r/menubar.json +52 -0
  31. package/r/mind-map.json +67 -0
  32. package/r/navigation-menu.json +52 -0
  33. package/r/order-detail.json +54 -0
  34. package/r/org-chart.json +66 -0
  35. package/r/page-builder.json +105 -0
  36. package/r/payment-methods.json +51 -0
  37. package/r/pdf-document.json +79 -0
  38. package/r/pdf-viewer.json +88 -0
  39. package/r/product-detail.json +76 -0
  40. package/r/product-grid.json +65 -0
  41. package/r/record-form.json +67 -0
  42. package/r/record-notes.json +55 -0
  43. package/r/registry.json +3411 -29
  44. package/r/search-dialog.json +51 -0
  45. package/r/settings-list.json +66 -0
  46. package/r/setup-checklist.json +51 -0
  47. package/r/share-dialog.json +52 -0
  48. package/r/sidebar.json +69 -0
  49. package/r/signature-pad.json +72 -0
  50. package/r/sketch-canvas.json +99 -0
  51. package/r/spreadsheet.json +90 -0
  52. package/r/stat-cards.json +66 -0
  53. package/r/task-list.json +63 -0
  54. package/r/task-model.json +98 -0
  55. package/r/testimonials.json +52 -0
  56. package/r/transaction-list.json +53 -0
  57. package/r/whiteboard.json +66 -0
  58. package/registry/blocks/annotate/README.md +179 -0
  59. package/registry/blocks/annotate/annotate-data.js +250 -0
  60. package/registry/blocks/annotate/annotate.jsx +646 -0
  61. package/registry/blocks/annotate/block.json +10 -0
  62. package/registry/blocks/annotate/demo.jsx +135 -0
  63. package/registry/blocks/annotate/index.js +15 -0
  64. package/registry/blocks/approval-request/README.md +116 -0
  65. package/registry/blocks/approval-request/approval-request.jsx +386 -0
  66. package/registry/blocks/approval-request/block.json +10 -0
  67. package/registry/blocks/approval-request/demo.jsx +127 -0
  68. package/registry/blocks/approval-request/index.js +4 -0
  69. package/registry/blocks/audio-player/README.md +167 -0
  70. package/registry/blocks/audio-player/audio-player.jsx +623 -0
  71. package/registry/blocks/audio-player/block.json +14 -0
  72. package/registry/blocks/audio-player/colors.js +109 -0
  73. package/registry/blocks/audio-player/demo.jsx +209 -0
  74. package/registry/blocks/audio-player/index.js +6 -0
  75. package/registry/blocks/audio-player/time.js +67 -0
  76. package/registry/blocks/audio-player/wavesurfer.js +9 -0
  77. package/registry/blocks/auth-form/README.md +126 -0
  78. package/registry/blocks/auth-form/auth-form.jsx +568 -0
  79. package/registry/blocks/auth-form/block.json +10 -0
  80. package/registry/blocks/auth-form/demo.jsx +131 -0
  81. package/registry/blocks/auth-form/index.js +3 -0
  82. package/registry/blocks/board/README.md +55 -5
  83. package/registry/blocks/board/block.json +2 -2
  84. package/registry/blocks/board/board.jsx +230 -86
  85. package/registry/blocks/board/demo.jsx +103 -24
  86. package/registry/blocks/calendar-view/README.md +180 -0
  87. package/registry/blocks/calendar-view/agenda.jsx +147 -0
  88. package/registry/blocks/calendar-view/block.json +76 -0
  89. package/registry/blocks/calendar-view/calendar-model.js +442 -0
  90. package/registry/blocks/calendar-view/calendar.jsx +627 -0
  91. package/registry/blocks/calendar-view/demo.jsx +195 -0
  92. package/registry/blocks/calendar-view/draft.jsx +49 -0
  93. package/registry/blocks/calendar-view/index.js +39 -0
  94. package/registry/blocks/calendar-view/month.jsx +346 -0
  95. package/registry/blocks/calendar-view/paint.js +69 -0
  96. package/registry/blocks/calendar-view/time-grid.jsx +527 -0
  97. package/registry/blocks/calendar-view/tray.jsx +87 -0
  98. package/registry/blocks/cart/README.md +146 -0
  99. package/registry/blocks/cart/block.json +10 -0
  100. package/registry/blocks/cart/cart.jsx +597 -0
  101. package/registry/blocks/cart/demo.jsx +98 -0
  102. package/registry/blocks/cart/index.js +4 -0
  103. package/registry/blocks/cart/money.js +28 -0
  104. package/registry/blocks/chart/README.md +316 -0
  105. package/registry/blocks/chart/block.json +10 -0
  106. package/registry/blocks/chart/cartesian.jsx +642 -0
  107. package/registry/blocks/chart/core.jsx +262 -0
  108. package/registry/blocks/chart/demo.jsx +357 -0
  109. package/registry/blocks/chart/heatmap.jsx +336 -0
  110. package/registry/blocks/chart/index.js +8 -0
  111. package/registry/blocks/chart/pie.jsx +245 -0
  112. package/registry/blocks/chart/sparkline.jsx +88 -0
  113. package/registry/blocks/chat/README.md +273 -0
  114. package/registry/blocks/chat/attachment.jsx +95 -0
  115. package/registry/blocks/chat/block.json +10 -0
  116. package/registry/blocks/chat/chat.jsx +132 -0
  117. package/registry/blocks/chat/composer.jsx +364 -0
  118. package/registry/blocks/chat/demo.jsx +578 -0
  119. package/registry/blocks/chat/header.jsx +34 -0
  120. package/registry/blocks/chat/index.js +9 -0
  121. package/registry/blocks/chat/message.jsx +450 -0
  122. package/registry/blocks/chat/scroller.jsx +247 -0
  123. package/registry/blocks/chat/text.jsx +104 -0
  124. package/registry/blocks/code-editor/README.md +210 -0
  125. package/registry/blocks/code-editor/block.json +22 -0
  126. package/registry/blocks/code-editor/code-editor.jsx +534 -0
  127. package/registry/blocks/code-editor/codemirror.js +38 -0
  128. package/registry/blocks/code-editor/demo.jsx +205 -0
  129. package/registry/blocks/code-editor/index.js +8 -0
  130. package/registry/blocks/code-editor/json.js +317 -0
  131. package/registry/blocks/code-editor/sql.js +136 -0
  132. package/registry/blocks/code-editor/text.js +101 -0
  133. package/registry/blocks/code-editor/theme.js +186 -0
  134. package/registry/blocks/comment-box/README.md +123 -0
  135. package/registry/blocks/comment-box/block.json +10 -0
  136. package/registry/blocks/comment-box/comment-body.jsx +217 -0
  137. package/registry/blocks/comment-box/comment-box.jsx +198 -0
  138. package/registry/blocks/comment-box/comment-time.jsx +77 -0
  139. package/registry/blocks/comment-box/demo.jsx +141 -0
  140. package/registry/blocks/comment-box/index.js +8 -0
  141. package/registry/blocks/contact-section/README.md +152 -0
  142. package/registry/blocks/contact-section/block.json +11 -0
  143. package/registry/blocks/contact-section/contact-section.jsx +509 -0
  144. package/registry/blocks/contact-section/demo.jsx +93 -0
  145. package/registry/blocks/contact-section/index.js +4 -0
  146. package/registry/blocks/csv-import/README.md +133 -0
  147. package/registry/blocks/csv-import/block.json +13 -0
  148. package/registry/blocks/csv-import/csv-import.jsx +479 -0
  149. package/registry/blocks/csv-import/demo.jsx +106 -0
  150. package/registry/blocks/csv-import/import.js +200 -0
  151. package/registry/blocks/csv-import/index.js +7 -0
  152. package/registry/blocks/data-table/README.md +386 -0
  153. package/registry/blocks/data-table/block.json +10 -0
  154. package/registry/blocks/data-table/data-table.jsx +1164 -0
  155. package/registry/blocks/data-table/demo.jsx +588 -0
  156. package/registry/blocks/data-table/index.js +5 -0
  157. package/registry/blocks/data-table/model.js +169 -0
  158. package/registry/blocks/diagram/README.md +108 -0
  159. package/registry/blocks/diagram/block.json +13 -0
  160. package/registry/blocks/diagram/demo.jsx +113 -0
  161. package/registry/blocks/diagram/diagram.jsx +481 -0
  162. package/registry/blocks/diagram/index.js +7 -0
  163. package/registry/blocks/diagram/mermaid.js +52 -0
  164. package/registry/blocks/diagram/palette.js +313 -0
  165. package/registry/blocks/diagram/source.js +98 -0
  166. package/registry/blocks/doc-editor/README.md +215 -0
  167. package/registry/blocks/doc-editor/block.json +15 -0
  168. package/registry/blocks/doc-editor/demo.jsx +225 -0
  169. package/registry/blocks/doc-editor/doc-editor.jsx +291 -0
  170. package/registry/blocks/doc-editor/doc.js +309 -0
  171. package/registry/blocks/doc-editor/index.js +7 -0
  172. package/registry/blocks/doc-editor/schema.jsx +56 -0
  173. package/registry/blocks/doc-editor/shadcn-classes.js +110 -0
  174. package/registry/blocks/doc-editor/theme.js +82 -0
  175. package/registry/blocks/doc-page/README.md +119 -0
  176. package/registry/blocks/doc-page/autosave.js +104 -0
  177. package/registry/blocks/doc-page/block.json +10 -0
  178. package/registry/blocks/doc-page/demo.jsx +137 -0
  179. package/registry/blocks/doc-page/doc-page.jsx +276 -0
  180. package/registry/blocks/doc-page/index.js +4 -0
  181. package/registry/blocks/email-render/README.md +151 -0
  182. package/registry/blocks/email-render/block.json +14 -0
  183. package/registry/blocks/email-render/demo.jsx +153 -0
  184. package/registry/blocks/email-render/doc-email.jsx +210 -0
  185. package/registry/blocks/email-render/email.jsx +221 -0
  186. package/registry/blocks/email-render/index.js +10 -0
  187. package/registry/blocks/email-render/render.js +50 -0
  188. package/registry/blocks/email-render/theme.js +215 -0
  189. package/registry/blocks/faq/README.md +101 -0
  190. package/registry/blocks/faq/block.json +11 -0
  191. package/registry/blocks/faq/demo.jsx +89 -0
  192. package/registry/blocks/faq/faq.jsx +220 -0
  193. package/registry/blocks/faq/index.js +4 -0
  194. package/registry/blocks/file-upload/README.md +138 -0
  195. package/registry/blocks/file-upload/block.json +10 -0
  196. package/registry/blocks/file-upload/demo.jsx +204 -0
  197. package/registry/blocks/file-upload/file-upload.jsx +635 -0
  198. package/registry/blocks/file-upload/index.js +4 -0
  199. package/registry/blocks/flow-canvas/README.md +420 -0
  200. package/registry/blocks/flow-canvas/block.json +13 -0
  201. package/registry/blocks/flow-canvas/board.js +819 -0
  202. package/registry/blocks/flow-canvas/chrome.jsx +649 -0
  203. package/registry/blocks/flow-canvas/context.jsx +35 -0
  204. package/registry/blocks/flow-canvas/demo.jsx +249 -0
  205. package/registry/blocks/flow-canvas/edges.jsx +107 -0
  206. package/registry/blocks/flow-canvas/flow-canvas.jsx +1329 -0
  207. package/registry/blocks/flow-canvas/index.js +41 -0
  208. package/registry/blocks/flow-canvas/nodes.jsx +392 -0
  209. package/registry/blocks/flow-canvas/theme.js +174 -0
  210. package/registry/blocks/footer/README.md +133 -0
  211. package/registry/blocks/footer/block.json +11 -0
  212. package/registry/blocks/footer/demo.jsx +119 -0
  213. package/registry/blocks/footer/footer.jsx +406 -0
  214. package/registry/blocks/footer/index.js +4 -0
  215. package/registry/blocks/gantt/README.md +220 -0
  216. package/registry/blocks/gantt/block.json +10 -0
  217. package/registry/blocks/gantt/demo.jsx +148 -0
  218. package/registry/blocks/gantt/gantt.jsx +923 -0
  219. package/registry/blocks/gantt/index.js +7 -0
  220. package/registry/blocks/gantt/rows.jsx +468 -0
  221. package/registry/blocks/gantt/schedule.js +278 -0
  222. package/registry/blocks/job-panel/README.md +80 -0
  223. package/registry/blocks/job-panel/block.json +3 -3
  224. package/registry/blocks/job-panel/demo.jsx +230 -3
  225. package/registry/blocks/job-panel/job-panel.jsx +169 -3
  226. package/registry/blocks/json-editor/README.md +133 -0
  227. package/registry/blocks/json-editor/block.json +10 -0
  228. package/registry/blocks/json-editor/demo.jsx +92 -0
  229. package/registry/blocks/json-editor/index.js +7 -0
  230. package/registry/blocks/json-editor/json-editor.jsx +305 -0
  231. package/registry/blocks/json-editor/json-tree.jsx +393 -0
  232. package/registry/blocks/json-editor/tree.js +225 -0
  233. package/registry/blocks/map/README.md +221 -0
  234. package/registry/blocks/map/block.json +14 -0
  235. package/registry/blocks/map/demo.jsx +201 -0
  236. package/registry/blocks/map/geo.js +198 -0
  237. package/registry/blocks/map/index.js +8 -0
  238. package/registry/blocks/map/list.jsx +128 -0
  239. package/registry/blocks/map/map-view.jsx +715 -0
  240. package/registry/blocks/map/maplibre.js +30 -0
  241. package/registry/blocks/map/marks.jsx +146 -0
  242. package/registry/blocks/map/theme.js +142 -0
  243. package/registry/blocks/menubar/README.md +109 -0
  244. package/registry/blocks/menubar/block.json +10 -0
  245. package/registry/blocks/menubar/demo.jsx +128 -0
  246. package/registry/blocks/menubar/index.js +4 -0
  247. package/registry/blocks/menubar/menubar.jsx +199 -0
  248. package/registry/blocks/mind-map/README.md +206 -0
  249. package/registry/blocks/mind-map/block.json +10 -0
  250. package/registry/blocks/mind-map/demo.jsx +103 -0
  251. package/registry/blocks/mind-map/index.js +31 -0
  252. package/registry/blocks/mind-map/mind-map.jsx +1125 -0
  253. package/registry/blocks/mind-map/topic.jsx +143 -0
  254. package/registry/blocks/mind-map/tree.js +645 -0
  255. package/registry/blocks/navigation-menu/README.md +124 -0
  256. package/registry/blocks/navigation-menu/block.json +10 -0
  257. package/registry/blocks/navigation-menu/demo.jsx +84 -0
  258. package/registry/blocks/navigation-menu/index.js +4 -0
  259. package/registry/blocks/navigation-menu/navigation-menu.jsx +448 -0
  260. package/registry/blocks/order-detail/README.md +109 -0
  261. package/registry/blocks/order-detail/block.json +10 -0
  262. package/registry/blocks/order-detail/demo.jsx +88 -0
  263. package/registry/blocks/order-detail/index.js +4 -0
  264. package/registry/blocks/order-detail/order-detail.jsx +354 -0
  265. package/registry/blocks/org-chart/README.md +145 -0
  266. package/registry/blocks/org-chart/block.json +10 -0
  267. package/registry/blocks/org-chart/demo.jsx +94 -0
  268. package/registry/blocks/org-chart/index.js +7 -0
  269. package/registry/blocks/org-chart/org-chart.jsx +631 -0
  270. package/registry/blocks/org-chart/org.js +245 -0
  271. package/registry/blocks/org-chart/person.jsx +96 -0
  272. package/registry/blocks/page-builder/README.md +257 -0
  273. package/registry/blocks/page-builder/block.json +14 -0
  274. package/registry/blocks/page-builder/demo.jsx +145 -0
  275. package/registry/blocks/page-builder/grapes.js +18 -0
  276. package/registry/blocks/page-builder/index.js +11 -0
  277. package/registry/blocks/page-builder/page-builder.jsx +704 -0
  278. package/registry/blocks/page-builder/page-view.jsx +33 -0
  279. package/registry/blocks/page-builder/page.js +118 -0
  280. package/registry/blocks/page-builder/panels.jsx +588 -0
  281. package/registry/blocks/page-builder/sections.js +209 -0
  282. package/registry/blocks/page-builder/theme.js +87 -0
  283. package/registry/blocks/page-builder/thumbs.jsx +189 -0
  284. package/registry/blocks/page-builder/tokens.js +329 -0
  285. package/registry/blocks/payment-methods/README.md +104 -0
  286. package/registry/blocks/payment-methods/block.json +10 -0
  287. package/registry/blocks/payment-methods/demo.jsx +71 -0
  288. package/registry/blocks/payment-methods/index.js +4 -0
  289. package/registry/blocks/payment-methods/payment-methods.jsx +302 -0
  290. package/registry/blocks/pdf-document/README.md +213 -0
  291. package/registry/blocks/pdf-document/block.json +13 -0
  292. package/registry/blocks/pdf-document/demo.jsx +136 -0
  293. package/registry/blocks/pdf-document/index.js +33 -0
  294. package/registry/blocks/pdf-document/money.js +83 -0
  295. package/registry/blocks/pdf-document/pdf-document.jsx +144 -0
  296. package/registry/blocks/pdf-document/primitives.jsx +271 -0
  297. package/registry/blocks/pdf-document/quote.jsx +179 -0
  298. package/registry/blocks/pdf-document/theme.js +193 -0
  299. package/registry/blocks/pdf-viewer/README.md +191 -0
  300. package/registry/blocks/pdf-viewer/block.json +14 -0
  301. package/registry/blocks/pdf-viewer/demo.jsx +252 -0
  302. package/registry/blocks/pdf-viewer/helpers.js +270 -0
  303. package/registry/blocks/pdf-viewer/index.js +8 -0
  304. package/registry/blocks/pdf-viewer/notes.jsx +195 -0
  305. package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1368 -0
  306. package/registry/blocks/pdf-viewer/pdfjs.js +37 -0
  307. package/registry/blocks/pdf-viewer/ssr-shim-end.js +6 -0
  308. package/registry/blocks/pdf-viewer/ssr-shim.js +44 -0
  309. package/registry/blocks/product-detail/README.md +217 -0
  310. package/registry/blocks/product-detail/block.json +10 -0
  311. package/registry/blocks/product-detail/demo.jsx +233 -0
  312. package/registry/blocks/product-detail/index.js +7 -0
  313. package/registry/blocks/product-detail/price.jsx +115 -0
  314. package/registry/blocks/product-detail/product-detail.jsx +348 -0
  315. package/registry/blocks/product-detail/product-media.jsx +102 -0
  316. package/registry/blocks/product-detail/product-options.jsx +144 -0
  317. package/registry/blocks/product-grid/README.md +162 -0
  318. package/registry/blocks/product-grid/block.json +10 -0
  319. package/registry/blocks/product-grid/demo.jsx +255 -0
  320. package/registry/blocks/product-grid/index.js +5 -0
  321. package/registry/blocks/product-grid/product-card.jsx +232 -0
  322. package/registry/blocks/product-grid/product-grid.jsx +235 -0
  323. package/registry/blocks/record-form/README.md +196 -0
  324. package/registry/blocks/record-form/block.json +10 -0
  325. package/registry/blocks/record-form/demo.jsx +525 -0
  326. package/registry/blocks/record-form/index.js +4 -0
  327. package/registry/blocks/record-form/record-fields.jsx +457 -0
  328. package/registry/blocks/record-form/record-form.jsx +415 -0
  329. package/registry/blocks/record-form/rules.js +81 -0
  330. package/registry/blocks/record-notes/README.md +139 -0
  331. package/registry/blocks/record-notes/block.json +10 -0
  332. package/registry/blocks/record-notes/demo.jsx +179 -0
  333. package/registry/blocks/record-notes/index.js +4 -0
  334. package/registry/blocks/record-notes/record-notes.jsx +266 -0
  335. package/registry/blocks/search-dialog/README.md +108 -0
  336. package/registry/blocks/search-dialog/block.json +10 -0
  337. package/registry/blocks/search-dialog/demo.jsx +116 -0
  338. package/registry/blocks/search-dialog/index.js +3 -0
  339. package/registry/blocks/search-dialog/search-dialog.jsx +235 -0
  340. package/registry/blocks/settings-list/README.md +171 -0
  341. package/registry/blocks/settings-list/block.json +10 -0
  342. package/registry/blocks/settings-list/demo.jsx +333 -0
  343. package/registry/blocks/settings-list/index.js +4 -0
  344. package/registry/blocks/settings-list/row-state.jsx +93 -0
  345. package/registry/blocks/settings-list/settings-list.jsx +349 -0
  346. package/registry/blocks/settings-list/settings-matrix.jsx +118 -0
  347. package/registry/blocks/setup-checklist/README.md +142 -0
  348. package/registry/blocks/setup-checklist/block.json +10 -0
  349. package/registry/blocks/setup-checklist/demo.jsx +159 -0
  350. package/registry/blocks/setup-checklist/index.js +4 -0
  351. package/registry/blocks/setup-checklist/setup-checklist.jsx +446 -0
  352. package/registry/blocks/share-dialog/README.md +113 -0
  353. package/registry/blocks/share-dialog/block.json +10 -0
  354. package/registry/blocks/share-dialog/demo.jsx +140 -0
  355. package/registry/blocks/share-dialog/index.js +3 -0
  356. package/registry/blocks/share-dialog/share-dialog.jsx +384 -0
  357. package/registry/blocks/sidebar/README.md +190 -0
  358. package/registry/blocks/sidebar/block.json +43 -0
  359. package/registry/blocks/sidebar/demo.jsx +577 -0
  360. package/registry/blocks/sidebar/index.js +4 -0
  361. package/registry/blocks/sidebar/sidebar-panel.jsx +149 -0
  362. package/registry/blocks/sidebar/sidebar.jsx +864 -0
  363. package/registry/blocks/signature-pad/README.md +137 -0
  364. package/registry/blocks/signature-pad/block.json +13 -0
  365. package/registry/blocks/signature-pad/colors.js +115 -0
  366. package/registry/blocks/signature-pad/demo.jsx +94 -0
  367. package/registry/blocks/signature-pad/index.js +6 -0
  368. package/registry/blocks/signature-pad/pad.js +7 -0
  369. package/registry/blocks/signature-pad/signature-pad.jsx +434 -0
  370. package/registry/blocks/signature-pad/signature.js +68 -0
  371. package/registry/blocks/sketch-canvas/README.md +280 -0
  372. package/registry/blocks/sketch-canvas/block.json +13 -0
  373. package/registry/blocks/sketch-canvas/demo.jsx +159 -0
  374. package/registry/blocks/sketch-canvas/excalidraw.js +40 -0
  375. package/registry/blocks/sketch-canvas/index.js +9 -0
  376. package/registry/blocks/sketch-canvas/palette.js +226 -0
  377. package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1188 -0
  378. package/registry/blocks/sketch-canvas/sketch.js +179 -0
  379. package/registry/blocks/sketch-canvas/ssr-shim-end.js +6 -0
  380. package/registry/blocks/sketch-canvas/ssr-shim.js +45 -0
  381. package/registry/blocks/sketch-canvas/stamps.js +104 -0
  382. package/registry/blocks/sketch-canvas/theme.js +131 -0
  383. package/registry/blocks/spreadsheet/README.md +307 -0
  384. package/registry/blocks/spreadsheet/block.json +13 -0
  385. package/registry/blocks/spreadsheet/cells.jsx +455 -0
  386. package/registry/blocks/spreadsheet/chrome.jsx +325 -0
  387. package/registry/blocks/spreadsheet/demo.jsx +177 -0
  388. package/registry/blocks/spreadsheet/grid.js +387 -0
  389. package/registry/blocks/spreadsheet/index.js +34 -0
  390. package/registry/blocks/spreadsheet/spreadsheet.jsx +1732 -0
  391. package/registry/blocks/spreadsheet/types.js +473 -0
  392. package/registry/blocks/stat-cards/README.md +173 -0
  393. package/registry/blocks/stat-cards/block.json +10 -0
  394. package/registry/blocks/stat-cards/demo.jsx +257 -0
  395. package/registry/blocks/stat-cards/index.js +4 -0
  396. package/registry/blocks/stat-cards/stat-cards.jsx +307 -0
  397. package/registry/blocks/stat-cards/usage.jsx +243 -0
  398. package/registry/blocks/task-list/README.md +176 -0
  399. package/registry/blocks/task-list/block.json +10 -0
  400. package/registry/blocks/task-list/demo.jsx +134 -0
  401. package/registry/blocks/task-list/index.js +5 -0
  402. package/registry/blocks/task-list/task-list.jsx +907 -0
  403. package/registry/blocks/task-model/README.md +455 -0
  404. package/registry/blocks/task-model/block.json +167 -0
  405. package/registry/blocks/task-model/changes.js +89 -0
  406. package/registry/blocks/task-model/dates.js +371 -0
  407. package/registry/blocks/task-model/demo.jsx +215 -0
  408. package/registry/blocks/task-model/dnd.jsx +240 -0
  409. package/registry/blocks/task-model/index.js +97 -0
  410. package/registry/blocks/task-model/model.js +919 -0
  411. package/registry/blocks/task-model/parts.jsx +449 -0
  412. package/registry/blocks/task-model/toolbar.jsx +271 -0
  413. package/registry/blocks/testimonials/README.md +139 -0
  414. package/registry/blocks/testimonials/block.json +11 -0
  415. package/registry/blocks/testimonials/demo.jsx +137 -0
  416. package/registry/blocks/testimonials/index.js +4 -0
  417. package/registry/blocks/testimonials/testimonials.jsx +430 -0
  418. package/registry/blocks/transaction-list/README.md +126 -0
  419. package/registry/blocks/transaction-list/block.json +10 -0
  420. package/registry/blocks/transaction-list/demo.jsx +79 -0
  421. package/registry/blocks/transaction-list/index.js +4 -0
  422. package/registry/blocks/transaction-list/transaction-list.jsx +383 -0
  423. package/registry/blocks/whiteboard/README.md +179 -0
  424. package/registry/blocks/whiteboard/block.json +10 -0
  425. package/registry/blocks/whiteboard/demo.jsx +169 -0
  426. package/registry/blocks/whiteboard/index.js +6 -0
  427. package/registry/blocks/whiteboard/templates.js +150 -0
  428. package/registry/blocks/whiteboard/whiteboard.jsx +721 -0
  429. package/src/cli.mjs +19 -6
  430. package/src/install.mjs +18 -0
  431. package/src/registry.mjs +38 -4
  432. package/src/shadcn.mjs +14 -1
  433. package/templates/next/files/README.md +145 -11
  434. package/templates/next/files/app/globals.css +55 -4
  435. package/templates/next/files/check/run.mjs +3 -3
  436. package/templates/next/files/components/ui/avatar.jsx +141 -0
  437. package/templates/next/files/components/ui/breadcrumb.jsx +149 -0
  438. package/templates/next/files/components/ui/button-group.jsx +70 -0
  439. package/templates/next/files/components/ui/button.jsx +132 -12
  440. package/templates/next/files/components/ui/calendar.jsx +609 -0
  441. package/templates/next/files/components/ui/carousel.jsx +281 -0
  442. package/templates/next/files/components/ui/collapsible.jsx +208 -0
  443. package/templates/next/files/components/ui/combobox.jsx +608 -0
  444. package/templates/next/files/components/ui/command.jsx +577 -0
  445. package/templates/next/files/components/ui/context-menu.jsx +191 -0
  446. package/templates/next/files/components/ui/date-picker.jsx +360 -0
  447. package/templates/next/files/components/ui/distribution.jsx +117 -0
  448. package/templates/next/files/components/ui/feedback.jsx +143 -0
  449. package/templates/next/files/components/ui/field.jsx +115 -3
  450. package/templates/next/files/components/ui/file-input.jsx +234 -0
  451. package/templates/next/files/components/ui/icon.jsx +130 -8
  452. package/templates/next/files/components/ui/image.jsx +143 -0
  453. package/templates/next/files/components/ui/index.js +47 -2
  454. package/templates/next/files/components/ui/input-group.jsx +213 -0
  455. package/templates/next/files/components/ui/input-otp.jsx +170 -0
  456. package/templates/next/files/components/ui/item.jsx +206 -0
  457. package/templates/next/files/components/ui/menu.jsx +454 -82
  458. package/templates/next/files/components/ui/notice.jsx +126 -14
  459. package/templates/next/files/components/ui/pagination.jsx +156 -0
  460. package/templates/next/files/components/ui/popover.jsx +284 -0
  461. package/templates/next/files/components/ui/radio.jsx +193 -0
  462. package/templates/next/files/components/ui/rating.jsx +131 -0
  463. package/templates/next/files/components/ui/resizable.jsx +338 -0
  464. package/templates/next/files/components/ui/scroll-area.jsx +123 -0
  465. package/templates/next/files/components/ui/separator.jsx +119 -0
  466. package/templates/next/files/components/ui/sheet.jsx +96 -11
  467. package/templates/next/files/components/ui/slider.jsx +174 -0
  468. package/templates/next/files/components/ui/stat.jsx +574 -0
  469. package/templates/next/files/components/ui/steps.jsx +338 -0
  470. package/templates/next/files/components/ui/swatch.jsx +33 -0
  471. package/templates/next/files/components/ui/table.jsx +130 -0
  472. package/templates/next/files/components/ui/toast.jsx +440 -0
  473. package/templates/next/files/components/ui/toggle.jsx +195 -0
  474. package/templates/next/files/components/ui/tooltip.jsx +270 -0
  475. package/templates/next/template.json +264 -8
@@ -0,0 +1,82 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "doc-editor",
4
+ "type": "registry:block",
5
+ "title": "DocEditor",
6
+ "description": "A Notion-style rich-text editor (BlockNote) painted in the workspace theme: headings, lists, checklists, quotes, code, tables, images and dividers from a / menu or markdown shortcuts, a formatting toolbar, drag handles; @mentions and {merge fields} as inline chips; a minimal mode for comment boxes with Cmd/Ctrl+Enter to submit. The document is BlockNote JSON; helpers turn it into markdown or plain text and read its checklist, fields and mentions.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.1/r/core.json"
9
+ ],
10
+ "dependencies": [
11
+ "@blocknote/core@0.55.0",
12
+ "@blocknote/react@0.55.0",
13
+ "@blocknote/shadcn@0.55.0"
14
+ ],
15
+ "files": [
16
+ {
17
+ "path": "registry/blocks/doc-editor/index.js",
18
+ "type": "registry:component",
19
+ "target": "@components/makersclaw/blocks/doc-editor/index.js",
20
+ "content": "// The DocEditor block — `makersclaw add doc-editor`. Fixed: compose it\n// through its props (README.md beside this file). The helpers read a doc\n// anywhere, the server included; the other text blocks (doc-page,\n// comment-box, record-notes…) are built on this one and import it, never\n// @blocknote/* directly.\nexport { DocEditor, useDocEditor } from \"./doc-editor\";\nexport { docToMarkdown, docToText, emptyDoc, isDocEmpty, docChecklist, setChecklistItem, docFields, docMentions } from \"./doc\";\n"
21
+ },
22
+ {
23
+ "path": "registry/blocks/doc-editor/doc-editor.jsx",
24
+ "type": "registry:component",
25
+ "target": "@components/makersclaw/blocks/doc-editor/doc-editor.jsx",
26
+ "content": "\"use client\";\n\n// DocEditor — BlockNote, in the workspace theme, behind one set of props.\n//\n// Client only, one static bundle. BlockNote needs the DOM, and the app's\n// check refuses import() (so no next/dynamic): the modules load with the\n// page, and the editor itself is created only after the first effect\n// (the mount guard). Until then — on the server and in the first client\n// paint — a placeholder shows the doc's words in the same box, so the\n// server HTML already reads right and nothing jumps.\nimport \"@blocknote/shadcn/style.css\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { filterSuggestionItems } from \"@blocknote/core/extensions\";\nimport { en } from \"@blocknote/core/locales\";\nimport {\n DragHandleMenu,\n FormattingToolbar,\n FormattingToolbarController,\n RemoveBlockItem,\n SideMenu,\n SideMenuController,\n SuggestionMenuController,\n TableColumnHeaderItem,\n TableRowHeaderItem,\n getDefaultReactSlashMenuItems,\n getFormattingToolbarItems,\n useCreateBlockNote,\n} from \"@blocknote/react\";\nimport { BlockNoteView } from \"@blocknote/shadcn\";\nimport { Icon, cx } from \"../../ui\";\nimport { SCHEMA } from \"./schema\";\nimport { THEME } from \"./theme\";\nimport { docToText, emptyDoc, isDocEmpty } from \"./doc\";\n\n/** A ref for the editor's handle: `const ed = useDocEditor()`, then\n * `<DocEditor editorRef={ed} />` and `ed.current?.insertField(\"first_name\")`. */\nexport function useDocEditor() {\n return useRef(null);\n}\n\nconst nonEmpty = (doc) => (Array.isArray(doc) && doc.length ? doc : null);\n\n// The box, the same in the placeholder and the live editor.\nconst BOX = {\n full: \"min-h-40\",\n minimal: \"min-h-10\",\n};\n// `compact`: the full editor (menus, handles) in a short box, for a doc cut\n// into many small sections (brief-doc). A className cannot lower min-h-40:\n// cx joins classes without merging, and min-h-40 won (2026-09-24).\nconst boxOf = (minimal, compact) => (minimal ? BOX.minimal : compact ? \"min-h-16\" : BOX.full);\n// BlockNote's editor element: its own padding off (54px each side, for the\n// drag handles), ours on. Full mode keeps room for the handles from 640px;\n// on a phone there is no hover, so no handles and no gutter.\nconst EDITOR = {\n full: \"[&_.bn-editor]:px-0! [&_.bn-editor]:py-1! sm:[&_.bn-editor]:ps-12! text-base\",\n minimal: \"[&_.bn-editor]:px-0! [&_.bn-editor]:py-0! text-sm\",\n};\n\nexport function DocEditor(props) {\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n if (!mounted) return <DocPlaceholder {...props} />;\n return <LiveEditor {...props} />;\n}\n\n/** What the server renders (and the first client paint): the doc's words,\n * or the placeholder, in the editor's box. Not editable, not focusable. */\nfunction DocPlaceholder({ value, defaultValue, placeholder, minimal = false, compact = false, className }) {\n const doc = nonEmpty(value) ?? nonEmpty(defaultValue);\n const text = doc && !isDocEmpty(doc) ? docToText(doc) : \"\";\n const mode = minimal ? \"minimal\" : \"full\";\n return (\n <div className={cx(boxOf(minimal, compact), className)} aria-busy=\"true\">\n <div className={cx(EDITOR[mode], !minimal && \"sm:ps-12\", \"whitespace-pre-wrap\", text ? \"text-foreground\" : \"text-muted-foreground\")}>\n {text || placeholder || \" \"}\n </div>\n </div>\n );\n}\n\n/** Keep the members of a group together, in first-seen order: BlockNote's\n * menu draws a group heading each time the group changes. */\nfunction grouped(items) {\n const order = [];\n const by = new Map();\n for (const it of items) {\n const g = it.group ?? \"\";\n if (!by.has(g)) {\n by.set(g, []);\n order.push(g);\n }\n by.get(g).push(it);\n }\n return order.flatMap((g) => by.get(g));\n}\n\nconst iconOf = (icon) => (typeof icon === \"string\" ? <Icon name={icon} size={18} /> : icon);\n\n/** The workspace's light or dark, read off <html> (the desk sets data-theme). */\nfunction schemeOf() {\n const root = document.documentElement;\n if (root.dataset.theme === \"light\" || root.dataset.theme === \"dark\") return root.dataset.theme;\n const cs = getComputedStyle(root).colorScheme || \"\";\n return /\\blight\\b/.test(cs) && !/\\bdark\\b/.test(cs) ? \"light\" : \"dark\";\n}\n\nfunction LiveEditor({\n value,\n defaultValue,\n onChange,\n editable = true,\n placeholder,\n slashItems,\n mentions,\n fields,\n minimal = false,\n compact = false,\n autoFocus = false,\n onSubmit,\n onKeyDown,\n uploadFile,\n editorRef,\n className,\n label,\n}) {\n const mode = minimal ? \"minimal\" : \"full\";\n const initial = useRef(nonEmpty(value) ?? nonEmpty(defaultValue) ?? emptyDoc());\n const dictionary = useMemo(\n () => ({\n ...en,\n placeholders: {\n ...en.placeholders,\n emptyDocument: placeholder || undefined,\n // \"type '/' for commands\" is a lie with no slash menu.\n default: minimal ? undefined : en.placeholders.default,\n },\n }),\n // The editor is created once; a new placeholder needs a new key.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [],\n );\n const editor = useCreateBlockNote({ schema: SCHEMA, initialContent: initial.current, dictionary, uploadFile, trailingBlock: !minimal }, []);\n const [scheme, setScheme] = useState(\"dark\");\n useEffect(() => {\n setScheme(schemeOf());\n const obs = new MutationObserver(() => setScheme(schemeOf()));\n obs.observe(document.documentElement, { attributes: true, attributeFilter: [\"data-theme\", \"class\", \"style\"] });\n return () => obs.disconnect();\n }, []);\n\n // Controlled: a `value` that is not what we last sent replaces the doc.\n const last = useRef(JSON.stringify(initial.current));\n const applying = useRef(false);\n useEffect(() => {\n if (value === undefined) return;\n const next = nonEmpty(value) ?? emptyDoc();\n const s = JSON.stringify(next);\n if (s === last.current) return;\n last.current = s;\n applying.current = true;\n try {\n editor.replaceBlocks(editor.document, next);\n } finally {\n applying.current = false;\n }\n }, [value, editor]);\n\n const emit = () => {\n if (applying.current) return;\n const doc = editor.document;\n last.current = JSON.stringify(doc);\n onChange?.(doc);\n };\n\n // The handle a block reaches through editorRef / useDocEditor().\n const handle = useMemo(\n () => ({\n editor,\n focus: () => editor.focus(),\n getDoc: () => editor.document,\n setDoc: (doc) => editor.replaceBlocks(editor.document, nonEmpty(doc) ?? emptyDoc()),\n clear: () => editor.replaceBlocks(editor.document, emptyDoc()),\n isEmpty: () => isDocEmpty(editor.document),\n insertText: (text) => editor.insertInlineContent([String(text ?? \"\")]),\n insertMention: ({ id, label: l }) => editor.insertInlineContent([{ type: \"mention\", props: { id: String(id), label: String(l ?? id) } }, \" \"]),\n insertField: (key, l) => {\n const f = (fields ?? []).find((x) => x.key === key);\n editor.insertInlineContent([{ type: \"field\", props: { key, label: l ?? f?.label ?? key } }, \" \"]);\n },\n }),\n [editor, fields],\n );\n useEffect(() => {\n if (!editorRef) return;\n editorRef.current = handle;\n return () => {\n if (editorRef.current === handle) editorRef.current = null;\n };\n }, [editorRef, handle]);\n\n const fieldItems = (query, group) =>\n filterSuggestionItems(\n (fields ?? []).map((f) => ({\n title: f.label ?? f.key,\n subtext: `{${f.key}}`,\n aliases: [f.key, \"field\", \"merge\"],\n group,\n icon: iconOf(\"tag\"),\n onItemClick: () => handle.insertField(f.key, f.label),\n })),\n query,\n );\n\n const getSlashItems = async (query) => {\n const own = (slashItems ?? []).map((it) => ({\n title: it.title,\n subtext: it.subtext,\n aliases: it.aliases,\n group: it.group ?? \"Other\",\n icon: iconOf(it.icon),\n onItemClick: () => it.onSelect?.(handle),\n }));\n const all = grouped([...getDefaultReactSlashMenuItems(editor), ...own]);\n return [...filterSuggestionItems(all, query), ...(fields?.length ? fieldItems(query, \"Fields\") : [])];\n };\n\n const getMentionItems = async (query) => {\n let people = [];\n try {\n people = (await mentions(query)) ?? [];\n } catch {\n people = [];\n }\n return people.slice(0, 20).map((p) => ({\n title: p.label ?? String(p.id),\n subtext: p.description,\n icon: iconOf(\"user\"),\n onItemClick: () => handle.insertMention({ id: p.id, label: p.label }),\n }));\n };\n\n // Cmd/Ctrl+Enter submits, before BlockNote sees the Enter.\n const keyDown = (e) => {\n if (onSubmit && e.key === \"Enter\" && (e.metaKey || e.ctrlKey) && !e.nativeEvent.isComposing) {\n e.preventDefault();\n e.stopPropagation();\n onSubmit(editor.document);\n return;\n }\n onKeyDown?.(e);\n };\n\n return (\n // raw: a plain box that hears keys on their way into BlockNote's\n // contenteditable (capture), for Cmd/Ctrl+Enter; no control of its own.\n <div className={cx(boxOf(minimal, compact), className)} onKeyDownCapture={keyDown}>\n <BlockNoteView\n editor={editor}\n editable={editable}\n autoFocus={autoFocus}\n theme={scheme}\n className={cx(THEME, EDITOR[mode])}\n onChange={emit}\n aria-label={label}\n slashMenu={false}\n sideMenu={false}\n formattingToolbar={false}\n emojiPicker={!minimal}\n >\n <FormattingToolbarController formattingToolbar={Toolbar} />\n {!minimal && <SideMenuController sideMenu={Handles} />}\n {!minimal && <SuggestionMenuController triggerCharacter=\"/\" getItems={getSlashItems} />}\n {mentions && <SuggestionMenuController triggerCharacter=\"@\" getItems={getMentionItems} />}\n {fields?.length ? <SuggestionMenuController triggerCharacter=\"{\" getItems={async (q) => fieldItems(q)} /> : null}\n </BlockNoteView>\n </div>\n );\n}\n\n// BlockNote's toolbar and block menu without their colour pickers: the\n// theme has no red or purple to offer (theme.js maps what is stored).\nconst Toolbar = () => <FormattingToolbar>{getFormattingToolbarItems().filter((el) => el.key !== \"colorStyleButton\")}</FormattingToolbar>;\nconst BlockMenu = () => (\n <DragHandleMenu>\n <RemoveBlockItem>{en.drag_handle.delete_menuitem}</RemoveBlockItem>\n <TableRowHeaderItem>{en.drag_handle.header_row_menuitem}</TableRowHeaderItem>\n <TableColumnHeaderItem>{en.drag_handle.header_column_menuitem}</TableColumnHeaderItem>\n </DragHandleMenu>\n);\nconst Handles = (props) => <SideMenu {...props} dragHandleMenu={BlockMenu} />;\n"
27
+ },
28
+ {
29
+ "path": "registry/blocks/doc-editor/schema.jsx",
30
+ "type": "registry:component",
31
+ "target": "@components/makersclaw/blocks/doc-editor/schema.jsx",
32
+ "content": "\"use client\";\n\n// What a doc may hold: BlockNote's own blocks (less audio, video and file,\n// which need an upload store this block does not have), its text styles,\n// and two inline chips of ours:\n//\n// { type: \"mention\", props: { id, label } } @Priya — a person\n// { type: \"field\", props: { key, label } } {first_name} — a merge field\n//\n// Both are atoms (no text inside): the caret steps over them and Backspace\n// takes the whole chip. doc.js turns them into @Label and {key} / a value.\nimport { BlockNoteSchema, defaultBlockSpecs, defaultInlineContentSpecs } from \"@blocknote/core\";\nimport { createReactInlineContentSpec } from \"@blocknote/react\";\n\nconst CHIP = \"inline-flex items-baseline rounded-sm px-1 font-medium whitespace-nowrap\";\n\nconst Mention = createReactInlineContentSpec(\n {\n type: \"mention\",\n propSchema: { id: { default: \"\" }, label: { default: \"\" } },\n content: \"none\",\n },\n {\n render: ({ inlineContent }) => (\n <span className={`${CHIP} bg-primary/10 text-primary`} data-mention={inlineContent.props.id}>\n @{inlineContent.props.label || inlineContent.props.id}\n </span>\n ),\n },\n);\n\nconst Field = createReactInlineContentSpec(\n {\n type: \"field\",\n propSchema: { key: { default: \"\" }, label: { default: \"\" } },\n content: \"none\",\n },\n {\n render: ({ inlineContent }) => (\n <span\n className={`${CHIP} border border-dashed border-border bg-muted text-muted-foreground`}\n data-field={inlineContent.props.key}\n title={`{${inlineContent.props.key}}`}\n >\n {inlineContent.props.label || inlineContent.props.key}\n </span>\n ),\n },\n);\n\nconst { audio, video, file, ...blockSpecs } = defaultBlockSpecs;\n\nexport const SCHEMA = BlockNoteSchema.create({\n blockSpecs,\n inlineContentSpecs: { ...defaultInlineContentSpecs, mention: Mention, field: Field },\n});\n"
33
+ },
34
+ {
35
+ "path": "registry/blocks/doc-editor/theme.js",
36
+ "type": "registry:component",
37
+ "target": "@components/makersclaw/blocks/doc-editor/theme.js",
38
+ "content": "// BlockNote in the workspace theme — the one place its look is mapped.\n//\n// BlockNote paints itself from CSS variables (--bn-colors-editor-text,\n// --bn-colors-menu-background…) that its stylesheet sets on every\n// `.bn-root` with its own hex values. Here each one is pointed at a token\n// instead, as Tailwind arbitrary properties on the editor's root. The\n// root's className is also what BlockNote copies onto the roots of the\n// menus it portals out of the editor, so the menus wear the same tokens.\n//\n// Every class ends in `!`: BlockNote's stylesheet is unlayered, and an\n// unlayered rule beats any Tailwind utility (those live in @layer\n// utilities) whatever its specificity. `!important` inside a layer is the\n// one thing that outranks it.\n//\n// The highlight colours (BlockNote's red/blue/… text and backgrounds) map\n// to the status and chart tokens; every background to muted, because the\n// theme has one tint, not nine. The toolbar and the block menu do not offer\n// colours (doc-editor.jsx), so these only paint pasted or stored content.\nexport const THEME = [\n // surface, text, menus\n \"[--bn-colors-editor-text:var(--color-foreground)]!\",\n \"[--bn-colors-editor-background:transparent]!\",\n \"[--bn-colors-menu-text:var(--color-popover-foreground)]!\",\n \"[--bn-colors-menu-background:var(--color-popover)]!\",\n \"[--bn-colors-tooltip-text:var(--color-popover-foreground)]!\",\n \"[--bn-colors-tooltip-background:var(--color-popover)]!\",\n \"[--bn-colors-hovered-text:var(--color-accent-foreground)]!\",\n \"[--bn-colors-hovered-background:var(--color-accent)]!\",\n \"[--bn-colors-selected-text:var(--color-primary-foreground)]!\",\n \"[--bn-colors-selected-background:var(--color-primary)]!\",\n \"[--bn-colors-disabled-text:var(--color-muted-foreground)]!\",\n \"[--bn-colors-disabled-background:var(--color-muted)]!\",\n \"[--bn-colors-shadow:var(--color-border)]!\",\n \"[--bn-colors-border:var(--color-border)]!\",\n \"[--bn-colors-side-menu:var(--color-muted-foreground)]!\",\n // highlights: text → status / chart tokens, backgrounds → muted\n \"[--bn-colors-highlights-gray-text:var(--color-muted-foreground)]!\",\n \"[--bn-colors-highlights-brown-text:var(--color-muted-foreground)]!\",\n \"[--bn-colors-highlights-red-text:var(--color-destructive)]!\",\n \"[--bn-colors-highlights-orange-text:var(--color-warning)]!\",\n \"[--bn-colors-highlights-yellow-text:var(--color-warning)]!\",\n \"[--bn-colors-highlights-green-text:var(--color-success)]!\",\n \"[--bn-colors-highlights-blue-text:var(--color-info)]!\",\n \"[--bn-colors-highlights-purple-text:var(--color-chart-3)]!\",\n \"[--bn-colors-highlights-pink-text:var(--color-chart-4)]!\",\n \"[--bn-colors-highlights-gray-background:var(--color-muted)]!\",\n \"[--bn-colors-highlights-brown-background:var(--color-muted)]!\",\n \"[--bn-colors-highlights-red-background:var(--color-muted)]!\",\n \"[--bn-colors-highlights-orange-background:var(--color-muted)]!\",\n \"[--bn-colors-highlights-yellow-background:var(--color-muted)]!\",\n \"[--bn-colors-highlights-green-background:var(--color-muted)]!\",\n \"[--bn-colors-highlights-blue-background:var(--color-muted)]!\",\n \"[--bn-colors-highlights-purple-background:var(--color-muted)]!\",\n \"[--bn-colors-highlights-pink-background:var(--color-muted)]!\",\n // face and radius: the workspace's, not Inter at 16px\n \"[--bn-font-family:var(--theme-font-sans)]!\",\n \"[--bn-border-radius:var(--theme-radius)]!\",\n \"[&_.bn-default-styles]:font-sans!\",\n \"[&_.bn-default-styles]:[font-size:inherit]!\",\n \"[&_.bn-default-styles]:[letter-spacing:inherit]!\",\n // what BlockNote paints with fixed values rather than its variables\n \"[&_.bn-editor_a]:text-primary!\",\n \"[&_.bn-editor_a]:underline!\",\n \"[&_.bn-inline-content_code]:font-mono!\",\n \"[&_.bn-inline-content_code]:rounded-sm!\",\n \"[&_.bn-inline-content_code]:bg-muted!\",\n \"[&_.bn-inline-content_code]:px-1!\",\n \"[&_[data-content-type=codeBlock]]:bg-muted!\",\n \"[&_[data-content-type=codeBlock]]:text-foreground!\",\n \"[&_[data-content-type=codeBlock]]:font-mono!\",\n \"[&_[data-content-type=codeBlock]]:rounded-md!\",\n \"[&_[data-content-type=codeBlock]_select]:text-muted-foreground!\",\n \"[&_[data-content-type=quote]_blockquote]:text-muted-foreground!\",\n \"[&_[data-content-type=quote]_blockquote]:border-border!\",\n \"[&_[data-content-type=checkListItem]_input]:accent-primary!\",\n \"[&_[data-checked=true]_.bn-inline-content]:text-muted-foreground!\",\n \"[&_.bn-block-content]:after:text-muted-foreground!\",\n \"[&_.bn-block-content]:after:not-italic!\",\n \"[&_table]:border-border!\",\n \"[&_td]:border-border!\",\n \"[&_th]:border-border!\",\n].join(\" \");\n"
39
+ },
40
+ {
41
+ "path": "registry/blocks/doc-editor/doc.js",
42
+ "type": "registry:component",
43
+ "target": "@components/makersclaw/blocks/doc-editor/doc.js",
44
+ "content": "// The document, outside the editor. A doc is BlockNote JSON: an array of\n// blocks, each { id, type, props, content, children }. These are plain\n// functions over that JSON — no editor, no browser — so a server surface,\n// an action or a test can read a doc the same way the screen does.\n//\n// Why not BlockNote's own blocksToMarkdownLossy: it needs a mounted editor\n// and a DOM, and it drops our inline content (a mention, a merge field).\n// Walking the JSON keeps both, and gives the same text on the server.\n\n/** A doc with nothing in it: one empty paragraph (BlockNote refuses []). */\nexport function emptyDoc() {\n return [{ type: \"paragraph\", content: [] }];\n}\n\nconst blocksOf = (doc) => (Array.isArray(doc) ? doc : []);\n\n/** Walk every block, depth first, with its depth. */\nfunction eachBlock(doc, fn, depth = 0) {\n for (const b of blocksOf(doc)) {\n if (!b || typeof b !== \"object\") continue;\n fn(b, depth);\n eachBlock(b.children, fn, depth + 1);\n }\n}\n\n/** The inline pieces of a block's content, whatever shape it came in. */\nfunction inlineOf(content) {\n if (typeof content === \"string\") return [{ type: \"text\", text: content, styles: {} }];\n return Array.isArray(content) ? content : [];\n}\n\n/** How a merge field reads: its value when `fields` gives one, else `{key}`. */\nfunction fieldValue(props, fields) {\n const key = props?.key ?? \"\";\n const v = typeof fields === \"function\" ? fields(key, props) : fields && typeof fields === \"object\" ? fields[key] : undefined;\n return v === undefined || v === null || v === \"\" ? `{${key}}` : String(v);\n}\n\nfunction inlineText(content, opts) {\n let out = \"\";\n for (const c of inlineOf(content)) {\n if (!c) continue;\n if (c.type === \"text\") out += c.text ?? \"\";\n else if (c.type === \"link\") out += inlineText(c.content, opts);\n else if (c.type === \"mention\") out += `@${c.props?.label ?? c.props?.id ?? \"\"}`;\n else if (c.type === \"field\") out += fieldValue(c.props, opts?.fields);\n else if (Array.isArray(c.content)) out += inlineText(c.content, opts);\n }\n return out;\n}\n\n/** The text of one block's own content (not its children). */\nfunction blockText(block, opts) {\n const c = block.content;\n if (c && typeof c === \"object\" && !Array.isArray(c) && c.type === \"tableContent\") {\n return (c.rows ?? []).map((r) => (r.cells ?? []).map((cell) => inlineText(cell?.content ?? cell, opts)).join(\"\\t\")).join(\"\\n\");\n }\n if (block.type === \"image\" || block.type === \"file\" || block.type === \"video\" || block.type === \"audio\") {\n return block.props?.caption || block.props?.name || \"\";\n }\n return inlineText(c, opts);\n}\n\nconst PREFIX_TEXT = {\n bulletListItem: () => \"- \",\n toggleListItem: () => \"- \",\n checkListItem: (b) => (b.props?.checked ? \"[x] \" : \"[ ] \"),\n};\n\n/**\n * Plain text: one line per block (an empty paragraph is an empty line),\n * children indented by two spaces, list markers kept (\"- \", \"1. \",\n * \"[ ] \"), a mention as @Label, a merge field as its value or {key}.\n *\n * docToText(doc) → \"Hi {first_name},\\n…\"\n * docToText(doc, { fields: { first_name: \"Ana\" } }) → \"Hi Ana,\\n…\"\n *\n * `fields` is { key: value } or (key, props) => value; an empty value\n * leaves {key} in place.\n */\nexport function docToText(doc, opts = {}) {\n const lines = [];\n const walk = (blocks, depth) => {\n let n = 0;\n for (const b of blocksOf(blocks)) {\n if (!b || typeof b !== \"object\") continue;\n n = b.type === \"numberedListItem\" ? n + 1 : 0;\n if (b.type !== \"divider\") {\n const marker = b.type === \"numberedListItem\" ? `${n}. ` : PREFIX_TEXT[b.type]?.(b) ?? \"\";\n const text = blockText(b, opts);\n const pad = \" \".repeat(depth);\n lines.push(...`${marker}${text}`.split(\"\\n\").map((l, i) => (i === 0 ? pad + l : pad + \" \".repeat(marker.length) + l)));\n }\n walk(b.children, depth + 1);\n }\n };\n walk(doc, 0);\n return lines.join(\"\\n\").replace(/\\s+$/, \"\");\n}\n\n// ---- markdown --------------------------------------------------------------\n\n// Escape what would turn into markup. An underscore inside a word\n// (first_name) is not emphasis in GFM, so it stays bare.\nconst escapeMd = (s) => s.replace(/([\\\\`*[\\]])/g, \"\\\\$1\").replace(/(^|[^\\w])_|_(?![\\w])/g, (m) => m.replace(\"_\", \"\\\\_\"));\n\n/** Wrap a run in markers, keeping its edge spaces outside (\"** bold**\" is not bold). */\nfunction wrap(text, marker) {\n const m = /^(\\s*)([\\s\\S]*?)(\\s*)$/.exec(text);\n return m[2] ? `${m[1]}${marker}${m[2]}${marker}${m[3]}` : text;\n}\n\nfunction inlineMd(content, opts) {\n let out = \"\";\n for (const c of inlineOf(content)) {\n if (!c) continue;\n if (c.type === \"text\") {\n const s = c.styles ?? {};\n let t = s.code ? `\\`${(c.text ?? \"\").replace(/`/g, \"ˋ\")}\\`` : escapeMd(c.text ?? \"\");\n if (s.strike) t = wrap(t, \"~~\");\n if (s.italic) t = wrap(t, \"*\");\n if (s.bold) t = wrap(t, \"**\");\n out += t;\n } else if (c.type === \"link\") out += `[${inlineMd(c.content, opts)}](${c.href ?? \"\"})`;\n else if (c.type === \"mention\") out += `@${escapeMd(c.props?.label ?? c.props?.id ?? \"\")}`;\n else if (c.type === \"field\") out += escapeMd(fieldValue(c.props, opts?.fields));\n else if (Array.isArray(c.content)) out += inlineMd(c.content, opts);\n }\n return out;\n}\n\nfunction tableMd(content, opts) {\n const rows = (content.rows ?? []).map((r) => (r.cells ?? []).map((cell) => inlineMd(cell?.content ?? cell, opts).replace(/\\|/g, \"\\\\|\").replace(/\\n/g, \" \")));\n if (!rows.length) return \"\";\n const width = Math.max(...rows.map((r) => r.length));\n const line = (r) => `| ${Array.from({ length: width }, (_, i) => r[i] ?? \"\").join(\" | \")} |`;\n return [line(rows[0]), `| ${Array(width).fill(\"---\").join(\" | \")} |`, ...rows.slice(1).map(line)].join(\"\\n\");\n}\n\nconst LIST = new Set([\"bulletListItem\", \"numberedListItem\", \"checkListItem\", \"toggleListItem\"]);\n\n/**\n * Markdown (GitHub-flavoured): headings, lists with nesting, checklists\n * (- [ ] / - [x]), quotes, fenced code, tables, images, dividers; bold,\n * italic, strike, inline code and links. A mention is @Label, a merge\n * field its value or {key} (same `fields` as docToText). Lossy where\n * markdown has no word for it: underline, colours, alignment, a toggle's\n * folding. Empty paragraphs are dropped.\n */\nexport function docToMarkdown(doc, opts = {}) {\n const out = [];\n const walk = (blocks, indent) => {\n let n = 0;\n let prevList = false;\n for (const b of blocksOf(blocks)) {\n if (!b || typeof b !== \"object\") continue;\n n = b.type === \"numberedListItem\" ? n + 1 : 0;\n // Which list it belongs to: a numbered list after a bulleted one is a\n // new list, and gets a blank line between.\n const isList = LIST.has(b.type) ? (b.type === \"numberedListItem\" ? \"ol\" : \"ul\") : false;\n let text;\n let marker = \"\";\n const c = b.content;\n switch (b.type) {\n case \"heading\":\n text = `${\"#\".repeat(Math.min(6, Math.max(1, Number(b.props?.level) || 1)))} ${inlineMd(c, opts)}`;\n break;\n case \"bulletListItem\":\n case \"toggleListItem\":\n marker = \"- \";\n text = inlineMd(c, opts);\n break;\n case \"numberedListItem\":\n marker = `${n}. `;\n text = inlineMd(c, opts);\n break;\n case \"checkListItem\":\n marker = `- [${b.props?.checked ? \"x\" : \" \"}] `;\n text = inlineMd(c, opts);\n break;\n case \"quote\":\n text = inlineMd(c, opts)\n .split(\"\\n\")\n .map((l) => `> ${l}`)\n .join(\"\\n\");\n break;\n case \"codeBlock\":\n text = `\\`\\`\\`${b.props?.language && b.props.language !== \"text\" ? b.props.language : \"\"}\\n${inlineText(c)}\\n\\`\\`\\``;\n break;\n case \"divider\":\n text = \"---\";\n break;\n case \"table\":\n text = c && typeof c === \"object\" && !Array.isArray(c) ? tableMd(c, opts) : \"\";\n break;\n case \"image\":\n text = b.props?.url ? `![${escapeMd(b.props?.caption || b.props?.name || \"\")}](${b.props.url})` : \"\";\n break;\n case \"file\":\n case \"video\":\n case \"audio\":\n text = b.props?.url ? `[${escapeMd(b.props?.name || b.props?.caption || b.props.url)}](${b.props.url})` : \"\";\n break;\n default:\n text = inlineMd(c, opts);\n }\n const pad = \" \".repeat(indent);\n if (text.trim() || marker) {\n const body = `${marker}${text}`.split(\"\\n\").map((l, i) => (i === 0 ? pad + l : l ? pad + \" \".repeat(marker.length) + l : l));\n // Blank line between blocks; list items in a row stay together.\n if (out.length && !(isList && isList === prevList)) out.push(\"\");\n out.push(body.join(\"\\n\"));\n }\n prevList = isList;\n if (b.children?.length) {\n const before = out.length;\n walk(b.children, indent + (marker ? marker.length : 2));\n // A nested list follows its item directly.\n if (isList && out.length > before && out[before] === \"\") out.splice(before, 1);\n }\n }\n };\n walk(doc, 0);\n return out.join(\"\\n\").replace(/\\n{3,}/g, \"\\n\\n\").trim();\n}\n\n// ---- reading a doc ---------------------------------------------------------\n\n/** True when there is no text, no mention, no field and no block but empty paragraphs. */\nexport function isDocEmpty(doc) {\n let empty = true;\n eachBlock(doc, (b) => {\n if (!empty) return;\n if (b.type !== \"paragraph\") empty = false;\n else if (inlineOf(b.content).some((c) => c && (c.type !== \"text\" || (c.text ?? \"\").trim()))) empty = false;\n });\n return empty;\n}\n\nfunction inlineOfType(content, type, out) {\n for (const c of inlineOf(content)) {\n if (!c) continue;\n if (c.type === type) out.push(c.props ?? {});\n else if (Array.isArray(c.content)) inlineOfType(c.content, type, out);\n }\n return out;\n}\n\n/**\n * The checklist items, in order, nested ones too:\n * [{ id, text, checked, depth, mentions: [{ id, label }] }]. `text` is the\n * item's words (a mention as @Label); `mentions` lets a meeting-notes block\n * read an owner off \"@Priya send the deck\".\n */\nexport function docChecklist(doc, opts = {}) {\n const items = [];\n eachBlock(doc, (b, depth) => {\n if (b.type !== \"checkListItem\") return;\n items.push({\n id: b.id ?? null,\n text: inlineText(b.content, opts),\n checked: Boolean(b.props?.checked),\n depth,\n mentions: inlineOfType(b.content, \"mention\", []).map((p) => ({ id: p.id ?? \"\", label: p.label ?? \"\" })),\n });\n });\n return items;\n}\n\n/** The doc with one checklist item ticked or unticked (by block id). A new array; the old doc is untouched. */\nexport function setChecklistItem(doc, id, checked) {\n const walk = (blocks) =>\n blocksOf(blocks).map((b) =>\n b?.id === id && b.type === \"checkListItem\"\n ? { ...b, props: { ...b.props, checked: Boolean(checked) } }\n : b?.children?.length\n ? { ...b, children: walk(b.children) }\n : b,\n );\n return walk(doc);\n}\n\nfunction tally(doc, type, keyOf) {\n const seen = new Map();\n eachBlock(doc, (b) => {\n const content = b.content && typeof b.content === \"object\" && b.content.type === \"tableContent\" ? (b.content.rows ?? []).flatMap((r) => (r.cells ?? []).flatMap((cell) => inlineOf(cell?.content ?? cell))) : b.content;\n for (const p of inlineOfType(content, type, [])) {\n const k = keyOf(p);\n const had = seen.get(k);\n if (had) had.count++;\n else seen.set(k, { ...p, count: 1 });\n }\n });\n return [...seen.values()];\n}\n\n/**\n * The merge fields a doc uses, in order of first use:\n * [{ key, label, count }]. Compare with the recipient's values to warn\n * \"missing field\" before a send.\n */\nexport function docFields(doc) {\n return tally(doc, \"field\", (p) => p.key ?? \"\").map((p) => ({ key: p.key ?? \"\", label: p.label ?? p.key ?? \"\", count: p.count }));\n}\n\n/** The people a doc mentions, in order of first mention: [{ id, label, count }]. */\nexport function docMentions(doc) {\n return tally(doc, \"mention\", (p) => p.id ?? \"\").map((p) => ({ id: p.id ?? \"\", label: p.label ?? \"\", count: p.count }));\n}\n"
45
+ },
46
+ {
47
+ "path": "registry/blocks/doc-editor/shadcn-classes.js",
48
+ "type": "registry:component",
49
+ "target": "@components/makersclaw/blocks/doc-editor/shadcn-classes.js",
50
+ "content": "// Written by scripts/blocknote-classes.mjs from @blocknote/shadcn 0.55.0 — do not edit.\n//\n// The Tailwind classes BlockNote's shadcn menus, toolbar and popovers use.\n// Nothing imports this file: it is here so the app's Tailwind, which reads\n// components/**, makes these classes (globals.css does not read\n// node_modules). Only token classes: dark:, colour functions and raw hues\n// were dropped on the way in.\nexport const SHADCN_CLASSES = [\n \"**:data-[slot=kbd]:isolate **:data-[slot=kbd]:relative **:data-[slot=kbd]:rounded-sm\",\n \"**:data-[slot=kbd]:z-50 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl\",\n \"*:[span]:last:flex *:[span]:last:gap-2 *:[span]:last:items-center *:data-[slot=select-value]:flex\",\n \"*:data-[slot=select-value]:gap-1.5 *:data-[slot=select-value]:items-center\",\n \"*:data-[slot=select-value]:line-clamp-1 -mx-1 [&>svg]:pointer-events-none [&>svg]:size-3!\",\n \"[&_svg:not([class*='size-'])]:size-3 [&_svg:not([class*='size-'])]:size-4\",\n \"[&_svg:not([class*=size-])]:size-4 [&_svg:not([class*=text-])]:text-muted-foreground\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [--card-spacing:--spacing(6)]\",\n \"[a]:hover:bg-destructive/20 [a]:hover:bg-muted [a]:hover:bg-primary/80 [a]:hover:bg-secondary/80\",\n \"[a]:hover:text-muted-foreground active:not-aria-[haspopup]:translate-y-px after:absolute\",\n \"after:bg-foreground after:border after:border-border after:inset-0 after:mix-blend-darken\",\n \"after:opacity-0 after:rounded-full after:transition-opacity animate-pulse aria-activedescendant\",\n \"aria-disabled:opacity-50 aria-disabled:pointer-events-none aria-expanded:bg-muted\",\n \"aria-expanded:bg-secondary aria-expanded:text-foreground aria-expanded:text-secondary-foreground\",\n \"aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-label\",\n \"aria-pressed:bg-muted aria-selected aria-selected:bg-accent aria-selected:text-accent-foreground\",\n \"aspect-square avatar-fallback avatar-image bg-background bg-border bg-card bg-clip-padding\",\n \"bg-destructive/10 bg-foreground bg-muted bg-popover bg-primary bg-secondary bg-transparent\",\n \"bn-form bn-shadcn bn-snapshot-menu border-border border-input border-none border-transparent\",\n \"bottom-0 card-content cursor-default data-[align-trigger=true]:animate-none\",\n \"data-[disabled]:opacity-50 data-[disabled]:pointer-events-none data-[inset]:pl-8\",\n \"data-[orientation=horizontal]:flex-col data-[side=bottom]:slide-in-from-top-2\",\n \"data-[side=bottom]:top-1 data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2\",\n \"data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-end]:top-1/2!\",\n \"data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2\",\n \"data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-start]:top-1/2!\",\n \"data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2\",\n \"data-[side=left]:slide-in-from-right-2 data-[side=left]:top-1/2! data-[side=right]:-left-1\",\n \"data-[side=right]:-translate-y-1/2 data-[side=right]:slide-in-from-left-2\",\n \"data-[side=right]:top-1/2! data-[side=top]:-bottom-2.5 data-[side=top]:slide-in-from-bottom-2\",\n \"data-[size=default]:h-9 data-[size=lg]:size-10 data-[size=sm]:[--card-spacing:--spacing(4)]\",\n \"data-[size=sm]:h-8 data-[size=sm]:size-6 data-[state=delayed-open]:animate-in\",\n \"data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95\",\n \"data-[variant=destructive]:*:[svg]:!text-destructive\",\n \"data-[variant=destructive]:*:[svg]:text-destructive\",\n \"data-[variant=destructive]:aria-selected:bg-destructive/10\",\n \"data-[variant=destructive]:aria-selected:text-destructive\",\n \"data-[variant=destructive]:focus:bg-destructive/10\",\n \"data-[variant=destructive]:focus:text-destructive\",\n \"data-[variant=destructive]:hover:bg-destructive/10\",\n \"data-[variant=destructive]:hover:text-destructive data-[variant=destructive]:text-destructive\",\n \"data-[variant=line]:rounded-none data-active:bg-background data-active:text-foreground\",\n \"data-align-trigger data-closed:animate-out data-closed:fade-out-0 data-closed:overflow-hidden\",\n \"data-closed:zoom-out-95 data-disabled:opacity-50 data-disabled:pointer-events-none data-inset\",\n \"data-inset:pl-8 data-open:animate-in data-open:bg-accent data-open:fade-in-0\",\n \"data-open:text-accent-foreground data-open:zoom-in-95 data-orientation\",\n \"data-placeholder:text-muted-foreground data-popup-open:bg-accent\",\n \"data-popup-open:text-accent-foreground data-size data-slot data-test data-variant\",\n \"disabled:cursor-not-allowed disabled:opacity-50 disabled:pointer-events-none dropdown-menu\",\n \"dropdown-menu-checkbox-item dropdown-menu-checkbox-item-indicator dropdown-menu-content\",\n \"dropdown-menu-group dropdown-menu-item dropdown-menu-label dropdown-menu-separator\",\n \"dropdown-menu-sub dropdown-menu-sub-content dropdown-menu-sub-trigger dropdown-menu-trigger\",\n \"duration-100 embed-input file:bg-transparent file:border-0 file:font-medium file:h-7\",\n \"file:inline-flex file:text-foreground file:text-sm fill-foreground flex-1 flex-col flex-nowrap\",\n \"flex-row flex-wrap focus-visible:border-destructive/40 focus-visible:border-ring\",\n \"focus-visible:outline-1 focus-visible:outline-none focus-visible:outline-ring\",\n \"focus-visible:ring-2 focus-visible:ring-3 focus-visible:ring-[3px]\",\n \"focus-visible:ring-destructive/20 focus-visible:ring-offset-2 focus-visible:ring-ring\",\n \"focus-visible:ring-ring/50 focus:**:text-accent-foreground focus:bg-accent\",\n \"focus:text-accent-foreground font-medium gap-(--card-spacing) gap-0 gap-1 gap-1.5 gap-2 gap-4\",\n \"gap-6 group-data-[disabled=true]:opacity-50 group-data-[disabled=true]:pointer-events-none\",\n \"group-data-[orientation=horizontal]/tabs:after:bottom-[-5px]\",\n \"group-data-[orientation=horizontal]/tabs:after:h-0.5\",\n \"group-data-[orientation=horizontal]/tabs:after:inset-x-0\",\n \"group-data-[orientation=horizontal]/tabs:h-9\",\n \"group-data-[orientation=vertical]/tabs:after:-right-1\",\n \"group-data-[orientation=vertical]/tabs:after:inset-y-0\",\n \"group-data-[orientation=vertical]/tabs:after:w-0.5\",\n \"group-data-[orientation=vertical]/tabs:flex-col group-data-[orientation=vertical]/tabs:h-fit\",\n \"group-data-[orientation=vertical]/tabs:justify-start\",\n \"group-data-[orientation=vertical]/tabs:w-full group-data-[size=sm]/avatar:text-xs\",\n \"group-data-[variant=default]/tabs-list:data-active:shadow-sm\",\n \"group-data-[variant=line]/tabs-list:bg-transparent\",\n \"group-data-[variant=line]/tabs-list:data-active:after:opacity-100\",\n \"group-data-[variant=line]/tabs-list:data-active:bg-transparent\",\n \"group-data-[variant=line]/tabs-list:data-active:shadow-none group/dropdown-menu-item\",\n \"group/tabs-list h-10 h-4 h-5 h-6 h-7 h-8 h-9 h-[calc(100%-1px)] h-auto h-fit h-px\",\n \"has-[>img:first-child]:pt-0 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-end]:pr-2\",\n \"has-data-[icon=inline-start]:pl-1.5 has-data-[icon=inline-start]:pl-2 has-data-[slot=kbd]:pr-1.5\",\n \"hover:bg-accent hover:bg-destructive/20 hover:bg-muted hover:bg-primary/80\",\n \"hover:text-accent-foreground hover:text-foreground hover:text-muted-foreground hover:underline\",\n \"icon-lg icon-sm icon-xs in-data-[slot=button-group]:rounded-md inline-flex items-center\",\n \"items-start justify-between justify-center leading-none max-h-(--available-height) max-w-full\",\n \"max-w-none max-w-screen max-w-xs md:text-sm min-w-0 min-w-10 min-w-32 min-w-36 min-w-6 min-w-8\",\n \"min-w-9 min-w-[96px] ml-auto my-1 not-data-[variant=destructive]:focus:**:text-accent-foreground\",\n \"object-cover origin-(--transform-origin) outline-hidden outline-none overflow-hidden\",\n \"overflow-x-hidden overflow-y-auto p-0 p-1 p-2 p-4 p-[3px] panel-popover\",\n \"peer-disabled:cursor-not-allowed peer-disabled:opacity-50 pl-2 pl-2.5\",\n \"placeholder:text-muted-foreground pointer-events-none popover-content popover-trigger pr-2 pr-8\",\n \"pt-4 px-(--card-spacing) px-2 px-2.5 px-3 py-(--card-spacing) py-0 py-0.5 py-1 py-1.5 py-2\",\n \"right-0 right-2 ring-1 ring-foreground/10 ring-offset-background rotate-45 rounded-4xl\",\n \"rounded-[2px] rounded-[min(var(--radius-md),10px)] rounded-[min(var(--radius-md),8px)]\",\n \"rounded-full rounded-lg rounded-md rounded-sm rounded-xl select-content select-item select-none\",\n \"select-scroll-down-button select-scroll-up-button select-trigger select-value shadow-lg shadow-md\",\n \"shadow-none shadow-xs shrink-0 size-10 size-2.5 size-4 size-6 size-8 size-9 size-full\",\n \"tabs-content tabs-list tabs-trigger text-background text-base text-base! text-card-foreground\",\n \"text-destructive text-foreground text-foreground/60 text-left text-muted-foreground\",\n \"text-popover-foreground text-primary text-primary-foreground text-secondary-foreground text-sm\",\n \"text-xs tooltip-content tooltip-provider tooltip-trigger top-0 transition-[color,box-shadow]\",\n \"transition-all translate-y-[calc(-50%-2px)] underline-offset-4 w-(--anchor-width) w-32 w-4 w-72\",\n \"w-80 w-auto w-fit w-full whitespace-nowrap whitespace-pre-wrap z-10 z-50\",\n].join(\" \");\n"
51
+ },
52
+ {
53
+ "path": "registry/blocks/doc-editor/README.md",
54
+ "type": "registry:file",
55
+ "target": "@components/makersclaw/blocks/doc-editor/README.md",
56
+ "content": "# DocEditor\n\nA Notion-style rich-text editor in the workspace theme. Type `/` for a\nblock (headings, lists, checklists, quotes, code, tables, images,\ndividers), or use markdown shortcuts (`#`, `-`, `1.`, `[]`, `>`). Select\ntext for the formatting toolbar; hover a block for its drag handle. `@`\nmentions a person, `{` inserts a merge field; both sit in the text as\nchips. In `minimal` mode it is a comment box: no side menu, no `/` menu,\nand Cmd/Ctrl+Enter submits.\n\nIt is [BlockNote](https://www.blocknotejs.org) (MPL-2.0) underneath, with\nits colours, face and radius mapped onto the workspace's tokens. The\nother text blocks — `doc-page`, `comment-box`, `record-notes`,\n`email-composer`, `meeting-notes`, `brief-doc`, `knowledge-base` — are\nbuilt on this one: they import it, never `@blocknote/*`.\n\n```jsx\nimport { DocEditor, docToMarkdown } from \"../components/blocks/doc-editor\"; // a Makersclaw app (\"./blocks/doc-editor\" from components/)\nimport { DocEditor, docToMarkdown } from \"@/components/makersclaw/blocks/doc-editor\"; // a shadcn project\n```\n\n`DocEditor` is a client component: render it from a `\"use client\"`\ncomponent. The helpers (`docToMarkdown`, `docToText`, `docChecklist`…) are\nplain functions and work on the server too.\n\n`makersclaw add doc-editor` adds `@blocknote/core`, `@blocknote/react` and\n`@blocknote/shadcn` (pinned, exact) to package.json: run `npm install`\nafter. It loads with the page (no `import()`): the editor is created\nafter the first render, and until then — on the server — the box shows\nthe doc's words as plain text.\n\n## The data\n\nThe document is BlockNote's JSON: an array of blocks. Store it as it\ncomes out of `onChange` (a `jsonb` column).\n\n```js\n[\n { id: \"…\", type: \"heading\", props: { level: 2 }, content: [{ type: \"text\", text: \"Launch\", styles: {} }], children: [] },\n { id: \"…\", type: \"paragraph\", content: [\n { type: \"text\", text: \"Hi \", styles: {} },\n { type: \"field\", props: { key: \"first_name\", label: \"First name\" } },\n { type: \"text\", text: \", \", styles: {} },\n { type: \"mention\", props: { id: \"u-42\", label: \"Priya Raman\" } },\n { type: \"text\", text: \" will call.\", styles: { bold: true } },\n ], children: [] },\n { id: \"…\", type: \"checkListItem\", props: { checked: false }, content: [{ type: \"text\", text: \"Send the deck\", styles: {} }], children: [] },\n]\n```\n\n- Block types: `paragraph`, `heading` (`props.level` 1–3), `bulletListItem`,\n `numberedListItem`, `checkListItem` (`props.checked`), `toggleListItem`,\n `quote`, `codeBlock` (`props.language`), `table`, `image` (`props.url`,\n `caption`), `divider`. Blocks nest through `children`.\n- Inline content: `text` (`styles`: `bold`, `italic`, `underline`,\n `strike`, `code`), `link` (`href`, `content`), and two of ours:\n - `{ type: \"mention\", props: { id, label } }` — a person, drawn `@Label`.\n - `{ type: \"field\", props: { key, label } }` — a merge field, drawn as a\n chip with its label, written out as `{key}` or its value.\n- When you write a doc by hand (a starter doc, a template), ids, `props`\n and `children` may be left out and `content` may be a string:\n `[{ type: \"paragraph\", content: \"Hello\" }]`. The editor gives blocks\n ids on the first edit; give them ids yourself when something reads\n them before that (`setChecklistItem` on a starter checklist).\n\n## Props\n\n- `value` — the doc, controlled. A new `value` that differs from what the\n editor last sent through `onChange` replaces the document (loading\n another record). Pass back exactly what `onChange` gave you and nothing\n resets.\n- `defaultValue` — the starting doc, uncontrolled. With neither, the doc\n starts empty.\n- `onChange(doc)` — every edit, with the whole doc. Debounce a save\n yourself (doc-page does).\n- `editable` — `true` (default). `false` shows the doc read-only: no\n caret, no menus.\n- `placeholder` — shown while the doc is empty (and on the server while\n it is).\n- `minimal` — `false` (default). `true` for a comment box: no side menu\n (drag handles, +), no `/` menu, no emoji picker, smaller text, no\n padding of its own. Mentions, fields and the formatting toolbar still\n work.\n- `compact` — `false` (default). `true` keeps the full editor (menus,\n handles) in a short box: 4rem at least instead of 10rem. For a doc cut\n into many small sections (brief-doc).\n- `autoFocus` — put the caret in it when it mounts.\n- `onSubmit(doc)` — called on Cmd/Ctrl+Enter (and not passed to the\n editor). Post a comment, send a reply. Clear the box yourself through\n the handle (below).\n- `onKeyDown(event)` — every other key, before the editor sees it\n (`event.preventDefault()` stops it).\n- `mentions` — `async (query) => [{ id, label, description? }]`: turns on\n `@`. Called as the person types after `@` (`query` is the text after\n it, `\"\"` at first); return at most 20. A throw shows nothing. The chosen\n person goes in as a mention chip.\n- `fields` — `[{ key, label }]`: turns on merge fields. `{` opens a menu of\n them, filtered as you type; the `/` menu lists them under \"Fields\".\n Each goes in as a field chip showing its label.\n- `slashItems` — more entries for the `/` menu: `[{ title, onSelect(handle),\n subtext?, aliases?, group?, icon? }]`. `onSelect` gets the handle\n (below); `group` puts it under a heading (\"Other\" when left out);\n `icon` is a kit icon name. BlockNote's own entries come first.\n- `uploadFile` — `async (file) => url`: lets an image block upload a\n file (without it, an image is added by URL).\n- `editorRef` — a ref (`useDocEditor()`) that receives the handle.\n- `label` — the accessible name of the editing area (\"Meeting notes\").\n- `className` — layout around it (width, margin, min-height). Never colour.\n\nThe handle (`editorRef.current`, and what a slash item's `onSelect` gets):\n\n- `focus()` · `clear()` · `isEmpty()` · `getDoc()` · `setDoc(doc)`\n- `insertText(text)` · `insertField(key, label?)` · `insertMention({ id, label })`\n — at the caret.\n- `editor` — the BlockNote editor itself, for anything else\n (`editor.insertBlocks(…)`, `editor.getTextCursorPosition()`). Reaching\n for it ties you to BlockNote's API; prefer the methods above.\n\nThe helpers (import them from the block; they take a doc and never throw\non a partial one):\n\n- `emptyDoc()` — `[{ type: \"paragraph\", content: [] }]`.\n- `isDocEmpty(doc)` — no text, chip or non-paragraph block. Use it to\n disable Post.\n- `docToText(doc, { fields? })` — plain text: one line per block (an empty\n paragraph is an empty line), children indented, list markers kept\n (`- `, `1. `, `[ ] `/`[x] `), a mention as `@Label`, a field as its value\n or `{key}`. `fields` is `{ key: value }` or `(key, props) => value`; an\n empty value leaves `{key}` in place.\n- `docToMarkdown(doc, { fields? })` — GitHub markdown: headings, nested\n lists, `- [ ]` checklists, quotes, fenced code, tables, images, dividers,\n bold/italic/strike/code, links; mentions and fields as in `docToText`.\n Lossy where markdown has no word: underline, colours, alignment.\n- `docChecklist(doc)` — `[{ id, text, checked, depth, mentions: [{ id,\n label }] }]`, every checklist item in order, nested ones too. Read an\n owner off its mentions.\n- `setChecklistItem(doc, id, checked)` — a new doc with that item ticked\n or not (for a side list of action items).\n- `docFields(doc)` — `[{ key, label, count }]`, the merge fields the doc\n uses, in order of first use. Compare with a recipient's values to warn\n of a missing one.\n- `docMentions(doc)` — `[{ id, label, count }]`, the people it mentions.\n\n## Example\n\n```jsx\n// components/DealNotes.jsx — client\n\"use client\";\nimport { useState } from \"react\";\nimport { updateRow } from \"../lib/actions\";\nimport { DocEditor } from \"./blocks/doc-editor\";\n\nexport default function DealNotes({ deal, team }) {\n const [doc, setDoc] = useState(deal.notes); // jsonb, may be null\n return (\n <DocEditor\n value={doc}\n onChange={(d) => {\n setDoc(d);\n updateRow({ source: \"public.deals\", id: deal.id, patch: { notes: d } }); // debounce this in a real app\n }}\n mentions={async (q) => team.filter((p) => p.name.toLowerCase().includes(q.toLowerCase())).map((p) => ({ id: p.id, label: p.name }))}\n placeholder=\"Notes on this deal…\"\n label=\"Deal notes\"\n />\n );\n}\n```\n\nA comment box that posts on Cmd/Ctrl+Enter and clears itself:\n\n```jsx\nconst box = useDocEditor();\nconst post = async (doc) => {\n if (isDocEmpty(doc)) return;\n const res = await insertRow({ source: \"public.comments\", values: { body: doc, body_text: docToText(doc) } });\n if (res.ok) box.current?.clear();\n};\n\n<DocEditor minimal editorRef={box} onSubmit={post} mentions={findPeople} placeholder=\"Write a comment…\" />\n```\n\nAn email body with merge fields, filled per recipient:\n\n```jsx\nconst FIELDS = [{ key: \"first_name\", label: \"First name\" }, { key: \"company\", label: \"Company\" }];\n\n<DocEditor value={body} onChange={setBody} fields={FIELDS} />\nconst text = docToText(body, { fields: contact }); // \"Hi Ana, …\"\nconst missing = docFields(body).filter((f) => !contact[f.key]); // warn before sending\n```\n\n## What it will not do\n\n- Collaborate live (several carets at once): that is BlockNote's Yjs\n mode, not wired here. Saves are last-write-wins.\n- Export to PDF or Word, multi-column layouts or AI writing: those are\n BlockNote's `xl-` packages (GPL / commercial), which never come here.\n- Colour text: the toolbar has no colour picker (the theme has no red or\n purple to offer). Coloured text pasted in is shown in status and chart\n tokens.\n- Upload files, audio or video: an image by URL, or through your\n `uploadFile`.\n- Render on the server as rich text: the server shows the plain text\n (`docToText`) until the editor mounts. For a read-only page that must\n be rich in the HTML, render `docToMarkdown` yourself.\n\nAn editor it cannot be is your own component in `components/` — not an\nedit to this block. Its files are fixed: `npm run check` refuses a change,\nand `makersclaw add doc-editor` restores them.\n\n## Credits\n\nBlockNote (https://github.com/TypeCellOS/BlockNote), MPL-2.0 — installed\nfrom npm as a dependency, not copied here. See THIRD-PARTY-NOTICES.md.\n"
57
+ }
58
+ ],
59
+ "categories": [
60
+ "editor",
61
+ "rich text",
62
+ "document",
63
+ "notion",
64
+ "blocknote",
65
+ "markdown",
66
+ "notes",
67
+ "wiki",
68
+ "mention",
69
+ "merge field",
70
+ "slash",
71
+ "comment",
72
+ "checklist",
73
+ "wysiwyg"
74
+ ],
75
+ "docs": "Installed to components/makersclaw/blocks/doc-editor. Import: import { DocEditor, useDocEditor, docToMarkdown, docToText, emptyDoc, isDocEmpty, docChecklist, setChecklistItem, docFields, docMentions } from \"@/components/makersclaw/blocks/doc-editor\". Props and an example: components/makersclaw/blocks/doc-editor/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
76
+ "meta": {
77
+ "makersclaw": {
78
+ "version": "0.3.1",
79
+ "useWhen": "A screen edits rich text: a wiki or doc page, notes on a record, a brief, meeting notes, an email body with merge fields, a comment field with mentions. The other text blocks (doc-page, record-notes, comment-box…) are built on it. Not for one line of plain text (the kit's Input) or a form of fields (record-form)."
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,61 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "doc-page",
4
+ "type": "registry:block",
5
+ "title": "DocPage",
6
+ "description": "A Notion-style page: an emoji icon (picked from a grid), a big plain title, and the rich-text editor under it (doc-editor: / menu, markdown shortcuts, @mentions). Edits autosave through an async onSave a moment after typing stops, with Saved / Saving… / Couldn't save + Retry in the top bar, Cmd/Ctrl+S to save now and unsent edits sent when the page is left. A read-only mode shows the same page with nothing to edit; slots for a breadcrumb, actions and a meta line.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.1/r/core.json",
9
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.1/r/doc-editor.json"
10
+ ],
11
+ "dependencies": [],
12
+ "files": [
13
+ {
14
+ "path": "registry/blocks/doc-page/index.js",
15
+ "type": "registry:component",
16
+ "target": "@components/makersclaw/blocks/doc-page/index.js",
17
+ "content": "// The DocPage block — `makersclaw add doc-page`. Fixed: compose it through\n// its props (README.md beside this file). A page it cannot be is your own\n// component in components/, built on the doc-editor block.\nexport { DocPage } from \"./doc-page\";\n"
18
+ },
19
+ {
20
+ "path": "registry/blocks/doc-page/doc-page.jsx",
21
+ "type": "registry:component",
22
+ "target": "@components/makersclaw/blocks/doc-page/doc-page.jsx",
23
+ "content": "\"use client\";\n\n// DocPage — one Notion-style page: an icon, a title, the rich text under\n// it (DocEditor), and a save state in the top bar. Edits save themselves\n// through `onSave` a moment after typing stops (useAutosave), and the bar\n// says \"Saved\", \"Saving…\" or \"Couldn't save\" with a Retry. Read-only\n// shows the same page with nothing to edit.\n//\n// The page reads `title`, `icon` and `doc` when it mounts and whenever\n// `id` changes (another page opened); in between it owns the draft, so a\n// parent re-rendering with stale props never throws away typing. Leaving\n// a page with an edit still unsent sends it on the way out.\nimport { useEffect, useId, useLayoutEffect, useRef, useState } from \"react\";\nimport { Button, Icon, Popover, Spinner, cx } from \"../../ui\";\nimport { DocEditor, emptyDoc, useDocEditor } from \"../doc-editor\";\nimport { useAutosave } from \"./autosave\";\n\nconst DEFAULT_ICONS = [\n \"📄\", \"📘\", \"📝\", \"📌\", \"📎\", \"🗂️\",\n \"💡\", \"🎯\", \"🚀\", \"📈\", \"🧭\", \"🛠️\",\n \"🤝\", \"👋\", \"📣\", \"🧪\", \"🔒\", \"⚙️\",\n \"🗓️\", \"✅\", \"❓\", \"⭐\", \"🌱\", \"🏁\",\n];\n\nconst WIDTHS = { normal: \"max-w-3xl\", wide: \"max-w-5xl\", full: \"max-w-none\" };\n\nexport function DocPage({ id, ...props }) {\n // A new id is a new page: remount, so the draft, the editor and the save\n // loop start from that page (the old one flushes on its way out).\n return <PageBody key={id ?? \"page\"} id={id} {...props} />;\n}\n\nfunction PageBody({\n id,\n title: initialTitle = \"\",\n icon: initialIcon = null,\n doc: initialDoc,\n onSave,\n onChange,\n readOnly = false,\n saveDelay = 1000,\n placeholder = \"Write something, or press / for blocks\",\n titlePlaceholder = \"Untitled\",\n icons = DEFAULT_ICONS,\n meta,\n breadcrumb,\n actions,\n width = \"normal\",\n autoFocus = false,\n mentions,\n fields,\n slashItems,\n uploadFile,\n className,\n}) {\n const [title, setTitle] = useState(initialTitle ?? \"\");\n const [icon, setIcon] = useState(initialIcon || null);\n // The page as last edited, read by every save (the editor's onChange and\n // the title's fire from different renders).\n const current = useRef({ id, title: initialTitle ?? \"\", icon: initialIcon || null, doc: Array.isArray(initialDoc) && initialDoc.length ? initialDoc : emptyDoc() });\n const editorRef = useDocEditor();\n const titleRef = useRef(null);\n const saving = useAutosave(onSave && !readOnly ? onSave : null, saveDelay);\n\n const edit = (patch) => {\n const page = { ...current.current, ...patch };\n current.current = page;\n onChange?.(page);\n saving.change(page);\n };\n\n // The title grows with its text (it wraps on a phone) and never holds a\n // line break: Enter moves into the page instead.\n useLayoutEffect(() => {\n const el = titleRef.current;\n if (!el) return;\n el.style.height = \"auto\";\n el.style.height = `${el.scrollHeight}px`;\n }, [title]);\n useEffect(() => {\n if (autoFocus && !readOnly) titleRef.current?.focus();\n }, [autoFocus, readOnly]);\n\n const onTitleKey = (e) => {\n if (e.key === \"Enter\" && !e.nativeEvent.isComposing) {\n e.preventDefault();\n editorRef.current?.focus();\n } else if (e.key === \"ArrowDown\" && e.currentTarget.selectionStart === e.currentTarget.value.length) {\n e.preventDefault();\n editorRef.current?.focus();\n }\n };\n\n // Cmd/Ctrl+S saves now instead of the browser's \"save page as\".\n const onKeyDown = (e) => {\n if (!onSave || readOnly) return;\n if ((e.metaKey || e.ctrlKey) && !e.altKey && e.key.toLowerCase() === \"s\") {\n e.preventDefault();\n saving.flush();\n }\n };\n\n const showBar = breadcrumb || actions || (onSave && !readOnly);\n\n return (\n // raw: a plain box that hears Cmd/Ctrl+S from the title and the editor\n // inside it; no control of its own.\n <article className={cx(\"group/page mx-auto w-full min-w-0\", WIDTHS[width] ?? WIDTHS.normal, className)} onKeyDown={onKeyDown}>\n {showBar ? (\n <div className=\"flex min-h-11 items-center gap-2\">\n <div className=\"min-w-0 flex-1\">{breadcrumb}</div>\n {onSave && !readOnly ? <SaveState status={saving.status} error={saving.error} onRetry={saving.flush} /> : null}\n {actions ? <div className=\"flex shrink-0 items-center gap-1\">{actions}</div> : null}\n </div>\n ) : null}\n\n <header className=\"pt-6 sm:ps-12 sm:pt-10\">\n {readOnly ? (\n icon ? (\n <div className=\"mb-2 text-5xl leading-none\" aria-hidden=\"true\">\n {icon}\n </div>\n ) : null\n ) : (\n <IconPicker icon={icon} icons={icons} onPick={(next) => {\n setIcon(next);\n edit({ icon: next });\n }} />\n )}\n\n {readOnly ? (\n <h1 className=\"text-3xl font-bold leading-tight tracking-tight text-foreground [overflow-wrap:anywhere] sm:text-4xl\">\n {title || <span className=\"text-muted-foreground\">{titlePlaceholder}</span>}\n </h1>\n ) : (\n // raw: a page title is heading-sized text that wraps with no box\n // around it; the kit's Input and Textarea are boxed form fields.\n <textarea\n ref={titleRef}\n rows={1}\n value={title}\n placeholder={titlePlaceholder}\n aria-label=\"Page title\"\n spellCheck\n onChange={(e) => {\n const next = e.target.value.replace(/\\r?\\n/g, \" \");\n setTitle(next);\n edit({ title: next });\n }}\n onKeyDown={onTitleKey}\n className=\"block w-full resize-none overflow-hidden border-0 bg-transparent p-0 text-3xl font-bold leading-tight tracking-tight text-foreground outline-none placeholder:text-muted-foreground/70 focus-visible:outline-none sm:text-4xl\"\n />\n )}\n {meta ? <div className=\"mt-2 text-sm text-muted-foreground\">{meta}</div> : null}\n </header>\n\n <div className=\"mt-4 pb-16\">\n <DocEditor\n defaultValue={initialDoc}\n onChange={(doc) => edit({ doc })}\n editable={!readOnly}\n placeholder={placeholder}\n editorRef={editorRef}\n label={title || titlePlaceholder}\n mentions={mentions}\n fields={fields}\n slashItems={slashItems}\n uploadFile={uploadFile}\n className=\"min-h-64\"\n />\n </div>\n </article>\n );\n}\n\n/** \"Saved\" · \"Saving…\" · \"Couldn't save\" + Retry, announced politely. */\nfunction SaveState({ status, error, onRetry }) {\n return (\n <div role=\"status\" aria-live=\"polite\" className=\"flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground\">\n {status === \"error\" ? (\n <>\n <span className=\"flex items-center gap-1 text-destructive\" title={error || undefined}>\n <Icon name=\"warning\" size={14} />\n Couldn't save\n </span>\n <span className=\"sr-only\">{error}</span>\n <Button variant=\"link\" size=\"sm\" onClick={onRetry}>\n Retry\n </Button>\n </>\n ) : status === \"saved\" ? (\n <span className=\"flex items-center gap-1\">\n <Icon name=\"check\" size={14} />\n Saved\n </span>\n ) : (\n <span className=\"flex items-center gap-1.5\">\n <Spinner size=\"sm\" label={null} />\n Saving…\n </span>\n )}\n </div>\n );\n}\n\n/** The page's icon: a big emoji that opens a grid of others, or \"Add icon\"\n * while there is none. An empty `icons` list turns picking off. */\nfunction IconPicker({ icon, icons, onPick }) {\n const [open, setOpen] = useState(false);\n const gridId = useId();\n if (!icons?.length) {\n return icon ? (\n <div className=\"mb-2 text-5xl leading-none\" aria-hidden=\"true\">\n {icon}\n </div>\n ) : null;\n }\n const pick = (next) => {\n onPick(next);\n setOpen(false);\n };\n return (\n <div className={cx(icon ? \"mb-2\" : \"mb-1 -ms-3\")}>\n <Popover\n open={open}\n onOpenChange={setOpen}\n side=\"bottom\"\n align=\"start\"\n width=\"auto\"\n label=\"Page icon\"\n className=\"p-2\"\n trigger={(p) =>\n icon ? (\n // raw: the page icon is a 64px emoji you press; the kit's\n // Button squares are 36 and 44px and cx cannot resize them.\n <button\n {...p}\n type=\"button\"\n aria-label={`Page icon ${icon}, change it`}\n className=\"-ms-2 grid size-16 place-items-center rounded-md outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <span className=\"text-5xl leading-none\" aria-hidden=\"true\">\n {icon}\n </span>\n </button>\n ) : (\n <Button\n {...p}\n variant=\"quiet\"\n size=\"sm\"\n icon=\"plus\"\n className=\"text-muted-foreground opacity-0 transition-opacity group-hover/page:opacity-100 group-focus-within/page:opacity-100 focus-visible:opacity-100 [@media(hover:none)]:opacity-100\"\n >\n Add icon\n </Button>\n )\n }\n >\n <div id={gridId} role=\"group\" aria-label=\"Choose an icon\" className=\"grid grid-cols-6 gap-1\">\n {icons.map((e) => (\n <Button key={e} variant=\"quiet\" iconOnly aria-label={e} aria-pressed={e === icon} onClick={() => pick(e)} className=\"aria-pressed:bg-accent\">\n <span className=\"text-2xl leading-none\">{e}</span>\n </Button>\n ))}\n </div>\n {icon ? (\n <div className=\"mt-2 border-t border-border pt-2\">\n <Button variant=\"quiet\" size=\"sm\" icon=\"trash\" onClick={() => pick(null)} className=\"w-full\">\n Remove icon\n </Button>\n </div>\n ) : null}\n </Popover>\n </div>\n );\n}\n"
24
+ },
25
+ {
26
+ "path": "registry/blocks/doc-page/autosave.js",
27
+ "type": "registry:component",
28
+ "target": "@components/makersclaw/blocks/doc-page/autosave.js",
29
+ "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\n// useAutosave — the save loop behind DocPage, kept apart so it reads on\n// its own. Every edit bumps a version and (re)starts a timer; when the\n// timer fires the latest page goes out through `save`. One save at a\n// time: edits made while one is in flight are sent right after it. A\n// failure keeps the edits, says so, and the next edit (or `flush`) tries\n// again. Leaving (unmount) sends what is still unsent, without waiting.\n//\n// status \"saved\" · \"pending\" (an edit waits for the timer) ·\n// \"saving\" · \"error\"\n// error the failure's text while status is \"error\"\n// change(payload) an edit: payload is what `save` will receive\n// flush() save now (Cmd/Ctrl+S, Retry)\n\nexport function useAutosave(save, delay = 1000) {\n const [status, setStatus] = useState(\"saved\");\n const [error, setError] = useState(null);\n const saveRef = useRef(save);\n saveRef.current = save;\n const latest = useRef(null);\n const version = useRef(0);\n const saved = useRef(0);\n const busy = useRef(false);\n const timer = useRef(null);\n const alive = useRef(true);\n\n const run = useCallback(async () => {\n clearTimeout(timer.current);\n timer.current = null;\n if (busy.current || !saveRef.current) return;\n if (version.current === saved.current) {\n setStatus(\"saved\");\n return;\n }\n const v = version.current;\n busy.current = true;\n setStatus(\"saving\");\n let res;\n try {\n res = await saveRef.current(latest.current);\n } catch (e) {\n res = { ok: false, error: e?.message };\n }\n busy.current = false;\n if (!alive.current) return;\n if (res?.ok === false) {\n setError(res.error || \"Couldn't reach the server.\");\n setStatus(\"error\");\n return;\n }\n saved.current = Math.max(saved.current, v);\n setError(null);\n if (version.current !== saved.current) run();\n else setStatus(\"saved\");\n }, []);\n\n const change = useCallback(\n (payload) => {\n latest.current = payload;\n version.current += 1;\n if (!saveRef.current) return;\n setStatus((s) => (s === \"saving\" ? s : \"pending\"));\n clearTimeout(timer.current);\n timer.current = setTimeout(run, delay);\n },\n [delay, run],\n );\n\n const flush = useCallback(() => {\n run();\n }, [run]);\n\n // Unsent edits on the way out: warn before the tab closes, and send them\n // when the page goes (another page opened, the surface left).\n const unsent = status !== \"saved\";\n useEffect(() => {\n if (!unsent) return;\n const warn = (e) => {\n e.preventDefault();\n e.returnValue = \"\";\n };\n window.addEventListener(\"beforeunload\", warn);\n return () => window.removeEventListener(\"beforeunload\", warn);\n }, [unsent]);\n\n useEffect(() => {\n alive.current = true;\n return () => {\n alive.current = false;\n clearTimeout(timer.current);\n if (version.current !== saved.current && saveRef.current) {\n saved.current = version.current;\n Promise.resolve()\n .then(() => saveRef.current(latest.current))\n .catch(() => {});\n }\n };\n }, []);\n\n return { status, error, change, flush };\n}\n"
30
+ },
31
+ {
32
+ "path": "registry/blocks/doc-page/README.md",
33
+ "type": "registry:file",
34
+ "target": "@components/makersclaw/blocks/doc-page/README.md",
35
+ "content": "# DocPage\n\nOne Notion-style page: an emoji icon, a big title, and the rich text\nunder it. The text is the `doc-editor` block (type `/` for headings,\nlists, checklists, tables; `@` mentions a person). Edits save themselves\nthrough `onSave` a moment after typing stops; the top bar says **Saved**,\n**Saving…** or **Couldn't save** with a Retry. Cmd/Ctrl+S saves at once.\nRead-only shows the same page with nothing to edit. It is the wiki page:\n`knowledge-base` puts a page tree beside it.\n\n```jsx\nimport { DocPage } from \"../components/blocks/doc-page\"; // a Makersclaw app (\"./blocks/doc-page\" from components/)\nimport { DocPage } from \"@/components/makersclaw/blocks/doc-page\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component, and\nload the page in the server surface (README §4). `makersclaw add\ndoc-page` adds `doc-editor` too (and its BlockNote packages: run `npm\ninstall` after).\n\n## The data\n\nA page is `{ id, title, icon, doc }`:\n\n- `id` — the page's key.\n- `title` — plain text, one line (it wraps on screen).\n- `icon` — an emoji, or `null` for none.\n- `doc` — the body as BlockNote JSON (see the doc-editor README): store\n it as it comes, in a `jsonb` column.\n\n`onSave` receives the whole page, every time.\n\n## Props\n\n- `id` — which page this is. The page reads `title`, `icon` and `doc`\n when it mounts and again whenever `id` changes; in between it owns the\n draft, so a parent re-rendering with older props never throws away\n typing. To show another page, change `id`.\n- `title` — default `\"\"`.\n- `icon` — default `null`.\n- `doc` — default an empty doc.\n- `onSave(page)` — `async ({ id, title, icon, doc }) => ({ ok, error? })`.\n Called `saveDelay` ms after the last edit, one save at a time (edits\n made during a save go out right after it). `{ ok: false, error }` or a\n throw shows \"Couldn't save\" with `error` as the reason and a Retry; the\n edits stay on screen and the next edit tries again. Without `onSave`\n there is no save state (an editor you read from `onChange`).\n- `onChange(page)` — every edit, with the whole page (to update a title\n in a list beside it as it is typed). Not a save.\n- `readOnly` — `false` (default). `true`: the title is a heading, the\n icon a picture, the editor read-only; no save state.\n- `saveDelay` — ms of quiet before a save, default `1000`.\n- `placeholder` — the empty body's hint, default \"Write something, or\n press / for blocks\".\n- `titlePlaceholder` — default \"Untitled\".\n- `icons` — the emoji offered by the icon picker (default 24 common\n ones). `[]` turns picking off (a given `icon` still shows).\n- `meta` — a node under the title: \"Edited by June · 2 days ago\".\n- `breadcrumb` — a node at the left of the top bar (where the page\n sits).\n- `actions` — a node at the right of the top bar: a kit `Menu` of page\n actions, a Share button.\n- `width` — `\"normal\"` (default, a reading column) · `\"wide\"` · `\"full\"`.\n- `autoFocus` — put the caret in the title when it mounts (a page just\n created).\n- `mentions`, `fields`, `slashItems`, `uploadFile` — passed to the\n editor; see the doc-editor README.\n- `className` — layout around it. Never colour.\n\nKeys: Enter or ↓ at the end of the title moves into the body; Cmd/Ctrl+S\nsaves now. A tab closed with an edit unsaved asks first; a page left\n(another `id`, the surface unmounted) sends its unsent edit on the way\nout without waiting for the answer.\n\n## Example\n\n```jsx\n// components/WikiPage.jsx — client\n\"use client\";\nimport { updateRow } from \"../lib/actions\";\nimport { DocPage } from \"./blocks/doc-page\";\n\nexport default function WikiPage({ page, team, canEdit }) {\n return (\n <DocPage\n id={page.id}\n title={page.title}\n icon={page.icon}\n doc={page.body}\n readOnly={!canEdit}\n meta={`Edited by ${page.edited_by_name}`}\n onSave={({ id, title, icon, doc }) =>\n updateRow({ source: \"public.wiki_pages\", id, patch: { title, icon, body: doc } })\n }\n mentions={async (q) =>\n team.filter((p) => p.name.toLowerCase().includes(q.toLowerCase())).map((p) => ({ id: p.id, label: p.name }))\n }\n />\n );\n}\n```\n\nFor search or a preview, keep a plain-text copy beside the doc:\n`docToText(doc)` from the doc-editor block, saved in the same `onSave`.\n\n## What it will not do\n\n- Hold more than one page, or a tree of them: that is `knowledge-base`.\n- Collaborate live: saves are last-write-wins, whole page at a time.\n- Keep an edit when the browser is closed without an answer: the tab\n asks before closing while an edit is unsaved; after that it is gone.\n- Cover images, page history, comments or sharing: put your own\n controls in `actions`.\n- Pick any emoji in the world: the grid is `icons`.\n\nA page it cannot be is your own component in `components/`, built on\nthe doc-editor block — not an edit to this block. Its files are fixed:\n`npm run check` refuses a change, and `makersclaw add doc-page` restores\nthem.\n"
36
+ }
37
+ ],
38
+ "categories": [
39
+ "page",
40
+ "document",
41
+ "doc",
42
+ "wiki",
43
+ "notion",
44
+ "editor",
45
+ "rich text",
46
+ "autosave",
47
+ "title",
48
+ "icon",
49
+ "emoji",
50
+ "read-only",
51
+ "handbook",
52
+ "notes"
53
+ ],
54
+ "docs": "Installed to components/makersclaw/blocks/doc-page. Import: import { DocPage } from \"@/components/makersclaw/blocks/doc-page\". Props and an example: components/makersclaw/blocks/doc-page/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
55
+ "meta": {
56
+ "makersclaw": {
57
+ "version": "0.3.1",
58
+ "useWhen": "A screen is one document someone reads and writes: a wiki or handbook page, a playbook, a policy, a project page, a one-pager. The knowledge-base block puts a page tree beside it. Not for a short field of rich text inside a form (doc-editor), notes on a record (record-notes) or a comment (comment-box)."
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,73 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "email-render",
4
+ "type": "registry:block",
5
+ "title": "EmailRender",
6
+ "description": "Email HTML from React (react-email) in the workspace theme: a layout with the 600px column and the inbox preview line, a header brand line, a footer with the address and unsubscribe link, buttons, headings and text, and DocEmail, which draws a doc-editor document as email markup (lists, quotes, code, tables, images, merge fields). Email cannot read CSS variables, so resolveEmailTheme reads the tokens where the editor sits and hands the email plain hex values; renderEmail returns { html, text }, in the browser for a preview and on the server for a send.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.1/r/core.json"
9
+ ],
10
+ "dependencies": [
11
+ "@react-email/components@1.0.12",
12
+ "@react-email/render@2.1.0"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "registry/blocks/email-render/index.js",
17
+ "type": "registry:component",
18
+ "target": "@components/makersclaw/blocks/email-render/index.js",
19
+ "content": "// The EmailRender block — `makersclaw add email-render`. Fixed: compose it\n// through its exports (README.md beside this file). The door to\n// react-email: themed email parts, a doc-editor document as email markup,\n// and renderEmail() → { html, text }. Server-safe: the same tree renders\n// in a browser preview and in a server send. Blocks built on this one\n// (newsletter-builder…) import it, never @react-email/* directly.\nexport { EmailLayout, EmailCard, EmailHeader, EmailFooter, EmailButton, EmailHeading, EmailText, EmailDivider, EMAIL_WIDTH } from \"./email\";\nexport { DocEmail, fieldText } from \"./doc-email\";\nexport { renderEmail, emailPlainText } from \"./render\";\nexport { EMAIL_TOKENS, EMAIL_FALLBACK, resolveEmailTheme, emailColor, emailFontStack, emailThemeKey, luminance, toHex } from \"./theme\";\n"
20
+ },
21
+ {
22
+ "path": "registry/blocks/email-render/email.jsx",
23
+ "type": "registry:component",
24
+ "target": "@components/makersclaw/blocks/email-render/email.jsx",
25
+ "content": "// The email parts: react-email's components with the workspace theme laid\n// on as concrete values. Server-safe (no hooks, no context): the same tree\n// renders in the browser for a preview and on the server for a send.\n//\n// Every part takes `theme` (resolveEmailTheme(root) in the browser, or\n// EMAIL_FALLBACK) and reads its colours from it. Styles are inline, which\n// is what email clients keep; widths are a 600px column, the size every\n// client draws well.\nimport { Body, Button, Column, Container, Head, Heading, Hr, Html, Img, Link, Preview, Row, Section, Text } from \"@react-email/components\";\nimport { EMAIL_FALLBACK, emailColor } from \"./theme\";\n\n/** The width of an email's column, in px. */\nexport const EMAIL_WIDTH = 600;\n\nconst themeOf = (theme) => theme ?? EMAIL_FALLBACK;\n\n/**\n * The whole email: <html>, the preview line inboxes show after the\n * subject, the page background and a centred 600px column.\n *\n * preview the preheader: up to ~110 characters shown in the inbox list\n * lang the document's language (\"en\")\n * title the <title> (some clients show it in a browser view)\n */\nexport function EmailLayout({ theme, preview, title, lang = \"en\", children }) {\n const t = themeOf(theme);\n return (\n <Html lang={lang} dir=\"ltr\">\n <Head>\n <meta name=\"color-scheme\" content={t.scheme === \"dark\" ? \"dark\" : \"light\"} />\n <meta name=\"supported-color-schemes\" content={t.scheme === \"dark\" ? \"dark\" : \"light\"} />\n {title ? <title>{title}</title> : null}\n </Head>\n {preview ? <Preview>{preview}</Preview> : null}\n <Body\n style={{\n margin: 0,\n padding: \"32px 12px\",\n backgroundColor: emailColor(t, \"background\"),\n color: emailColor(t, \"foreground\"),\n fontFamily: t.font,\n WebkitFontSmoothing: \"antialiased\",\n }}\n >\n <Container style={{ width: \"100%\", maxWidth: `${EMAIL_WIDTH}px`, margin: \"0 auto\" }}>{children}</Container>\n </Body>\n </Html>\n );\n}\n\n/** A raised panel inside the column: the card colour, a hairline, the\n * theme's corner. `padding` in px. */\nexport function EmailCard({ theme, padding = 32, children }) {\n const t = themeOf(theme);\n return (\n <Section\n style={{\n backgroundColor: emailColor(t, \"card\"),\n color: emailColor(t, \"card-foreground\"),\n border: `1px solid ${emailColor(t, \"border\")}`,\n borderRadius: t.radius,\n padding: `${padding}px`,\n }}\n >\n {children}\n </Section>\n );\n}\n\n/**\n * The brand line at the top: a logo or the name, linked to the site.\n * band \"none\" (default: on the page background) · \"primary\" · \"muted\"\n * · \"card\" — a filled strip; the words take the matching\n * foreground\n * align \"left\" (default) · \"center\"\n */\nexport function EmailHeader({ theme, name, logoUrl, logoAlt, logoHeight = 28, href, band = \"none\", align = \"left\", tagline }) {\n const t = themeOf(theme);\n const filled = band && band !== \"none\";\n const fill = filled ? emailColor(t, band) : undefined;\n const ink = filled ? emailColor(t, band === \"card\" ? \"card-foreground\" : `${band}-foreground`) : emailColor(t, \"foreground\");\n const mark = logoUrl ? (\n <Img src={logoUrl} alt={logoAlt || name || \"\"} height={logoHeight} style={{ display: align === \"center\" ? \"inline-block\" : \"block\", height: `${logoHeight}px`, width: \"auto\", border: 0 }} />\n ) : (\n <Text style={{ margin: 0, fontFamily: t.fontDisplay, fontSize: \"18px\", lineHeight: \"24px\", fontWeight: 700, letterSpacing: \"-0.01em\", color: ink }}>{name}</Text>\n );\n return (\n <Section\n style={{\n padding: filled ? \"18px 24px\" : \"0 4px 20px\",\n backgroundColor: fill,\n borderRadius: filled ? t.radius : undefined,\n marginBottom: filled ? \"16px\" : undefined,\n textAlign: align,\n }}\n >\n <Row>\n <Column style={{ textAlign: align }}>\n {href ? (\n <Link href={href} style={{ color: ink, textDecoration: \"none\" }}>\n {mark}\n </Link>\n ) : (\n mark\n )}\n {tagline ? (\n <Text style={{ margin: \"4px 0 0\", fontSize: \"13px\", lineHeight: \"18px\", color: filled ? ink : emailColor(t, \"muted-foreground\"), opacity: filled ? 0.8 : 1 }}>{tagline}</Text>\n ) : null}\n </Column>\n </Row>\n </Section>\n );\n}\n\n/**\n * The small print at the bottom: a line or two about why the reader gets\n * this, the sender's postal address (the law in most places wants it) and\n * the unsubscribe link.\n * note \"You get this because you signed up at acme.com.\"\n * address \"Acme, 12 Market St, Leeds LS1 4AB\"\n * unsubscribeUrl the address your sender fills in (\"{{unsubscribe_url}}\")\n * links [{ label, href }] — a row above the small print\n */\nexport function EmailFooter({ theme, note, address, unsubscribeUrl, unsubscribeLabel = \"Unsubscribe\", links = [], align = \"center\" }) {\n const t = themeOf(theme);\n const muted = emailColor(t, \"muted-foreground\");\n const small = { margin: \"0 0 6px\", fontSize: \"12px\", lineHeight: \"18px\", color: muted };\n return (\n <Section style={{ padding: \"24px 16px 8px\", textAlign: align }}>\n {links.length ? (\n <Text style={{ ...small, marginBottom: \"12px\", fontSize: \"13px\" }}>\n {links.map((l, i) => (\n <span key={`${l.href}-${i}`}>\n {i ? <span style={{ padding: \"0 8px\" }}>·</span> : null}\n <Link href={l.href} style={{ color: muted, textDecoration: \"underline\" }}>\n {l.label}\n </Link>\n </span>\n ))}\n </Text>\n ) : null}\n {note ? <Text style={small}>{note}</Text> : null}\n {address ? <Text style={small}>{address}</Text> : null}\n {unsubscribeUrl ? (\n <Text style={small}>\n <Link href={unsubscribeUrl} style={{ color: muted, textDecoration: \"underline\" }}>\n {unsubscribeLabel}\n </Link>\n </Text>\n ) : null}\n </Section>\n );\n}\n\n/** A button that works in every client (a padded link, no images).\n * tone \"primary\" (default) · \"secondary\" · \"foreground\". */\nexport function EmailButton({ theme, href, tone = \"primary\", align = \"left\", children }) {\n const t = themeOf(theme);\n const fill = emailColor(t, tone === \"foreground\" ? \"foreground\" : tone);\n const ink = emailColor(t, tone === \"foreground\" ? \"background\" : `${tone}-foreground`);\n return (\n <Section style={{ textAlign: align, margin: \"8px 0 24px\" }}>\n <Button\n href={href || \"#\"}\n style={{\n display: \"inline-block\",\n backgroundColor: fill,\n color: ink,\n borderRadius: t.radius,\n padding: \"12px 22px\",\n fontSize: \"15px\",\n lineHeight: \"20px\",\n fontWeight: 600,\n textDecoration: \"none\",\n }}\n >\n {children}\n </Button>\n </Section>\n );\n}\n\nconst HEADING_SIZES = { 1: [\"28px\", \"34px\"], 2: [\"22px\", \"28px\"], 3: [\"18px\", \"24px\"] };\n\n/** A heading in the display face. level 1 · 2 · 3. */\nexport function EmailHeading({ theme, level = 2, align, children }) {\n const t = themeOf(theme);\n const [size, line] = HEADING_SIZES[level] ?? HEADING_SIZES[2];\n return (\n <Heading\n as={`h${Math.min(3, Math.max(1, level))}`}\n style={{ margin: level === 1 ? \"0 0 16px\" : \"24px 0 12px\", fontFamily: t.fontDisplay, fontSize: size, lineHeight: line, fontWeight: 700, letterSpacing: \"-0.015em\", color: emailColor(t, \"card-foreground\"), textAlign: align }}\n >\n {children}\n </Heading>\n );\n}\n\n/** A paragraph. `muted` for secondary words, `size` \"md\" (16px) · \"sm\". */\nexport function EmailText({ theme, muted = false, size = \"md\", align, children }) {\n const t = themeOf(theme);\n return (\n <Text\n style={{\n margin: \"0 0 16px\",\n fontSize: size === \"sm\" ? \"14px\" : \"16px\",\n lineHeight: size === \"sm\" ? \"22px\" : \"26px\",\n color: emailColor(t, muted ? \"muted-foreground\" : \"card-foreground\"),\n textAlign: align,\n }}\n >\n {children}\n </Text>\n );\n}\n\n/** A hairline. */\nexport function EmailDivider({ theme }) {\n const t = themeOf(theme);\n return <Hr style={{ border: 0, borderTop: `1px solid ${emailColor(t, \"border\")}`, margin: \"24px 0\" }} />;\n}\n"
26
+ },
27
+ {
28
+ "path": "registry/blocks/email-render/doc-email.jsx",
29
+ "type": "registry:component",
30
+ "target": "@components/makersclaw/blocks/email-render/doc-email.jsx",
31
+ "content": "// DocEmail — a doc-editor document (BlockNote JSON) drawn as email markup.\n//\n// The doc is plain JSON, so this walks it directly; it does not import the\n// editor (a server send never loads BlockNote). What email can show, it\n// shows: headings, paragraphs, lists (nested), checklists, quotes, code,\n// tables, images, dividers; bold, italic, underline, strike, inline code,\n// links. A mention is @Label; a merge field is its value, or what\n// `fields` makes of its key (a tag your sender fills in, \"{{first_name}}\").\n// Text and highlight colours are dropped: an email wears the theme.\nimport { Img, Link, Section, Text } from \"@react-email/components\";\nimport { EMAIL_FALLBACK, emailColor } from \"./theme\";\n\nconst blocksOf = (doc) => (Array.isArray(doc) ? doc : []);\nconst inlineOf = (content) => (typeof content === \"string\" ? [{ type: \"text\", text: content, styles: {} }] : Array.isArray(content) ? content : []);\n\n/** A merge field's words: `fields` is { key: value } or (key, props) → value.\n * Nothing given leaves \"{key}\", as docToText does. */\nexport function fieldText(props, fields) {\n const key = props?.key ?? \"\";\n const v = typeof fields === \"function\" ? fields(key, props) : fields && typeof fields === \"object\" ? fields[key] : undefined;\n return v === undefined || v === null || v === \"\" ? `{${key}}` : String(v);\n}\n\nfunction Inline({ content, t, fields, linkColor }) {\n return inlineOf(content).map((c, i) => {\n if (!c) return null;\n if (c.type === \"link\") {\n return (\n <Link key={i} href={c.href ?? \"#\"} style={{ color: linkColor, textDecoration: \"underline\" }}>\n <Inline content={c.content} t={t} fields={fields} linkColor={linkColor} />\n </Link>\n );\n }\n if (c.type === \"mention\") return <span key={i}>{`@${c.props?.label ?? c.props?.id ?? \"\"}`}</span>;\n if (c.type === \"field\") return <span key={i}>{fieldText(c.props, fields)}</span>;\n if (c.type !== \"text\") return Array.isArray(c.content) ? <Inline key={i} content={c.content} t={t} fields={fields} linkColor={linkColor} /> : null;\n const s = c.styles ?? {};\n let node = c.text ?? \"\";\n if (s.code) {\n node = (\n <code style={{ fontFamily: t.fontMono, fontSize: \"0.9em\", backgroundColor: emailColor(t, \"muted\"), borderRadius: \"4px\", padding: \"1px 4px\" }}>{node}</code>\n );\n }\n if (s.bold) node = <strong>{node}</strong>;\n if (s.italic) node = <em>{node}</em>;\n if (s.underline) node = <u>{node}</u>;\n if (s.strike) node = <s>{node}</s>;\n return <span key={i}>{node}</span>;\n });\n}\n\nconst HEAD = {\n 1: { fontSize: \"28px\", lineHeight: \"34px\", margin: \"8px 0 16px\" },\n 2: { fontSize: \"22px\", lineHeight: \"28px\", margin: \"28px 0 12px\" },\n 3: { fontSize: \"18px\", lineHeight: \"24px\", margin: \"24px 0 8px\" },\n};\n\nconst LIST = new Set([\"bulletListItem\", \"numberedListItem\", \"checkListItem\", \"toggleListItem\"]);\n\n/** Consecutive list items of one kind become one list. */\nfunction runs(blocks) {\n const out = [];\n for (const b of blocksOf(blocks)) {\n if (!b || typeof b !== \"object\") continue;\n const kind = LIST.has(b.type) ? (b.type === \"numberedListItem\" ? \"ol\" : b.type === \"checkListItem\" ? \"check\" : \"ul\") : null;\n const last = out[out.length - 1];\n if (kind && last?.kind === kind) last.items.push(b);\n else out.push(kind ? { kind, items: [b] } : { kind: null, block: b });\n }\n return out;\n}\n\nfunction Blocks({ blocks, t, fields, depth = 0 }) {\n const text = emailColor(t, \"card-foreground\");\n const muted = emailColor(t, \"muted-foreground\");\n const link = emailColor(t, \"primary\");\n const para = { margin: \"0 0 16px\", fontSize: \"16px\", lineHeight: \"26px\", color: text };\n return runs(blocks).map((run, i) => {\n if (run.kind === \"ul\" || run.kind === \"ol\") {\n const Tag = run.kind;\n return (\n <Tag key={i} style={{ margin: depth ? \"4px 0 4px\" : \"0 0 16px\", paddingLeft: \"24px\", color: text }}>\n {run.items.map((b, j) => (\n <li key={b.id ?? j} style={{ margin: \"0 0 6px\", fontSize: \"16px\", lineHeight: \"26px\" }}>\n <Inline content={b.content} t={t} fields={fields} linkColor={link} />\n {b.children?.length ? <Blocks blocks={b.children} t={t} fields={fields} depth={depth + 1} /> : null}\n </li>\n ))}\n </Tag>\n );\n }\n if (run.kind === \"check\") {\n return (\n <Section key={i} style={{ margin: \"0 0 16px\" }}>\n {run.items.map((b, j) => (\n <Text key={b.id ?? j} style={{ ...para, margin: \"0 0 6px\", paddingLeft: `${depth * 24}px` }}>\n <span style={{ color: b.props?.checked ? link : muted }}>{b.props?.checked ? \"☑\\u00a0 \" : \"☐\\u00a0 \"}</span>\n <span style={b.props?.checked ? { color: muted, textDecoration: \"line-through\" } : undefined}>\n <Inline content={b.content} t={t} fields={fields} linkColor={link} />\n </span>\n </Text>\n ))}\n </Section>\n );\n }\n const b = run.block;\n const kids = b.children?.length ? <Blocks blocks={b.children} t={t} fields={fields} depth={depth + 1} /> : null;\n switch (b.type) {\n case \"heading\": {\n const level = Math.min(3, Math.max(1, Number(b.props?.level) || 2));\n const Tag = `h${level}`;\n return (\n <div key={i}>\n <Tag style={{ ...HEAD[level], fontFamily: t.fontDisplay, fontWeight: 700, letterSpacing: \"-0.015em\", color: text }}>\n <Inline content={b.content} t={t} fields={fields} linkColor={link} />\n </Tag>\n {kids}\n </div>\n );\n }\n case \"quote\":\n return (\n <Section key={i} style={{ margin: \"0 0 16px\", borderLeft: `3px solid ${link}`, padding: \"2px 0 2px 16px\" }}>\n <Text style={{ ...para, margin: 0, fontSize: \"17px\", lineHeight: \"28px\", fontStyle: \"italic\" }}>\n <Inline content={b.content} t={t} fields={fields} linkColor={link} />\n </Text>\n {kids}\n </Section>\n );\n case \"codeBlock\":\n return (\n <pre\n key={i}\n style={{\n margin: \"0 0 16px\",\n padding: \"14px 16px\",\n backgroundColor: emailColor(t, \"muted\"),\n color: emailColor(t, \"foreground\"),\n border: `1px solid ${emailColor(t, \"border\")}`,\n borderRadius: t.radius,\n fontFamily: t.fontMono,\n fontSize: \"13px\",\n lineHeight: \"20px\",\n whiteSpace: \"pre-wrap\",\n wordBreak: \"break-word\",\n }}\n >\n {inlineOf(b.content)\n .map((c) => c?.text ?? \"\")\n .join(\"\")}\n </pre>\n );\n case \"divider\":\n return <hr key={i} style={{ border: 0, borderTop: `1px solid ${emailColor(t, \"border\")}`, margin: \"28px 0\" }} />;\n case \"image\":\n if (!b.props?.url) return null;\n return (\n <Section key={i} style={{ margin: \"0 0 20px\" }}>\n <Img src={b.props.url} alt={b.props?.caption || b.props?.name || \"\"} width=\"100%\" style={{ display: \"block\", width: \"100%\", maxWidth: \"100%\", height: \"auto\", borderRadius: t.radius, border: 0 }} />\n {b.props?.caption ? <Text style={{ margin: \"8px 0 0\", fontSize: \"13px\", lineHeight: \"18px\", color: muted, textAlign: \"center\" }}>{b.props.caption}</Text> : null}\n </Section>\n );\n case \"table\": {\n const rows = b.content?.rows ?? [];\n if (!rows.length) return null;\n const cell = { padding: \"8px 10px\", borderBottom: `1px solid ${emailColor(t, \"border\")}`, fontSize: \"14px\", lineHeight: \"20px\", color: text, textAlign: \"left\", verticalAlign: \"top\" };\n return (\n <table key={i} role=\"presentation\" cellPadding=\"0\" cellSpacing=\"0\" width=\"100%\" style={{ margin: \"0 0 20px\", borderCollapse: \"collapse\", width: \"100%\" }}>\n <tbody>\n {rows.map((r, ri) => (\n <tr key={ri}>\n {(r.cells ?? []).map((c, ci) => {\n const Cell = ri === 0 ? \"th\" : \"td\";\n return (\n <Cell key={ci} style={{ ...cell, fontWeight: ri === 0 ? 600 : 400 }}>\n <Inline content={c?.content ?? c} t={t} fields={fields} linkColor={link} />\n </Cell>\n );\n })}\n </tr>\n ))}\n </tbody>\n </table>\n );\n }\n default: {\n const empty = !inlineOf(b.content).some((c) => c && (c.type !== \"text\" || (c.text ?? \"\") !== \"\"));\n return (\n <div key={i}>\n <Text style={empty ? { ...para, margin: \"0 0 8px\", lineHeight: \"8px\" } : para}>{empty ? \" \" : <Inline content={b.content} t={t} fields={fields} linkColor={link} />}</Text>\n {kids}\n </div>\n );\n }\n }\n });\n}\n\n/**\n * A doc as email markup, inside whatever frame you give it (EmailCard).\n * doc the doc-editor document (BlockNote JSON)\n * theme resolveEmailTheme(root) or EMAIL_FALLBACK\n * fields merge fields: { key: value } or (key, props) → value; a key\n * with no value stays \"{key}\"\n */\nexport function DocEmail({ doc, theme, fields }) {\n const t = theme ?? EMAIL_FALLBACK;\n return <Blocks blocks={doc} t={t} fields={fields} />;\n}\n\n"
32
+ },
33
+ {
34
+ "path": "registry/blocks/email-render/render.js",
35
+ "type": "registry:component",
36
+ "target": "@components/makersclaw/blocks/email-render/render.js",
37
+ "content": "// renderEmail — a react-email tree → the HTML and the plain text a sender\n// needs. The one door to @react-email/render; blocks built on this one\n// never import it.\n//\n// Works in the browser (a live preview) and on the server (a send). It is\n// async: react-email renders through React's streaming renderer, so an\n// image or a font can load before the HTML is final.\nimport { render, toPlainText } from \"@react-email/render\";\n\n// The plain-text part reads like the email: headings in their own case\n// (html-to-text shouts them by default) and tables as rows and columns.\nconst TEXT_OPTIONS = {\n selectors: [\n { selector: \"h1\", options: { uppercase: false } },\n { selector: \"h2\", options: { uppercase: false } },\n { selector: \"h3\", options: { uppercase: false } },\n { selector: \"table\", format: \"dataTable\" },\n ],\n};\n\n/**\n * `await renderEmail(<EmailLayout …/>)` →\n * { ok: true, html, text } html: a full <!DOCTYPE html> document\n * { ok: false, error } a render that threw, in words\n *\n * `pretty: true` indents the HTML (slower; for reading, not for sending).\n */\nexport async function renderEmail(element, { pretty = false } = {}) {\n try {\n const html = await render(element, { pretty });\n let text = \"\";\n try {\n text = toPlainText(html, TEXT_OPTIONS);\n } catch {\n text = \"\";\n }\n return { ok: true, html, text };\n } catch (e) {\n return { ok: false, error: e?.message ? `The email could not be drawn: ${e.message}` : \"The email could not be drawn.\" };\n }\n}\n\n/** The plain-text part for HTML you already have. */\nexport function emailPlainText(html) {\n try {\n return toPlainText(String(html ?? \"\"), TEXT_OPTIONS);\n } catch {\n return \"\";\n }\n}\n"
38
+ },
39
+ {
40
+ "path": "registry/blocks/email-render/theme.js",
41
+ "type": "registry:component",
42
+ "target": "@components/makersclaw/blocks/email-render/theme.js",
43
+ "content": "// The email's theme: the workspace tokens, resolved to concrete values.\n//\n// Why: an email is read in Gmail, Outlook and Apple Mail, which never see\n// the app's stylesheet and mostly ignore CSS variables. So an email cannot\n// say var(--color-primary) the way a page does. At render time we read\n// what each token IS where the editor sits (its light or dark scheme, any\n// [data-theme] scope) and hand the email plain hex values. The source of\n// truth stays the theme; the email carries a copy made when it is rendered.\n//\n// resolveEmailTheme(root) browser: read the tokens at `root`\n// EMAIL_FALLBACK a neutral light theme, for the server or a\n// test, when there is no page to read\n// emailColor(theme, name) a token name (\"primary\") → its hex\n//\n// A theme is plain data, so it can travel to the server with a send:\n// { colors: { background, foreground, primary, … }, font, fontDisplay,\n// fontMono, radius, scheme }\n\n/** The tokens an email may name, in the order a picker would show them. */\nexport const EMAIL_TOKENS = [\n \"background\",\n \"foreground\",\n \"card\",\n \"card-foreground\",\n \"primary\",\n \"primary-foreground\",\n \"secondary\",\n \"secondary-foreground\",\n \"muted\",\n \"muted-foreground\",\n \"accent\",\n \"accent-foreground\",\n \"border\",\n \"destructive\",\n \"success\",\n \"warning\",\n \"info\",\n];\n\n// Web-safe tails for each face: most clients only have these installed.\nconst SANS_TAIL = [\"-apple-system\", \"BlinkMacSystemFont\", \"Segoe UI\", \"Helvetica\", \"Arial\", \"sans-serif\"];\nconst MONO_TAIL = [\"SFMono-Regular\", \"Menlo\", \"Consolas\", \"monospace\"];\n\nconst byte = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, \"0\");\n\n/** [r, g, b] → \"#rrggbb\". */\nexport function toHex([r, g, b]) {\n return \"#\" + byte(r) + byte(g) + byte(b);\n}\n\nconst grey = (v) => toHex([v, v, v]);\n\n/** A neutral light theme: what an email wears when no page can be read\n * (the server, a test). Greys only, so it never claims a brand. */\nexport const EMAIL_FALLBACK = Object.freeze({\n colors: Object.freeze({\n background: grey(244),\n foreground: grey(23),\n card: grey(255),\n \"card-foreground\": grey(23),\n primary: grey(23),\n \"primary-foreground\": grey(250),\n secondary: grey(240),\n \"secondary-foreground\": grey(23),\n muted: grey(245),\n \"muted-foreground\": grey(107),\n accent: grey(240),\n \"accent-foreground\": grey(23),\n border: grey(226),\n destructive: toHex([185, 28, 28]),\n success: toHex([21, 112, 58]),\n warning: toHex([154, 74, 6]),\n info: toHex([29, 78, 216]),\n }),\n font: SANS_TAIL.map(quoteFamily).join(\", \"),\n fontDisplay: SANS_TAIL.map(quoteFamily).join(\", \"),\n fontMono: MONO_TAIL.map(quoteFamily).join(\", \"),\n radius: \"8px\",\n scheme: \"light\",\n});\n\n/** A token name → its hex in `theme`, else the fallback's. A value that is\n * not a token name (\"\" or unknown) gives `fallback`. */\nexport function emailColor(theme, name, fallback = \"\") {\n const colors = theme?.colors ?? EMAIL_FALLBACK.colors;\n return colors[name] ?? EMAIL_FALLBACK.colors[name] ?? fallback;\n}\n\n/** Quote a family name that needs it (\"Segoe UI\" → '\"Segoe UI\"'). */\nfunction quoteFamily(name) {\n const n = String(name).trim().replace(/^[\"']|[\"']$/g, \"\");\n return /^[a-z-]+$/i.test(n) ? n : `\"${n}\"`;\n}\n\n/** A computed font-family → an email-safe stack: the theme's own families\n * first (a reader who has them sees them), next/font's hashed aliases\n * (\"__Inter_a1b2\") dropped, a web-safe tail added. */\nexport function emailFontStack(computed, tail = SANS_TAIL) {\n const seen = new Set();\n const out = [];\n for (const raw of String(computed ?? \"\").split(\",\")) {\n const name = raw.trim().replace(/^[\"']|[\"']$/g, \"\");\n if (!name || name.startsWith(\"__\") || /^var\\(/.test(name)) continue;\n if ([\"system-ui\", \"ui-sans-serif\", \"ui-monospace\"].includes(name)) continue;\n const key = name.toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(quoteFamily(name));\n }\n for (const name of tail) {\n const key = name.toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(quoteFamily(name));\n }\n return out.join(\", \");\n}\n\n/** Relative luminance of \"#rrggbb\" (0 black … 1 white). */\nexport function luminance(hex) {\n const m = /^#?([0-9a-f]{6})$/i.exec(String(hex ?? \"\"));\n if (!m) return 1;\n const n = parseInt(m[1], 16);\n const lin = (c) => {\n const s = c / 255;\n return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n };\n return 0.2126 * lin((n >> 16) & 255) + 0.7152 * lin((n >> 8) & 255) + 0.0722 * lin(n & 255);\n}\n\n/**\n * Browser only: the theme as it is painted at `root` (default: the body).\n *\n * Each colour goes through a hidden probe (`color: var(--color-x)`), so\n * light-dark(), color-mix() and a [data-theme] scope resolve exactly as\n * the app paints them. A 1×1 canvas then turns the computed colour into\n * bytes whatever syntax the browser wrote it in; a colour with alpha is\n * laid over the background first, because email has no transparency to\n * rely on. Faces and the corner radius are read the same way.\n */\nexport function resolveEmailTheme(root) {\n if (typeof document === \"undefined\") return EMAIL_FALLBACK;\n const host = root ?? document.body;\n if (!host) return EMAIL_FALLBACK;\n const probe = document.createElement(\"span\");\n probe.setAttribute(\"aria-hidden\", \"true\");\n probe.style.position = \"absolute\";\n probe.style.visibility = \"hidden\";\n probe.style.pointerEvents = \"none\";\n host.appendChild(probe);\n const canvas = document.createElement(\"canvas\");\n canvas.width = 1;\n canvas.height = 1;\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n const colors = {};\n const read = (token, under) => {\n probe.style.color = \"\";\n probe.style.color = `var(--color-${token})`;\n const computed = getComputedStyle(probe).color;\n if (!computed || !ctx) return null;\n ctx.clearRect(0, 0, 1, 1);\n if (under) {\n ctx.fillStyle = under;\n ctx.fillRect(0, 0, 1, 1);\n }\n ctx.fillStyle = \"transparent\";\n ctx.fillStyle = computed;\n ctx.fillRect(0, 0, 1, 1);\n const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;\n if (a === 0) return null;\n return toHex([r, g, b]);\n };\n let theme = EMAIL_FALLBACK;\n try {\n const bg = read(\"background\", null) ?? EMAIL_FALLBACK.colors.background;\n colors.background = bg;\n for (const token of EMAIL_TOKENS) {\n if (token === \"background\") continue;\n colors[token] = read(token, bg) ?? EMAIL_FALLBACK.colors[token];\n }\n probe.style.fontFamily = \"var(--theme-font-sans)\";\n const sans = getComputedStyle(probe).fontFamily;\n probe.style.fontFamily = \"var(--theme-font-display)\";\n const display = getComputedStyle(probe).fontFamily;\n probe.style.fontFamily = \"var(--theme-font-mono)\";\n const mono = getComputedStyle(probe).fontFamily;\n probe.style.borderRadius = \"var(--theme-radius)\";\n const radius = getComputedStyle(probe).borderTopLeftRadius;\n const px = Number.parseFloat(radius);\n theme = {\n colors,\n font: emailFontStack(sans),\n fontDisplay: emailFontStack(display || sans),\n fontMono: emailFontStack(mono, MONO_TAIL),\n radius: Number.isFinite(px) ? `${Math.min(24, Math.round(px))}px` : EMAIL_FALLBACK.radius,\n scheme: luminance(bg) < 0.35 ? \"dark\" : \"light\",\n };\n } finally {\n probe.remove();\n }\n return theme;\n}\n\n/** A stable fingerprint of a theme: equal strings, nothing to re-render. */\nexport function emailThemeKey(theme) {\n const t = theme ?? EMAIL_FALLBACK;\n return [\n ...Object.keys(t.colors ?? {})\n .sort()\n .map((k) => `${k}=${t.colors[k]}`),\n t.font,\n t.fontDisplay,\n t.radius,\n ].join(\";\");\n}\n"
44
+ },
45
+ {
46
+ "path": "registry/blocks/email-render/README.md",
47
+ "type": "registry:file",
48
+ "target": "@components/makersclaw/blocks/email-render/README.md",
49
+ "content": "# EmailRender\n\nEmail HTML from React, in the workspace theme. A layout with the 600px\ncolumn every client draws well and the preview line inboxes show after\nthe subject; a brand line for the top; a footer with the sender's address\nand the unsubscribe link; buttons, headings and text; and `DocEmail`,\nwhich draws a `doc-editor` document as email markup. `renderEmail` turns\nthe tree into `{ html, text }`: the HTML for the send and the plain-text\npart beside it.\n\nIt is [react-email](https://react.email) (MIT) underneath:\n`@react-email/components` for the parts and `@react-email/render` for the\nHTML. The blocks built on this one (`newsletter-builder` first) import it,\nnever `@react-email/*` directly.\n\n```jsx\nimport { EmailLayout, EmailCard, DocEmail, renderEmail, resolveEmailTheme } from \"../components/blocks/email-render\"; // a Makersclaw app (\"./blocks/email-render\" from components/)\nimport { EmailLayout, EmailCard, DocEmail, renderEmail, resolveEmailTheme } from \"@/components/makersclaw/blocks/email-render\"; // a shadcn project\n```\n\nEverything here is server-safe (no hooks, no context), except\n`resolveEmailTheme`, which reads the page. `makersclaw add email-render`\nadds `@react-email/components` and `@react-email/render` (pinned, exact)\nto package.json: run `npm install` after.\n\n## Email cannot read the theme\n\n**An email never sees the app's stylesheet, and most clients ignore CSS\nvariables. So an email carries concrete colours, copied from the theme\nwhen it is rendered.**\n\n- In the browser, `resolveEmailTheme(root)` reads every token where\n `root` sits (its light or dark scheme, any `[data-theme]` scope) and\n returns plain hex values, the theme's typefaces with web-safe\n fallbacks, and its corner radius. A colour with transparency is laid\n over the background first: email has none to rely on.\n- On the server there is no page to read. Pass the theme the browser\n resolved (it is plain JSON; send it along with the email), or use\n `EMAIL_FALLBACK`, a neutral light theme of greys.\n- The source of truth stays the theme. Re-render to pick up a change.\n\nThe theme object:\n\n```js\n{\n colors: { background: \"…\", foreground: \"…\", card: \"…\", \"card-foreground\": \"…\",\n primary: \"…\", \"primary-foreground\": \"…\", muted: \"…\", \"muted-foreground\": \"…\",\n border: \"…\", accent: \"…\", … }, // every name in EMAIL_TOKENS, as hex\n font: \"\\\"Schibsted Grotesk\\\", -apple-system, … sans-serif\",\n fontDisplay: \"…\", fontMono: \"…\",\n radius: \"8px\",\n scheme: \"light\" | \"dark\", // from the background; sets the color-scheme meta\n}\n```\n\n## Props\n\nEvery part takes `theme` (above). Leave it out and the part uses\n`EMAIL_FALLBACK`.\n\n- `EmailLayout` — the whole email. `preview` (the preheader: the line an\n inbox shows after the subject, up to ~110 characters), `title`, `lang`\n (`\"en\"`), `children`.\n- `EmailCard` — a raised panel in the column: card colour, hairline,\n the theme's corner. `padding` (px, `32`).\n- `EmailHeader` — the brand line. `name`, `logoUrl`, `logoAlt`,\n `logoHeight` (`28`), `href`, `tagline`, `band` (`\"none\"` ·\n `\"primary\"` · `\"muted\"` · `\"card\"`: a filled strip, the words in the\n matching foreground), `align` (`\"left\"` · `\"center\"`).\n- `EmailFooter` — the small print. `note` (why the reader gets this),\n `address` (the sender's postal address; the law in most places wants\n it), `unsubscribeUrl` (what your sender fills in, e.g.\n `\"{{unsubscribe_url}}\"`), `unsubscribeLabel` (`\"Unsubscribe\"`), `links`\n (`[{ label, href }]`), `align` (`\"center\"`).\n- `EmailButton` — a padded link that works in every client. `href`,\n `tone` (`\"primary\"` · `\"secondary\"` · `\"foreground\"`), `align`.\n- `EmailHeading` (`level` 1–3, `align`), `EmailText` (`muted`, `size`\n `\"md\"` · `\"sm\"`, `align`), `EmailDivider`.\n- `DocEmail` — a `doc-editor` document as email markup. `doc` (the\n BlockNote JSON), `fields` (merge fields: `{ key: value }` or\n `(key, props) => value`; a key with no value stays `{key}`). Headings,\n paragraphs, nested lists, checklists, quotes, code, tables, images with\n captions, dividers; bold, italic, underline, strike, inline code and\n links. A mention reads `@Label`. Text colours are dropped.\n\nThe functions:\n\n- `await renderEmail(element, { pretty? })` → `{ ok: true, html, text }`\n or `{ ok: false, error }` (in words, never a throw). `html` is a whole\n `<!DOCTYPE html>` document with inline styles; `text` is its plain-text\n part (headings in their own case, tables as rows).\n- `resolveEmailTheme(root?)` — browser only; on the server it returns\n `EMAIL_FALLBACK`.\n- `emailColor(theme, token)` — a token name to its hex.\n- `emailThemeKey(theme)` — a fingerprint: equal keys, nothing to\n re-render.\n- `emailPlainText(html)`, `fieldText(props, fields)`, `emailFontStack`,\n `luminance`, `toHex`, `EMAIL_TOKENS`, `EMAIL_WIDTH` (600).\n\n## Example\n\nA preview in the browser:\n\n```jsx\n\"use client\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { DocEmail, EmailButton, EmailCard, EmailFooter, EmailHeader, EmailLayout, renderEmail, resolveEmailTheme } from \"./blocks/email-render\";\n\nexport function InvitePreview({ doc, team }) {\n const box = useRef(null);\n const [html, setHtml] = useState(\"\");\n useEffect(() => {\n const theme = resolveEmailTheme(box.current);\n renderEmail(\n <EmailLayout theme={theme} preview={`${team.name} invited you to plan with them.`}>\n <EmailHeader theme={theme} name={team.name} />\n <EmailCard theme={theme}>\n <DocEmail doc={doc} theme={theme} fields={{ first_name: \"Ana\" }} />\n <EmailButton theme={theme} href={team.inviteUrl}>Accept the invite</EmailButton>\n </EmailCard>\n <EmailFooter theme={theme} address={team.address} />\n </EmailLayout>,\n ).then((r) => r.ok && setHtml(r.html));\n }, [doc, team]);\n return <div ref={box}><iframe title=\"Invite email\" srcDoc={html} sandbox=\"\" className=\"h-[600px] w-full rounded-md border border-border\" /></div>;\n}\n```\n\nThe same tree on the server, for the send (a server action):\n\n```js\nconst { ok, html, text, error } = await renderEmail(<Newsletter theme={savedTheme} … />);\nif (ok) await sender.send({ to, subject, html, text });\n```\n\n## What it will not do\n\n- Send email. It makes the HTML; your sender (Resend, Postmark, SES, the\n app's server) sends it.\n- Follow a later theme change inside an email already sent: the colours\n are a copy made at render time.\n- Dark-mode variants inside one email. It is drawn in the scheme it was\n resolved in, and says so in its `color-scheme` meta.\n- Tailwind classes in email. Styles are inline, which is what clients\n keep.\n\n## Credits\n\nreact-email (https://github.com/resend/react-email), MIT, © Resend —\ninstalled from npm as dependencies, not copied here. See\nTHIRD-PARTY-NOTICES.md.\n"
50
+ }
51
+ ],
52
+ "categories": [
53
+ "email",
54
+ "html email",
55
+ "react-email",
56
+ "newsletter",
57
+ "transactional email",
58
+ "render",
59
+ "template",
60
+ "inbox",
61
+ "preheader",
62
+ "unsubscribe",
63
+ "merge field",
64
+ "plain text"
65
+ ],
66
+ "docs": "Installed to components/makersclaw/blocks/email-render. Import: import { EmailLayout, EmailCard, EmailHeader, EmailFooter, EmailButton, EmailHeading, EmailText, EmailDivider, EMAIL_WIDTH, DocEmail, fieldText, renderEmail, emailPlainText, EMAIL_TOKENS, EMAIL_FALLBACK, resolveEmailTheme, emailColor, emailFontStack, emailThemeKey, luminance, toHex } from \"@/components/makersclaw/blocks/email-render\". Props and an example: components/makersclaw/blocks/email-render/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
67
+ "meta": {
68
+ "makersclaw": {
69
+ "version": "0.3.1",
70
+ "useWhen": "Code that must produce an email's HTML: a newsletter, a transactional email (receipt, invite, reset), a digest, a notification, a test send from an editor. newsletter-builder is built on it. Not for writing the email (doc-editor or email-composer), sending it (your sender or the app's server) or a web page (page-builder)."
71
+ }
72
+ }
73
+ }
package/r/faq.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "faq",
4
+ "type": "registry:block",
5
+ "title": "FAQ",
6
+ "description": "A marketing section of questions and answers: an accordion (one open at a time, or several), a grid of cards with optional icons, or a plain list with every answer shown; the title above or beside them, and an optional \"need more help?\" call to action. Loading and empty states.",
7
+ "registryDependencies": [
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.3.1/r/core.json"
9
+ ],
10
+ "dependencies": [],
11
+ "files": [
12
+ {
13
+ "path": "registry/blocks/faq/index.js",
14
+ "type": "registry:component",
15
+ "target": "@components/makersclaw/blocks/faq/index.js",
16
+ "content": "// The Faq block — `makersclaw add faq`. Fixed: compose it through its\n// props (README.md beside this file); a section it cannot be is your own\n// component in components/.\nexport { Faq } from \"./faq\";\n"
17
+ },
18
+ {
19
+ "path": "registry/blocks/faq/faq.jsx",
20
+ "type": "registry:component",
21
+ "target": "@components/makersclaw/blocks/faq/faq.jsx",
22
+ "content": "\"use client\";\n\nimport { Accordion, Button, Empty, Icon, Kicker, Skeleton, cx } from \"../../ui\";\n\n// Faq — a marketing section of questions and answers. Three layouts over\n// the same rows: an accordion (one open at a time, or several), a grid\n// of cards (optionally with an icon each), and a plain list with every\n// answer shown. The title sits above or beside them, and an optional\n// \"need more help?\" call to action closes the section.\n//\n// <Faq title=\"Questions, answered\" items={faqs}\n// cta={{ title: \"Still stuck?\", label: \"Email support\", href: \"/contact\" }} />\n//\n// README.md beside this file lists every prop.\n\nconst COLUMNS = {\n 1: \"grid-cols-1\",\n 2: \"grid-cols-1 sm:grid-cols-2\",\n 3: \"grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\",\n};\n\nconst keyOf = (item, i) => String(item.id ?? i);\n\nfunction Header({ kicker, title, description, center }) {\n if (!kicker && !title && !description) return null;\n return (\n <div className={cx(\"grid content-start gap-3\", center ? \"mx-auto max-w-2xl justify-items-center text-center\" : \"max-w-xl\")}>\n {kicker ? <Kicker>{kicker}</Kicker> : null}\n {title ? <h2 className=\"text-balance text-3xl font-semibold tracking-tight sm:text-4xl\">{title}</h2> : null}\n {description ? <p className=\"text-pretty text-base text-muted-foreground sm:text-lg\">{description}</p> : null}\n </div>\n );\n}\n\nfunction Answer({ children }) {\n return <div className=\"text-pretty text-base leading-relaxed text-muted-foreground [&_a]:text-primary [&_a]:underline [&_p+p]:mt-3\">{children}</div>;\n}\n\nfunction IconBadge({ name }) {\n if (!name) return null;\n return (\n <span className=\"grid size-12 shrink-0 place-items-center rounded-lg bg-primary text-primary-foreground\">\n <Icon name={name} size={22} />\n </span>\n );\n}\n\nfunction Cta({ cta, center }) {\n if (!cta) return null;\n return (\n <div className={cx(\"grid gap-3\", center ? \"justify-items-center text-center\" : \"justify-items-start\")}>\n {cta.title ? <h3 className=\"text-2xl font-semibold tracking-tight\">{cta.title}</h3> : null}\n {cta.body ? <p className=\"max-w-xl text-pretty text-base text-muted-foreground sm:text-lg\">{cta.body}</p> : null}\n {cta.label ? (\n <Button variant=\"primary\" href={cta.href} onClick={cta.onClick} icon={cta.icon} className=\"mt-2\">\n {cta.label}\n </Button>\n ) : null}\n </div>\n );\n}\n\nfunction Loading({ layout, columns }) {\n if (layout === \"grid\")\n return (\n <div aria-busy=\"true\" aria-label=\"Loading questions\" className={cx(\"grid gap-6\", COLUMNS[columns] ?? COLUMNS[3])}>\n {[0, 1, 2].map((i) => (\n <div key={i} className=\"grid gap-3 rounded-lg border border-border bg-card p-6\">\n <Skeleton className=\"w-3/4\" />\n <Skeleton className=\"w-full\" />\n <Skeleton className=\"w-5/6\" />\n </div>\n ))}\n </div>\n );\n return (\n <div aria-busy=\"true\" aria-label=\"Loading questions\" className=\"grid gap-5\">\n {[0, 1, 2, 3].map((i) => (\n <div key={i} className=\"grid gap-3 border-b border-border pb-5\">\n <Skeleton className={i % 2 ? \"w-1/2\" : \"w-2/3\"} />\n </div>\n ))}\n </div>\n );\n}\n\nfunction Grid({ items, columns, look }) {\n return (\n <div className={cx(\"grid gap-6\", COLUMNS[columns] ?? COLUMNS[3])}>\n {items.map((item, i) => (\n <article\n key={keyOf(item, i)}\n id={item.anchor}\n className={cx(\"grid min-w-0 content-start\", look === \"plain\" ? \"\" : \"rounded-lg border border-border bg-card text-card-foreground\")}\n >\n <div className={cx(\"grid gap-5 border-b border-border pb-4\", look === \"plain\" ? \"\" : \"px-6 pt-6\")}>\n <IconBadge name={item.icon} />\n <h3 className=\"text-pretty text-lg font-semibold leading-snug\">{item.question}</h3>\n </div>\n <div className={cx(\"pt-4\", look === \"plain\" ? \"\" : \"px-6 pb-6\")}>\n <Answer>{item.answer}</Answer>\n </div>\n </article>\n ))}\n </div>\n );\n}\n\nfunction List({ items }) {\n return (\n <dl className=\"grid gap-2\">\n {items.map((item, i) => (\n <div key={keyOf(item, i)} id={item.anchor} className=\"grid min-w-0\">\n <dt className=\"flex items-center gap-3 border-b border-border py-5 text-lg font-semibold\">\n {item.icon ? <Icon name={item.icon} className=\"shrink-0 text-muted-foreground\" /> : null}\n <span className=\"min-w-0 text-pretty\">{item.question}</span>\n </dt>\n <dd className=\"py-5\">\n <Answer>{item.answer}</Answer>\n </dd>\n </div>\n ))}\n </dl>\n );\n}\n\nfunction Folding({ items, type, defaultOpen, value, onOpenChange, label }) {\n const multiple = type === \"multiple\";\n return (\n <Accordion\n aria-label={label}\n type={multiple ? \"multiple\" : \"single\"}\n defaultValue={defaultOpen ?? (multiple ? [] : null)}\n value={value}\n onChange={onOpenChange}\n items={items.map((item, i) => ({\n key: keyOf(item, i),\n title: (\n <span className=\"flex items-center gap-3 py-2 text-base font-semibold sm:text-lg\">\n {item.icon ? <Icon name={item.icon} className=\"shrink-0 text-muted-foreground\" /> : null}\n <span className=\"min-w-0 text-pretty\">{item.question}</span>\n </span>\n ),\n content: <Answer>{item.answer}</Answer>,\n }))}\n />\n );\n}\n\nexport function Faq({\n items,\n layout = \"accordion\",\n kicker,\n title = \"Frequently asked questions\",\n description,\n titlePosition = \"top\",\n columns,\n look = \"card\",\n type = \"single\",\n defaultOpen,\n open,\n onOpenChange,\n cta,\n loading = false,\n empty,\n label,\n className,\n}) {\n const list = (items ?? []).filter((it) => it && it.question);\n const side = titlePosition === \"start\";\n const cols = columns ?? (side ? 2 : 3);\n const narrow = !side && layout !== \"grid\";\n\n let body;\n if (loading) body = <Loading layout={layout} columns={cols} />;\n else if (list.length === 0)\n body = (\n <Empty\n icon=\"message-circle\"\n title={empty?.title ?? \"No questions yet\"}\n body={empty?.body ?? \"Answers to what people ask most will show here.\"}\n />\n );\n else if (layout === \"grid\") body = <Grid items={list} columns={cols} look={look} />;\n else if (layout === \"list\") body = <List items={list} />;\n else\n body = (\n <Folding\n items={list}\n type={type}\n defaultOpen={defaultOpen}\n value={open}\n onOpenChange={onOpenChange}\n label={typeof title === \"string\" ? title : \"Questions\"}\n />\n );\n\n const header = <Header kicker={kicker} title={title} description={description} center={!side} />;\n return (\n <section aria-label={label ?? (typeof title === \"string\" ? title : \"Frequently asked questions\")} className={cx(\"px-4 py-16 sm:px-6 sm:py-20\", className)}>\n <div className={cx(\"mx-auto grid w-full gap-12 sm:gap-16\", narrow ? \"max-w-3xl\" : \"max-w-6xl\")}>\n {side ? (\n <div className=\"grid gap-10 lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)] lg:gap-12\">\n <div className=\"grid content-start gap-8\">\n {header}\n <Cta cta={cta} />\n </div>\n <div className=\"min-w-0\">{body}</div>\n </div>\n ) : (\n <>\n {header}\n <div className=\"min-w-0\">{body}</div>\n <Cta cta={cta} center />\n </>\n )}\n </div>\n </section>\n );\n}\n"
23
+ },
24
+ {
25
+ "path": "registry/blocks/faq/README.md",
26
+ "type": "registry:file",
27
+ "target": "@components/makersclaw/blocks/faq/README.md",
28
+ "content": "# FAQ\n\nA marketing section of questions and answers. Three layouts over the\nsame rows: an **accordion** built on the kit's `Accordion` (one open at a\ntime, or several; arrow keys move between questions, and find-in-page\nopens a closed one), a **grid** of cards with an optional icon each, and\na plain **list** with every answer shown. The title sits above the\nquestions or beside them, and an optional \"need more help?\" call to\naction closes the section. It is a full-width section: it pads itself\nand centres its content.\n\n```jsx\nimport { Faq } from \"../components/blocks/faq\"; // a Makersclaw app (\"./blocks/faq\" from components/)\nimport { Faq } from \"@/components/makersclaw/blocks/faq\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component.\n\n## The data\n\n`items` is the questions, in order:\n\n```js\n{\n question: \"How long is the free trial?\", // required; a row without one is skipped\n answer: \"Fourteen days, with every feature on.\", // a string, or a node (paragraphs, a link)\n id?: \"trial\", // a stable key; the accordion's open value\n icon?: \"clock\", // a name from the kit's Icon: a badge in the grid, a glyph otherwise\n anchor?: \"trial\", // grid and list: the element id, for /pricing#trial\n}\n```\n\n## Props\n\n- `items` — the questions, as above. Required.\n- `layout` — `accordion` (default) · `grid` · `list`.\n- `kicker`, `title`, `description` — the header. `title` defaults to\n \"Frequently asked questions\".\n- `titlePosition` — `top` (default, centred) · `start` (the header and\n the call to action in a column beside the questions from `lg` up).\n- `columns` — the grid: `1` · `2` · `3`. Default `3`, or `2` when the\n title is beside it. One column on a phone.\n- `look` — the grid's cards: `card` (default) · `plain` (no box, a rule\n under each question).\n- `type` — the accordion: `single` (default: opening one closes the\n rest) · `multiple`.\n- `defaultOpen` — the accordion's open question at first: an `id` (or\n the index as a string) when single, an array of them when multiple.\n- `open`, `onOpenChange(next)` — control the accordion yourself: `next`\n is an id or `null` when single, an array when multiple.\n- `cta` — `{ title?, body?, label?, href?, onClick?, icon? }`: \"Still\n have a question?\" and one primary button. Under the questions, or under\n the title when it is beside them.\n- `loading` — `true` draws placeholder rows or cards (`aria-busy`).\n- `empty` — `{ title?, body? }` when there are no `items`. Default \"No\n questions yet\".\n- `label` — what a screen reader calls the section. Default the title.\n- `className` — the section's spacing and width. Never colour.\n\n## Example\n\n```jsx\n// components/PricingFaq.jsx\n\"use client\";\nimport { Faq } from \"./blocks/faq\";\n\nconst FAQS = [\n { id: \"trial\", question: \"How long is the free trial?\", answer: \"Fourteen days, with every feature on.\" },\n { id: \"seats\", question: \"Do I pay per person?\", answer: \"Plans include five seats; each extra seat is $8 a month.\" },\n { id: \"cancel\", question: \"What happens if I cancel?\", answer: \"Your workspace stays readable for 90 days.\" },\n];\n\nexport default function PricingFaq() {\n return (\n <Faq\n items={FAQS}\n defaultOpen=\"trial\"\n description=\"The things people ask before they sign up.\"\n cta={{ title: \"Still have a question?\", label: \"Send us a note\", href: \"/contact\", icon: \"mail\" }}\n />\n );\n}\n```\n\n## What it will not do\n\n- Search or filter questions, or group them under categories: a help\n centre is a page of its own.\n- Render Markdown: pass a node for rich answers.\n- Add FAQ structured data (JSON-LD) to the page: that belongs in your\n page's metadata.\n\nA section it cannot be is your own component in `components/`, built\nfrom the kit — not an edit to this block. Its files are fixed: `npm run\ncheck` refuses a change, and `makersclaw add faq` restores them.\n\n## Credits\n\nLayouts ported from Creative Tim UI's FAQ blocks\n(https://github.com/creativetimofficial/ui, MIT), rewritten on the\nMakersclaw kit.\n"
29
+ }
30
+ ],
31
+ "categories": [
32
+ "faq",
33
+ "questions",
34
+ "answers",
35
+ "accordion",
36
+ "help",
37
+ "support",
38
+ "marketing",
39
+ "landing page",
40
+ "pricing",
41
+ "q&a"
42
+ ],
43
+ "docs": "Installed to components/makersclaw/blocks/faq. Import: import { Faq } from \"@/components/makersclaw/blocks/faq\". Props and an example: components/makersclaw/blocks/faq/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
44
+ "meta": {
45
+ "makersclaw": {
46
+ "version": "0.3.1",
47
+ "useWhen": "The questions section of a public page: a landing page, pricing, a help centre's front page, a product page. Not for a searchable help centre with articles and categories, or collapsible settings inside an app (use the kit's Accordion)."
48
+ }
49
+ }
50
+ }