makersclaw 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (478) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -1
  3. package/THIRD-PARTY-NOTICES.md +180 -0
  4. package/package.json +15 -8
  5. package/r/annotate.json +62 -0
  6. package/r/approval-request.json +52 -0
  7. package/r/audio-player.json +74 -0
  8. package/r/auth-form.json +55 -0
  9. package/r/board.json +55 -0
  10. package/r/calendar-view.json +105 -0
  11. package/r/cart.json +62 -0
  12. package/r/chart.json +83 -0
  13. package/r/chat.json +95 -0
  14. package/r/code-editor.json +94 -0
  15. package/r/comment-box.json +64 -0
  16. package/r/contact-section.json +53 -0
  17. package/r/core.json +341 -0
  18. package/r/csv-import.json +63 -0
  19. package/r/data-table.json +69 -0
  20. package/r/diagram.json +74 -0
  21. package/r/doc-editor.json +82 -0
  22. package/r/doc-page.json +61 -0
  23. package/r/email-render.json +73 -0
  24. package/r/faq.json +50 -0
  25. package/r/file-upload.json +54 -0
  26. package/r/flow-canvas.json +93 -0
  27. package/r/footer.json +52 -0
  28. package/r/gantt.json +70 -0
  29. package/r/job-panel.json +56 -0
  30. package/r/json-editor.json +66 -0
  31. package/r/map.json +89 -0
  32. package/r/menubar.json +52 -0
  33. package/r/mind-map.json +67 -0
  34. package/r/navigation-menu.json +52 -0
  35. package/r/order-detail.json +54 -0
  36. package/r/org-chart.json +66 -0
  37. package/r/page-builder.json +105 -0
  38. package/r/payment-methods.json +51 -0
  39. package/r/pdf-document.json +79 -0
  40. package/r/pdf-viewer.json +88 -0
  41. package/r/product-detail.json +76 -0
  42. package/r/product-grid.json +65 -0
  43. package/r/record-form.json +67 -0
  44. package/r/record-notes.json +55 -0
  45. package/r/registry.json +3612 -0
  46. package/r/search-dialog.json +51 -0
  47. package/r/settings-list.json +66 -0
  48. package/r/setup-checklist.json +51 -0
  49. package/r/share-dialog.json +52 -0
  50. package/r/sidebar.json +69 -0
  51. package/r/signature-pad.json +72 -0
  52. package/r/sketch-canvas.json +99 -0
  53. package/r/spreadsheet.json +90 -0
  54. package/r/stat-cards.json +66 -0
  55. package/r/task-list.json +63 -0
  56. package/r/task-model.json +98 -0
  57. package/r/testimonials.json +52 -0
  58. package/r/transaction-list.json +53 -0
  59. package/r/whiteboard.json +66 -0
  60. package/registry/blocks/annotate/README.md +179 -0
  61. package/registry/blocks/annotate/annotate-data.js +250 -0
  62. package/registry/blocks/annotate/annotate.jsx +646 -0
  63. package/registry/blocks/annotate/block.json +10 -0
  64. package/registry/blocks/annotate/demo.jsx +135 -0
  65. package/registry/blocks/annotate/index.js +15 -0
  66. package/registry/blocks/approval-request/README.md +116 -0
  67. package/registry/blocks/approval-request/approval-request.jsx +386 -0
  68. package/registry/blocks/approval-request/block.json +10 -0
  69. package/registry/blocks/approval-request/demo.jsx +127 -0
  70. package/registry/blocks/approval-request/index.js +4 -0
  71. package/registry/blocks/audio-player/README.md +167 -0
  72. package/registry/blocks/audio-player/audio-player.jsx +623 -0
  73. package/registry/blocks/audio-player/block.json +14 -0
  74. package/registry/blocks/audio-player/colors.js +109 -0
  75. package/registry/blocks/audio-player/demo.jsx +209 -0
  76. package/registry/blocks/audio-player/index.js +6 -0
  77. package/registry/blocks/audio-player/time.js +67 -0
  78. package/registry/blocks/audio-player/wavesurfer.js +9 -0
  79. package/registry/blocks/auth-form/README.md +126 -0
  80. package/registry/blocks/auth-form/auth-form.jsx +568 -0
  81. package/registry/blocks/auth-form/block.json +10 -0
  82. package/registry/blocks/auth-form/demo.jsx +131 -0
  83. package/registry/blocks/auth-form/index.js +3 -0
  84. package/registry/blocks/board/README.md +64 -13
  85. package/registry/blocks/board/block.json +2 -2
  86. package/registry/blocks/board/board.jsx +237 -94
  87. package/registry/blocks/board/demo.jsx +103 -24
  88. package/registry/blocks/calendar-view/README.md +180 -0
  89. package/registry/blocks/calendar-view/agenda.jsx +147 -0
  90. package/registry/blocks/calendar-view/block.json +76 -0
  91. package/registry/blocks/calendar-view/calendar-model.js +442 -0
  92. package/registry/blocks/calendar-view/calendar.jsx +627 -0
  93. package/registry/blocks/calendar-view/demo.jsx +195 -0
  94. package/registry/blocks/calendar-view/draft.jsx +49 -0
  95. package/registry/blocks/calendar-view/index.js +39 -0
  96. package/registry/blocks/calendar-view/month.jsx +346 -0
  97. package/registry/blocks/calendar-view/paint.js +69 -0
  98. package/registry/blocks/calendar-view/time-grid.jsx +527 -0
  99. package/registry/blocks/calendar-view/tray.jsx +87 -0
  100. package/registry/blocks/cart/README.md +146 -0
  101. package/registry/blocks/cart/block.json +10 -0
  102. package/registry/blocks/cart/cart.jsx +597 -0
  103. package/registry/blocks/cart/demo.jsx +98 -0
  104. package/registry/blocks/cart/index.js +4 -0
  105. package/registry/blocks/cart/money.js +28 -0
  106. package/registry/blocks/chart/README.md +316 -0
  107. package/registry/blocks/chart/block.json +10 -0
  108. package/registry/blocks/chart/cartesian.jsx +642 -0
  109. package/registry/blocks/chart/core.jsx +262 -0
  110. package/registry/blocks/chart/demo.jsx +357 -0
  111. package/registry/blocks/chart/heatmap.jsx +336 -0
  112. package/registry/blocks/chart/index.js +8 -0
  113. package/registry/blocks/chart/pie.jsx +245 -0
  114. package/registry/blocks/chart/sparkline.jsx +88 -0
  115. package/registry/blocks/chat/README.md +273 -0
  116. package/registry/blocks/chat/attachment.jsx +95 -0
  117. package/registry/blocks/chat/block.json +10 -0
  118. package/registry/blocks/chat/chat.jsx +132 -0
  119. package/registry/blocks/chat/composer.jsx +364 -0
  120. package/registry/blocks/chat/demo.jsx +578 -0
  121. package/registry/blocks/chat/header.jsx +34 -0
  122. package/registry/blocks/chat/index.js +9 -0
  123. package/registry/blocks/chat/message.jsx +450 -0
  124. package/registry/blocks/chat/scroller.jsx +247 -0
  125. package/registry/blocks/chat/text.jsx +104 -0
  126. package/registry/blocks/code-editor/README.md +210 -0
  127. package/registry/blocks/code-editor/block.json +22 -0
  128. package/registry/blocks/code-editor/code-editor.jsx +534 -0
  129. package/registry/blocks/code-editor/codemirror.js +38 -0
  130. package/registry/blocks/code-editor/demo.jsx +205 -0
  131. package/registry/blocks/code-editor/index.js +8 -0
  132. package/registry/blocks/code-editor/json.js +317 -0
  133. package/registry/blocks/code-editor/sql.js +136 -0
  134. package/registry/blocks/code-editor/text.js +101 -0
  135. package/registry/blocks/code-editor/theme.js +186 -0
  136. package/registry/blocks/comment-box/README.md +123 -0
  137. package/registry/blocks/comment-box/block.json +10 -0
  138. package/registry/blocks/comment-box/comment-body.jsx +217 -0
  139. package/registry/blocks/comment-box/comment-box.jsx +198 -0
  140. package/registry/blocks/comment-box/comment-time.jsx +77 -0
  141. package/registry/blocks/comment-box/demo.jsx +141 -0
  142. package/registry/blocks/comment-box/index.js +8 -0
  143. package/registry/blocks/contact-section/README.md +152 -0
  144. package/registry/blocks/contact-section/block.json +11 -0
  145. package/registry/blocks/contact-section/contact-section.jsx +509 -0
  146. package/registry/blocks/contact-section/demo.jsx +93 -0
  147. package/registry/blocks/contact-section/index.js +4 -0
  148. package/registry/blocks/csv-import/README.md +133 -0
  149. package/registry/blocks/csv-import/block.json +13 -0
  150. package/registry/blocks/csv-import/csv-import.jsx +479 -0
  151. package/registry/blocks/csv-import/demo.jsx +106 -0
  152. package/registry/blocks/csv-import/import.js +200 -0
  153. package/registry/blocks/csv-import/index.js +7 -0
  154. package/registry/blocks/data-table/README.md +386 -0
  155. package/registry/blocks/data-table/block.json +10 -0
  156. package/registry/blocks/data-table/data-table.jsx +1164 -0
  157. package/registry/blocks/data-table/demo.jsx +588 -0
  158. package/registry/blocks/data-table/index.js +5 -0
  159. package/registry/blocks/data-table/model.js +169 -0
  160. package/registry/blocks/diagram/README.md +108 -0
  161. package/registry/blocks/diagram/block.json +13 -0
  162. package/registry/blocks/diagram/demo.jsx +113 -0
  163. package/registry/blocks/diagram/diagram.jsx +481 -0
  164. package/registry/blocks/diagram/index.js +7 -0
  165. package/registry/blocks/diagram/mermaid.js +52 -0
  166. package/registry/blocks/diagram/palette.js +313 -0
  167. package/registry/blocks/diagram/source.js +98 -0
  168. package/registry/blocks/doc-editor/README.md +215 -0
  169. package/registry/blocks/doc-editor/block.json +15 -0
  170. package/registry/blocks/doc-editor/demo.jsx +225 -0
  171. package/registry/blocks/doc-editor/doc-editor.jsx +291 -0
  172. package/registry/blocks/doc-editor/doc.js +309 -0
  173. package/registry/blocks/doc-editor/index.js +7 -0
  174. package/registry/blocks/doc-editor/schema.jsx +56 -0
  175. package/registry/blocks/doc-editor/shadcn-classes.js +110 -0
  176. package/registry/blocks/doc-editor/theme.js +82 -0
  177. package/registry/blocks/doc-page/README.md +119 -0
  178. package/registry/blocks/doc-page/autosave.js +104 -0
  179. package/registry/blocks/doc-page/block.json +10 -0
  180. package/registry/blocks/doc-page/demo.jsx +137 -0
  181. package/registry/blocks/doc-page/doc-page.jsx +276 -0
  182. package/registry/blocks/doc-page/index.js +4 -0
  183. package/registry/blocks/email-render/README.md +151 -0
  184. package/registry/blocks/email-render/block.json +14 -0
  185. package/registry/blocks/email-render/demo.jsx +153 -0
  186. package/registry/blocks/email-render/doc-email.jsx +210 -0
  187. package/registry/blocks/email-render/email.jsx +221 -0
  188. package/registry/blocks/email-render/index.js +10 -0
  189. package/registry/blocks/email-render/render.js +50 -0
  190. package/registry/blocks/email-render/theme.js +215 -0
  191. package/registry/blocks/faq/README.md +101 -0
  192. package/registry/blocks/faq/block.json +11 -0
  193. package/registry/blocks/faq/demo.jsx +89 -0
  194. package/registry/blocks/faq/faq.jsx +220 -0
  195. package/registry/blocks/faq/index.js +4 -0
  196. package/registry/blocks/file-upload/README.md +138 -0
  197. package/registry/blocks/file-upload/block.json +10 -0
  198. package/registry/blocks/file-upload/demo.jsx +204 -0
  199. package/registry/blocks/file-upload/file-upload.jsx +635 -0
  200. package/registry/blocks/file-upload/index.js +4 -0
  201. package/registry/blocks/flow-canvas/README.md +420 -0
  202. package/registry/blocks/flow-canvas/block.json +13 -0
  203. package/registry/blocks/flow-canvas/board.js +819 -0
  204. package/registry/blocks/flow-canvas/chrome.jsx +649 -0
  205. package/registry/blocks/flow-canvas/context.jsx +35 -0
  206. package/registry/blocks/flow-canvas/demo.jsx +249 -0
  207. package/registry/blocks/flow-canvas/edges.jsx +107 -0
  208. package/registry/blocks/flow-canvas/flow-canvas.jsx +1329 -0
  209. package/registry/blocks/flow-canvas/index.js +41 -0
  210. package/registry/blocks/flow-canvas/nodes.jsx +392 -0
  211. package/registry/blocks/flow-canvas/theme.js +174 -0
  212. package/registry/blocks/footer/README.md +133 -0
  213. package/registry/blocks/footer/block.json +11 -0
  214. package/registry/blocks/footer/demo.jsx +119 -0
  215. package/registry/blocks/footer/footer.jsx +406 -0
  216. package/registry/blocks/footer/index.js +4 -0
  217. package/registry/blocks/gantt/README.md +220 -0
  218. package/registry/blocks/gantt/block.json +10 -0
  219. package/registry/blocks/gantt/demo.jsx +148 -0
  220. package/registry/blocks/gantt/gantt.jsx +923 -0
  221. package/registry/blocks/gantt/index.js +7 -0
  222. package/registry/blocks/gantt/rows.jsx +468 -0
  223. package/registry/blocks/gantt/schedule.js +278 -0
  224. package/registry/blocks/job-panel/README.md +86 -2
  225. package/registry/blocks/job-panel/block.json +3 -3
  226. package/registry/blocks/job-panel/demo.jsx +230 -3
  227. package/registry/blocks/job-panel/job-panel.jsx +169 -3
  228. package/registry/blocks/json-editor/README.md +133 -0
  229. package/registry/blocks/json-editor/block.json +10 -0
  230. package/registry/blocks/json-editor/demo.jsx +92 -0
  231. package/registry/blocks/json-editor/index.js +7 -0
  232. package/registry/blocks/json-editor/json-editor.jsx +305 -0
  233. package/registry/blocks/json-editor/json-tree.jsx +393 -0
  234. package/registry/blocks/json-editor/tree.js +225 -0
  235. package/registry/blocks/map/README.md +221 -0
  236. package/registry/blocks/map/block.json +14 -0
  237. package/registry/blocks/map/demo.jsx +201 -0
  238. package/registry/blocks/map/geo.js +198 -0
  239. package/registry/blocks/map/index.js +8 -0
  240. package/registry/blocks/map/list.jsx +128 -0
  241. package/registry/blocks/map/map-view.jsx +715 -0
  242. package/registry/blocks/map/maplibre.js +30 -0
  243. package/registry/blocks/map/marks.jsx +146 -0
  244. package/registry/blocks/map/theme.js +142 -0
  245. package/registry/blocks/menubar/README.md +109 -0
  246. package/registry/blocks/menubar/block.json +10 -0
  247. package/registry/blocks/menubar/demo.jsx +128 -0
  248. package/registry/blocks/menubar/index.js +4 -0
  249. package/registry/blocks/menubar/menubar.jsx +199 -0
  250. package/registry/blocks/mind-map/README.md +206 -0
  251. package/registry/blocks/mind-map/block.json +10 -0
  252. package/registry/blocks/mind-map/demo.jsx +103 -0
  253. package/registry/blocks/mind-map/index.js +31 -0
  254. package/registry/blocks/mind-map/mind-map.jsx +1125 -0
  255. package/registry/blocks/mind-map/topic.jsx +143 -0
  256. package/registry/blocks/mind-map/tree.js +645 -0
  257. package/registry/blocks/navigation-menu/README.md +124 -0
  258. package/registry/blocks/navigation-menu/block.json +10 -0
  259. package/registry/blocks/navigation-menu/demo.jsx +84 -0
  260. package/registry/blocks/navigation-menu/index.js +4 -0
  261. package/registry/blocks/navigation-menu/navigation-menu.jsx +448 -0
  262. package/registry/blocks/order-detail/README.md +109 -0
  263. package/registry/blocks/order-detail/block.json +10 -0
  264. package/registry/blocks/order-detail/demo.jsx +88 -0
  265. package/registry/blocks/order-detail/index.js +4 -0
  266. package/registry/blocks/order-detail/order-detail.jsx +354 -0
  267. package/registry/blocks/org-chart/README.md +145 -0
  268. package/registry/blocks/org-chart/block.json +10 -0
  269. package/registry/blocks/org-chart/demo.jsx +94 -0
  270. package/registry/blocks/org-chart/index.js +7 -0
  271. package/registry/blocks/org-chart/org-chart.jsx +631 -0
  272. package/registry/blocks/org-chart/org.js +245 -0
  273. package/registry/blocks/org-chart/person.jsx +96 -0
  274. package/registry/blocks/page-builder/README.md +257 -0
  275. package/registry/blocks/page-builder/block.json +14 -0
  276. package/registry/blocks/page-builder/demo.jsx +145 -0
  277. package/registry/blocks/page-builder/grapes.js +18 -0
  278. package/registry/blocks/page-builder/index.js +11 -0
  279. package/registry/blocks/page-builder/page-builder.jsx +704 -0
  280. package/registry/blocks/page-builder/page-view.jsx +33 -0
  281. package/registry/blocks/page-builder/page.js +118 -0
  282. package/registry/blocks/page-builder/panels.jsx +588 -0
  283. package/registry/blocks/page-builder/sections.js +209 -0
  284. package/registry/blocks/page-builder/theme.js +87 -0
  285. package/registry/blocks/page-builder/thumbs.jsx +189 -0
  286. package/registry/blocks/page-builder/tokens.js +329 -0
  287. package/registry/blocks/payment-methods/README.md +104 -0
  288. package/registry/blocks/payment-methods/block.json +10 -0
  289. package/registry/blocks/payment-methods/demo.jsx +71 -0
  290. package/registry/blocks/payment-methods/index.js +4 -0
  291. package/registry/blocks/payment-methods/payment-methods.jsx +302 -0
  292. package/registry/blocks/pdf-document/README.md +213 -0
  293. package/registry/blocks/pdf-document/block.json +13 -0
  294. package/registry/blocks/pdf-document/demo.jsx +136 -0
  295. package/registry/blocks/pdf-document/index.js +33 -0
  296. package/registry/blocks/pdf-document/money.js +83 -0
  297. package/registry/blocks/pdf-document/pdf-document.jsx +144 -0
  298. package/registry/blocks/pdf-document/primitives.jsx +271 -0
  299. package/registry/blocks/pdf-document/quote.jsx +179 -0
  300. package/registry/blocks/pdf-document/theme.js +193 -0
  301. package/registry/blocks/pdf-viewer/README.md +191 -0
  302. package/registry/blocks/pdf-viewer/block.json +14 -0
  303. package/registry/blocks/pdf-viewer/demo.jsx +252 -0
  304. package/registry/blocks/pdf-viewer/helpers.js +270 -0
  305. package/registry/blocks/pdf-viewer/index.js +8 -0
  306. package/registry/blocks/pdf-viewer/notes.jsx +195 -0
  307. package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1368 -0
  308. package/registry/blocks/pdf-viewer/pdfjs.js +37 -0
  309. package/registry/blocks/pdf-viewer/ssr-shim-end.js +6 -0
  310. package/registry/blocks/pdf-viewer/ssr-shim.js +44 -0
  311. package/registry/blocks/product-detail/README.md +217 -0
  312. package/registry/blocks/product-detail/block.json +10 -0
  313. package/registry/blocks/product-detail/demo.jsx +233 -0
  314. package/registry/blocks/product-detail/index.js +7 -0
  315. package/registry/blocks/product-detail/price.jsx +115 -0
  316. package/registry/blocks/product-detail/product-detail.jsx +348 -0
  317. package/registry/blocks/product-detail/product-media.jsx +102 -0
  318. package/registry/blocks/product-detail/product-options.jsx +144 -0
  319. package/registry/blocks/product-grid/README.md +162 -0
  320. package/registry/blocks/product-grid/block.json +10 -0
  321. package/registry/blocks/product-grid/demo.jsx +255 -0
  322. package/registry/blocks/product-grid/index.js +5 -0
  323. package/registry/blocks/product-grid/product-card.jsx +232 -0
  324. package/registry/blocks/product-grid/product-grid.jsx +235 -0
  325. package/registry/blocks/record-form/README.md +196 -0
  326. package/registry/blocks/record-form/block.json +10 -0
  327. package/registry/blocks/record-form/demo.jsx +525 -0
  328. package/registry/blocks/record-form/index.js +4 -0
  329. package/registry/blocks/record-form/record-fields.jsx +457 -0
  330. package/registry/blocks/record-form/record-form.jsx +415 -0
  331. package/registry/blocks/record-form/rules.js +81 -0
  332. package/registry/blocks/record-notes/README.md +139 -0
  333. package/registry/blocks/record-notes/block.json +10 -0
  334. package/registry/blocks/record-notes/demo.jsx +179 -0
  335. package/registry/blocks/record-notes/index.js +4 -0
  336. package/registry/blocks/record-notes/record-notes.jsx +266 -0
  337. package/registry/blocks/search-dialog/README.md +108 -0
  338. package/registry/blocks/search-dialog/block.json +10 -0
  339. package/registry/blocks/search-dialog/demo.jsx +116 -0
  340. package/registry/blocks/search-dialog/index.js +3 -0
  341. package/registry/blocks/search-dialog/search-dialog.jsx +235 -0
  342. package/registry/blocks/settings-list/README.md +171 -0
  343. package/registry/blocks/settings-list/block.json +10 -0
  344. package/registry/blocks/settings-list/demo.jsx +333 -0
  345. package/registry/blocks/settings-list/index.js +4 -0
  346. package/registry/blocks/settings-list/row-state.jsx +93 -0
  347. package/registry/blocks/settings-list/settings-list.jsx +349 -0
  348. package/registry/blocks/settings-list/settings-matrix.jsx +118 -0
  349. package/registry/blocks/setup-checklist/README.md +142 -0
  350. package/registry/blocks/setup-checklist/block.json +10 -0
  351. package/registry/blocks/setup-checklist/demo.jsx +159 -0
  352. package/registry/blocks/setup-checklist/index.js +4 -0
  353. package/registry/blocks/setup-checklist/setup-checklist.jsx +446 -0
  354. package/registry/blocks/share-dialog/README.md +113 -0
  355. package/registry/blocks/share-dialog/block.json +10 -0
  356. package/registry/blocks/share-dialog/demo.jsx +140 -0
  357. package/registry/blocks/share-dialog/index.js +3 -0
  358. package/registry/blocks/share-dialog/share-dialog.jsx +384 -0
  359. package/registry/blocks/sidebar/README.md +190 -0
  360. package/registry/blocks/sidebar/block.json +43 -0
  361. package/registry/blocks/sidebar/demo.jsx +577 -0
  362. package/registry/blocks/sidebar/index.js +4 -0
  363. package/registry/blocks/sidebar/sidebar-panel.jsx +149 -0
  364. package/registry/blocks/sidebar/sidebar.jsx +864 -0
  365. package/registry/blocks/signature-pad/README.md +137 -0
  366. package/registry/blocks/signature-pad/block.json +13 -0
  367. package/registry/blocks/signature-pad/colors.js +115 -0
  368. package/registry/blocks/signature-pad/demo.jsx +94 -0
  369. package/registry/blocks/signature-pad/index.js +6 -0
  370. package/registry/blocks/signature-pad/pad.js +7 -0
  371. package/registry/blocks/signature-pad/signature-pad.jsx +434 -0
  372. package/registry/blocks/signature-pad/signature.js +68 -0
  373. package/registry/blocks/sketch-canvas/README.md +280 -0
  374. package/registry/blocks/sketch-canvas/block.json +13 -0
  375. package/registry/blocks/sketch-canvas/demo.jsx +159 -0
  376. package/registry/blocks/sketch-canvas/excalidraw.js +40 -0
  377. package/registry/blocks/sketch-canvas/index.js +9 -0
  378. package/registry/blocks/sketch-canvas/palette.js +226 -0
  379. package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1188 -0
  380. package/registry/blocks/sketch-canvas/sketch.js +179 -0
  381. package/registry/blocks/sketch-canvas/ssr-shim-end.js +6 -0
  382. package/registry/blocks/sketch-canvas/ssr-shim.js +45 -0
  383. package/registry/blocks/sketch-canvas/stamps.js +104 -0
  384. package/registry/blocks/sketch-canvas/theme.js +131 -0
  385. package/registry/blocks/spreadsheet/README.md +307 -0
  386. package/registry/blocks/spreadsheet/block.json +13 -0
  387. package/registry/blocks/spreadsheet/cells.jsx +455 -0
  388. package/registry/blocks/spreadsheet/chrome.jsx +325 -0
  389. package/registry/blocks/spreadsheet/demo.jsx +177 -0
  390. package/registry/blocks/spreadsheet/grid.js +387 -0
  391. package/registry/blocks/spreadsheet/index.js +34 -0
  392. package/registry/blocks/spreadsheet/spreadsheet.jsx +1732 -0
  393. package/registry/blocks/spreadsheet/types.js +473 -0
  394. package/registry/blocks/stat-cards/README.md +173 -0
  395. package/registry/blocks/stat-cards/block.json +10 -0
  396. package/registry/blocks/stat-cards/demo.jsx +257 -0
  397. package/registry/blocks/stat-cards/index.js +4 -0
  398. package/registry/blocks/stat-cards/stat-cards.jsx +307 -0
  399. package/registry/blocks/stat-cards/usage.jsx +243 -0
  400. package/registry/blocks/task-list/README.md +176 -0
  401. package/registry/blocks/task-list/block.json +10 -0
  402. package/registry/blocks/task-list/demo.jsx +134 -0
  403. package/registry/blocks/task-list/index.js +5 -0
  404. package/registry/blocks/task-list/task-list.jsx +907 -0
  405. package/registry/blocks/task-model/README.md +455 -0
  406. package/registry/blocks/task-model/block.json +167 -0
  407. package/registry/blocks/task-model/changes.js +89 -0
  408. package/registry/blocks/task-model/dates.js +371 -0
  409. package/registry/blocks/task-model/demo.jsx +215 -0
  410. package/registry/blocks/task-model/dnd.jsx +240 -0
  411. package/registry/blocks/task-model/index.js +97 -0
  412. package/registry/blocks/task-model/model.js +919 -0
  413. package/registry/blocks/task-model/parts.jsx +449 -0
  414. package/registry/blocks/task-model/toolbar.jsx +271 -0
  415. package/registry/blocks/testimonials/README.md +139 -0
  416. package/registry/blocks/testimonials/block.json +11 -0
  417. package/registry/blocks/testimonials/demo.jsx +137 -0
  418. package/registry/blocks/testimonials/index.js +4 -0
  419. package/registry/blocks/testimonials/testimonials.jsx +430 -0
  420. package/registry/blocks/transaction-list/README.md +126 -0
  421. package/registry/blocks/transaction-list/block.json +10 -0
  422. package/registry/blocks/transaction-list/demo.jsx +79 -0
  423. package/registry/blocks/transaction-list/index.js +4 -0
  424. package/registry/blocks/transaction-list/transaction-list.jsx +383 -0
  425. package/registry/blocks/whiteboard/README.md +179 -0
  426. package/registry/blocks/whiteboard/block.json +10 -0
  427. package/registry/blocks/whiteboard/demo.jsx +169 -0
  428. package/registry/blocks/whiteboard/index.js +6 -0
  429. package/registry/blocks/whiteboard/templates.js +150 -0
  430. package/registry/blocks/whiteboard/whiteboard.jsx +721 -0
  431. package/src/install.mjs +18 -0
  432. package/src/shadcn.mjs +171 -0
  433. package/templates/next/files/README.md +145 -11
  434. package/templates/next/files/app/globals.css +55 -4
  435. package/templates/next/files/check/run.mjs +3 -3
  436. package/templates/next/files/components/parts/fixture.jsx +1 -1
  437. package/templates/next/files/components/parts/sample.js +15 -0
  438. package/templates/next/files/components/ui/avatar.jsx +141 -0
  439. package/templates/next/files/components/ui/breadcrumb.jsx +149 -0
  440. package/templates/next/files/components/ui/button-group.jsx +70 -0
  441. package/templates/next/files/components/ui/button.jsx +132 -12
  442. package/templates/next/files/components/ui/calendar.jsx +609 -0
  443. package/templates/next/files/components/ui/carousel.jsx +281 -0
  444. package/templates/next/files/components/ui/collapsible.jsx +208 -0
  445. package/templates/next/files/components/ui/combobox.jsx +608 -0
  446. package/templates/next/files/components/ui/command.jsx +577 -0
  447. package/templates/next/files/components/ui/context-menu.jsx +191 -0
  448. package/templates/next/files/components/ui/date-picker.jsx +360 -0
  449. package/templates/next/files/components/ui/distribution.jsx +117 -0
  450. package/templates/next/files/components/ui/feedback.jsx +143 -0
  451. package/templates/next/files/components/ui/field.jsx +115 -3
  452. package/templates/next/files/components/ui/file-input.jsx +234 -0
  453. package/templates/next/files/components/ui/icon.jsx +130 -8
  454. package/templates/next/files/components/ui/image.jsx +143 -0
  455. package/templates/next/files/components/ui/index.js +47 -2
  456. package/templates/next/files/components/ui/input-group.jsx +213 -0
  457. package/templates/next/files/components/ui/input-otp.jsx +170 -0
  458. package/templates/next/files/components/ui/item.jsx +206 -0
  459. package/templates/next/files/components/ui/menu.jsx +454 -82
  460. package/templates/next/files/components/ui/notice.jsx +126 -14
  461. package/templates/next/files/components/ui/pagination.jsx +156 -0
  462. package/templates/next/files/components/ui/popover.jsx +284 -0
  463. package/templates/next/files/components/ui/radio.jsx +193 -0
  464. package/templates/next/files/components/ui/rating.jsx +131 -0
  465. package/templates/next/files/components/ui/resizable.jsx +338 -0
  466. package/templates/next/files/components/ui/scroll-area.jsx +123 -0
  467. package/templates/next/files/components/ui/separator.jsx +119 -0
  468. package/templates/next/files/components/ui/sheet.jsx +96 -11
  469. package/templates/next/files/components/ui/slider.jsx +174 -0
  470. package/templates/next/files/components/ui/stat.jsx +574 -0
  471. package/templates/next/files/components/ui/steps.jsx +338 -0
  472. package/templates/next/files/components/ui/swatch.jsx +33 -0
  473. package/templates/next/files/components/ui/table.jsx +130 -0
  474. package/templates/next/files/components/ui/toast.jsx +440 -0
  475. package/templates/next/files/components/ui/toggle.jsx +195 -0
  476. package/templates/next/files/components/ui/tooltip.jsx +270 -0
  477. package/templates/next/files/lib/sample.js +4 -12
  478. package/templates/next/template.json +264 -8
@@ -0,0 +1,1164 @@
1
+ "use client";
2
+
3
+ import { isValidElement, useEffect, useId, useMemo, useState } from "react";
4
+ import { isSample } from "../../parts";
5
+ import {
6
+ Button,
7
+ Checkbox,
8
+ Combobox,
9
+ Empty,
10
+ Icon,
11
+ IconButton,
12
+ InputGroup,
13
+ Menu,
14
+ Notice,
15
+ Pagination,
16
+ Skeleton,
17
+ Table,
18
+ TableBody,
19
+ TableCell,
20
+ TableFooter,
21
+ TableHead,
22
+ TableHeader,
23
+ TableRow,
24
+ Tooltip,
25
+ cx,
26
+ } from "../../ui";
27
+ import { facetOptions, groupKeyOf, groupRows, groupValueOf, nextSort, processRows, useControllable } from "./model";
28
+
29
+ // DataTable — records as a table the founder can work: sort by a column
30
+ // (shift for more than one), search every column, narrow by facets with
31
+ // counts, pick which columns show, select rows and act on them together,
32
+ // act on one from its menu, open one with a click, move a page at a
33
+ // time. Client-side over `rows` by default; with `manual` the rows are
34
+ // already the page a server chose, and the table only reports what the
35
+ // founder asked for. Every piece of state (search, filters, sort,
36
+ // selection, page, page size, hidden columns) is controlled when its
37
+ // value prop is given and the table's own otherwise.
38
+ //
39
+ // The block owns the behaviour a hand-drawn table gets wrong: a sort that
40
+ // keeps blanks last, a select-all that means this page, a page that goes
41
+ // back to 1 when the filter changes, "no matches" told apart from "no
42
+ // rows", a bulk action that can fail and says so, examples that are
43
+ // never acted on, and a 390px window where the table scrolls inside
44
+ // itself (or turns into cards). The surface owns what a cell says
45
+ // (`cell`), what a row can do (`rowActions`, `bulkActions`) and how a row
46
+ // is painted (`rowTone`, `rowClassName`).
47
+ //
48
+ // <DataTable rows={invoices} columns={COLUMNS}
49
+ // facets={[{ key: "status", label: "Status" }]}
50
+ // bulkActions={[{ label: "Mark paid", onSelect: markPaid }]}
51
+ // rowActions={(r) => [{ label: "Open", onSelect: () => open(r) }]}
52
+ // onRowClick={open} />
53
+ //
54
+ // Opt-in on top: a row's first actions as buttons in the row
55
+ // (`inlineActions`), rows that open under themselves (`renderExpanded`), rows
56
+ // filed under group headings with a subtotal each (`groupBy`,
57
+ // `groupSummary`, `groupFooter`), an aligned totals row (`totals`), arrow
58
+ // keys over the rows (`keyboard`) and a first column that stays put while
59
+ // a wide table scrolls sideways (`stickyFirstColumn`).
60
+ //
61
+ // README.md beside this file lists every prop.
62
+
63
+ const TONES = {
64
+ success: "bg-success/10 [tr:hover>&]:bg-success/15",
65
+ info: "bg-info/10 [tr:hover>&]:bg-info/15",
66
+ warning: "bg-warning/10 [tr:hover>&]:bg-warning/15",
67
+ danger: "bg-destructive/10 [tr:hover>&]:bg-destructive/15",
68
+ highlighted: "bg-primary/5 [tr:hover>&]:bg-primary/10",
69
+ };
70
+ const CARD_TONES = {
71
+ success: "border-success/40 bg-success/10",
72
+ info: "border-info/40 bg-info/10",
73
+ warning: "border-warning/50 bg-warning/10",
74
+ danger: "border-destructive/50 bg-destructive/10",
75
+ highlighted: "border-primary/40 bg-primary/5",
76
+ };
77
+ // A highlighted row (a pinned or best row, not a status) carries a bar
78
+ // at its leading edge as well as the tint.
79
+ const BAR = "shadow-[inset_3px_0_0_var(--color-primary)]";
80
+ // A sticky cell is opaque (the columns scroll under it), so a tone is a
81
+ // flat gradient layered over the card instead of a see-through fill.
82
+ const STICKY_TONES = {
83
+ success: "bg-linear-to-r from-success/10 to-success/10 [tr:hover>&]:from-success/15 [tr:hover>&]:to-success/15",
84
+ info: "bg-linear-to-r from-info/10 to-info/10 [tr:hover>&]:from-info/15 [tr:hover>&]:to-info/15",
85
+ warning: "bg-linear-to-r from-warning/10 to-warning/10 [tr:hover>&]:from-warning/15 [tr:hover>&]:to-warning/15",
86
+ danger: "bg-linear-to-r from-destructive/10 to-destructive/10 [tr:hover>&]:from-destructive/15 [tr:hover>&]:to-destructive/15",
87
+ highlighted: "bg-linear-to-r from-primary/5 to-primary/5 [tr:hover>&]:from-primary/10 [tr:hover>&]:to-primary/10",
88
+ };
89
+ const STICKY_HOVER = "[tr:hover>&]:bg-linear-to-r [tr:hover>&]:from-muted/50 [tr:hover>&]:to-muted/50";
90
+ const STICKY_EDGE = "shadow-[inset_-1px_0_0_var(--color-border)]";
91
+ const BAR_EDGE = "shadow-[inset_3px_0_0_var(--color-primary),inset_-1px_0_0_var(--color-border)]";
92
+ // The leading columns' widths, so a sticky first column knows its offset.
93
+ const EXPANDER_W = 2.5; // rem — w-10
94
+ const SELECT_W = 3; // rem — w-12
95
+ const INTERACTIVE = "button, a, input, select, textarea, label, [role=menuitem], [role=menu], [popover]";
96
+ const NO_FILTERS = {};
97
+ const NONE = [];
98
+
99
+ function humanize(key) {
100
+ const text = String(key ?? "").replace(/[_-]+/g, " ").trim();
101
+ return text ? text.charAt(0).toUpperCase() + text.slice(1) : "";
102
+ }
103
+
104
+ function plural(n, one, many = `${one}s`) {
105
+ return `${n.toLocaleString()} ${n === 1 ? one : many}`;
106
+ }
107
+
108
+ // A per-column map ({ amount: "$4,200" }) rather than one node.
109
+ const isColumnMap = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v) && !isValidElement(v);
110
+ const domId = (s) => String(s).replace(/[^\w-]/g, "_");
111
+
112
+ export function DataTable({
113
+ rows,
114
+ columns,
115
+ idColumn = "id",
116
+ label = "Table",
117
+ // search
118
+ searchable = true,
119
+ searchPlaceholder = "Search…",
120
+ search,
121
+ defaultSearch = "",
122
+ onSearchChange,
123
+ // facets
124
+ facets,
125
+ filters,
126
+ defaultFilters = NO_FILTERS,
127
+ onFiltersChange,
128
+ // sort
129
+ sort,
130
+ defaultSort = NONE,
131
+ onSortChange,
132
+ multiSort = true,
133
+ // selection
134
+ selectable,
135
+ selection,
136
+ defaultSelection = NONE,
137
+ onSelectionChange,
138
+ bulkActions,
139
+ // columns
140
+ hiddenColumns,
141
+ defaultHiddenColumns,
142
+ onHiddenColumnsChange,
143
+ // pagination
144
+ page,
145
+ defaultPage = 1,
146
+ onPageChange,
147
+ pageSize,
148
+ defaultPageSize = 10,
149
+ onPageSizeChange,
150
+ pageSizes = [10, 25, 50, 100],
151
+ paginate = true,
152
+ // server mode
153
+ manual = false,
154
+ rowCount,
155
+ // rows
156
+ rowActions,
157
+ inlineActions,
158
+ onRowClick,
159
+ rowTone,
160
+ rowClassName,
161
+ // expandable rows
162
+ renderExpanded,
163
+ canExpand,
164
+ expanded,
165
+ defaultExpanded = NONE,
166
+ onExpandedChange,
167
+ // groups
168
+ groupBy,
169
+ groupOrder,
170
+ groupLabel,
171
+ groupSummary,
172
+ groupFooter,
173
+ // totals
174
+ totals,
175
+ totalsLabel = "Total",
176
+ // keyboard and layout
177
+ keyboard = false,
178
+ stickyFirstColumn = false,
179
+ // slots and states
180
+ toolbar,
181
+ footer,
182
+ loading = false,
183
+ error,
184
+ emptyTitle = "Nothing here yet",
185
+ emptyBody,
186
+ emptyAction,
187
+ emptyIcon = "inbox",
188
+ noun = "row",
189
+ mobile = "scroll",
190
+ cardTitle,
191
+ variant = "outline",
192
+ className,
193
+ }) {
194
+ const uid = useId().replace(/[^\w-]/g, "");
195
+ const all = rows ?? NONE;
196
+ const cols = useMemo(() => (columns ?? []).map((c) => ({ ...c, key: String(c.key), header: c.header ?? humanize(c.key) })), [columns]);
197
+ const facetList = useMemo(
198
+ () =>
199
+ (facets ?? []).map((f) => {
200
+ const col = cols.find((c) => c.key === String(f.key));
201
+ return { ...f, key: String(f.key), label: f.label ?? (typeof col?.header === "string" ? col.header : humanize(f.key)) };
202
+ }),
203
+ [facets, cols],
204
+ );
205
+ const keyOf = (row) => String(row?.[idColumn]);
206
+
207
+ const [query, setQuery] = useControllable(search, defaultSearch, onSearchChange);
208
+ const [chosen, setChosen] = useControllable(filters, defaultFilters, onFiltersChange);
209
+ const [order, setOrder] = useControllable(sort, defaultSort, onSortChange);
210
+ const [picked, setPicked] = useControllable(selection, defaultSelection, onSelectionChange);
211
+ const [hidden, setHidden] = useControllable(
212
+ hiddenColumns,
213
+ defaultHiddenColumns ?? cols.filter((c) => c.hidden).map((c) => c.key),
214
+ onHiddenColumnsChange,
215
+ );
216
+ const [current, setPage] = useControllable(page, defaultPage, onPageChange);
217
+ const [size, setSize] = useControllable(pageSize, defaultPageSize, onPageSizeChange);
218
+ const [bulkError, setBulkError] = useState(null);
219
+ const [bulkNote, setBulkNote] = useState(null);
220
+ const [running, setRunning] = useState(null);
221
+ const [announce, setAnnounce] = useState("");
222
+ const [openIds, setOpenIds] = useControllable(expanded, defaultExpanded, onExpandedChange);
223
+ const [activeId, setActiveId] = useState(null);
224
+ const [rowBusy, setRowBusy] = useState(null);
225
+ const [rowError, setRowError] = useState(null);
226
+
227
+ const canSelect = selectable ?? Boolean(bulkActions || onSelectionChange || selection);
228
+ const hiddenSet = useMemo(() => new Set((hidden ?? NONE).map(String)), [hidden]);
229
+ const shown = cols.filter((c) => !hiddenSet.has(c.key));
230
+ const pickedSet = useMemo(() => new Set((picked ?? NONE).map(String)), [picked]);
231
+ const filterState = chosen ?? NO_FILTERS;
232
+ const activeFilters = facetList.filter((f) => (filterState[f.key] ?? []).length > 0).length;
233
+ const narrowed = Boolean(String(query ?? "").trim()) || activeFilters > 0;
234
+
235
+ // Client mode: search, facets, sort, then a page. Manual: the rows are
236
+ // the page already, and `rowCount` is how many the server has.
237
+ const matched = useMemo(
238
+ () => (manual ? all : processRows(all, { query, filters: filterState, facets: facetList, sort: order, columns: cols })),
239
+ [manual, all, query, filterState, facetList, order, cols],
240
+ );
241
+ const total = manual ? rowCount ?? all.length : matched.length;
242
+ const per = paginate ? Math.max(1, Number(size) || 10) : Math.max(1, total);
243
+ const pageCount = Math.max(1, Math.ceil(total / per));
244
+ const pageNow = Math.min(Math.max(1, Number(current) || 1), pageCount);
245
+ // Groups: sort first, then file each row under its group, so a sort
246
+ // orders rows within their groups. Groups keep their place (`groupOrder`,
247
+ // else the order they first appear in `rows`) whatever the sort.
248
+ const grouped = groupBy !== undefined && groupBy !== null && groupBy !== false;
249
+ const groups = useMemo(() => {
250
+ if (!grouped) return null;
251
+ const seen = all.map((r) => groupKeyOf(groupValueOf(groupBy, r)));
252
+ const order = typeof groupOrder === "function" ? groupOrder : [...(Array.isArray(groupOrder) ? groupOrder : NONE), ...seen];
253
+ return groupRows(matched, groupBy, order);
254
+ }, [grouped, groupBy, groupOrder, matched, all]);
255
+ const arranged = useMemo(() => (groups ? groups.flatMap((g) => g.rows) : matched), [groups, matched]);
256
+ const visible = manual || !paginate ? arranged : arranged.slice((pageNow - 1) * per, pageNow * per);
257
+
258
+ // A page past the end (the filter narrowed the rows) goes back to the last.
259
+ useEffect(() => {
260
+ if (!loading && Number(current) > pageCount) setPage(pageCount);
261
+ }, [current, pageCount, loading]); // eslint-disable-line react-hooks/exhaustive-deps
262
+
263
+ useEffect(() => {
264
+ if (!loading && narrowed) setAnnounce(total === 0 ? "No matches" : `${plural(total, noun)} match`);
265
+ }, [total, narrowed, loading, noun]);
266
+
267
+ // ---- changes that move back to page 1 ---------------------------------
268
+
269
+ function changeQuery(v) {
270
+ setQuery(v);
271
+ if (pageNow !== 1) setPage(1);
272
+ }
273
+ function changeFacet(key, values) {
274
+ setChosen((f) => {
275
+ const next = { ...(f ?? {}) };
276
+ if (values.length) next[key] = values;
277
+ else delete next[key];
278
+ return next;
279
+ });
280
+ if (pageNow !== 1) setPage(1);
281
+ }
282
+ function clearFilters() {
283
+ setQuery("");
284
+ setChosen({});
285
+ if (pageNow !== 1) setPage(1);
286
+ }
287
+ function pressSort(e, key) {
288
+ setOrder((s) => nextSort(s, key, multiSort && e?.shiftKey));
289
+ if (pageNow !== 1) setPage(1);
290
+ }
291
+
292
+ // ---- selection ---------------------------------------------------------
293
+
294
+ const pageIds = visible.map(keyOf);
295
+ const pagePicked = pageIds.filter((id) => pickedSet.has(id)).length;
296
+ const allOnPage = pageIds.length > 0 && pagePicked === pageIds.length;
297
+ const someOnPage = pagePicked > 0 && !allOnPage;
298
+
299
+ function toggleRow(row) {
300
+ const id = keyOf(row);
301
+ setPicked((p) => {
302
+ const list = (p ?? []).map(String);
303
+ return list.includes(id) ? list.filter((x) => x !== id) : [...list, id];
304
+ });
305
+ }
306
+ function togglePage() {
307
+ setPicked((p) => {
308
+ const list = (p ?? []).map(String);
309
+ if (allOnPage) return list.filter((id) => !pageIds.includes(id));
310
+ return [...list, ...pageIds.filter((id) => !list.includes(id))];
311
+ });
312
+ }
313
+ function selectAllMatches() {
314
+ setPicked((p) => [...new Set([...(p ?? []).map(String), ...matched.map(keyOf)])]);
315
+ }
316
+ function clearSelection() {
317
+ setPicked([]);
318
+ setBulkNote(null);
319
+ }
320
+
321
+ // ---- expandable rows ---------------------------------------------------
322
+
323
+ const expanding = typeof renderExpanded === "function";
324
+ const openSet = useMemo(() => new Set((openIds ?? NONE).map(String)), [openIds]);
325
+ const expandable = (row) => expanding && (canExpand ? Boolean(canExpand(row)) : true);
326
+ function toggleOpen(id, force) {
327
+ setOpenIds((o) => {
328
+ const list = (o ?? []).map(String);
329
+ const on = force ?? !list.includes(id);
330
+ if (on === list.includes(id)) return list;
331
+ return on ? [...list, id] : list.filter((x) => x !== id);
332
+ });
333
+ }
334
+
335
+ const selectedRows = useMemo(() => all.filter((r) => pickedSet.has(keyOf(r))), [all, pickedSet]); // eslint-disable-line react-hooks/exhaustive-deps
336
+ const count = manual ? pickedSet.size : selectedRows.length;
337
+ const bulkItems = (typeof bulkActions === "function" ? bulkActions(selectedRows) : bulkActions ?? NONE).filter(Boolean);
338
+
339
+ async function runBulk(action) {
340
+ // Examples are shown, never acted on: they are not rows.
341
+ const real = selectedRows.filter((r) => !isSample(r));
342
+ setBulkError(null);
343
+ setBulkNote(null);
344
+ if (real.length === 0 && selectedRows.length > 0) {
345
+ setBulkNote("These are examples. Nothing was changed.");
346
+ return;
347
+ }
348
+ setRunning(action.label);
349
+ let result;
350
+ try {
351
+ result = await action.onSelect?.(real, manual ? [...pickedSet] : real.map(keyOf));
352
+ } catch (e) {
353
+ result = { ok: false, error: e?.message ?? String(e) };
354
+ }
355
+ setRunning(null);
356
+ if (result && result.ok === false) {
357
+ setBulkError(`${action.label} failed: ${result.error ?? "nothing was changed"}`);
358
+ return;
359
+ }
360
+ if (action.keepSelection !== true) setPicked([]);
361
+ setAnnounce(`${action.label}: done for ${plural(real.length, noun)}`);
362
+ }
363
+
364
+ // ---- drawing -----------------------------------------------------------
365
+
366
+ const toneOf = (row) => (rowTone ? rowTone(row) : null);
367
+ const hasActions = typeof rowActions === "function";
368
+ // Inline buttons widen the actions column past the menu's w-12.
369
+ const inlining = hasActions && Boolean(inlineActions);
370
+ // A click opens the row (`onRowClick`); with no onRowClick, a row that
371
+ // expands opens its details instead.
372
+ const activate = typeof onRowClick === "function" ? onRowClick : expanding ? (row) => expandable(row) && toggleOpen(keyOf(row)) : null;
373
+ const clickable = Boolean(activate);
374
+ const lead = (expanding ? 1 : 0) + (canSelect ? 1 : 0);
375
+ const width = lead + shown.length + (hasActions ? 1 : 0);
376
+ const titleOf = (row) => {
377
+ if (cardTitle) return cardTitle(row);
378
+ const first = shown[0];
379
+ return first ? (first.cell ? first.cell(row) : row?.[first.key]) : keyOf(row);
380
+ };
381
+ const nameOf = (row) => {
382
+ const first = shown[0];
383
+ const v = first ? row?.[first.key] : null;
384
+ return typeof v === "string" || typeof v === "number" ? String(v) : keyOf(row);
385
+ };
386
+
387
+ function onRowActivate(e, row) {
388
+ if (!clickable) return;
389
+ if (e.target.closest?.(INTERACTIVE)) return;
390
+ if (e.type === "keydown") {
391
+ if (e.key !== "Enter" && e.key !== " ") return;
392
+ if (e.target !== e.currentTarget) return;
393
+ // With the keyboard grid, Space selects (the grid's handler does it).
394
+ if (e.key === " " && keyboard && canSelect) return;
395
+ e.preventDefault();
396
+ }
397
+ activate(row);
398
+ }
399
+
400
+ // ---- keyboard grid -------------------------------------------------------
401
+ // One row is in the tab order at a time (roving tabindex): the last one
402
+ // focused, else the first. Arrows, Home and End move it; Space selects;
403
+ // Enter opens (as a click does); → and ← open and close a row's details;
404
+ // Esc clears the selection.
405
+
406
+ const byId = new Map(visible.map((r) => [keyOf(r), r]));
407
+ const tabStop = keyboard ? (activeId !== null && byId.has(activeId) ? activeId : visible.length ? keyOf(visible[0]) : null) : null;
408
+
409
+ function onGridKey(e) {
410
+ if (!keyboard) return;
411
+ if (e.target.closest?.("[role=menu], [popover], input:not([type=checkbox]), textarea, select")) return;
412
+ if (e.key === "Escape") {
413
+ if (canSelect && count > 0) {
414
+ e.preventDefault();
415
+ clearSelection();
416
+ setAnnounce("Selection cleared");
417
+ }
418
+ return;
419
+ }
420
+ const el = e.target.closest?.("[data-row]");
421
+ if (!el || el !== e.target) return;
422
+ const list = [...e.currentTarget.querySelectorAll("[data-row]")];
423
+ const at = list.indexOf(el);
424
+ const row = byId.get(el.dataset.row);
425
+ let to = null;
426
+ if (e.key === "ArrowDown") to = at + 1;
427
+ else if (e.key === "ArrowUp") to = at - 1;
428
+ else if (e.key === "Home") to = 0;
429
+ else if (e.key === "End") to = list.length - 1;
430
+ else if (e.key === " " && canSelect && row) {
431
+ e.preventDefault();
432
+ const on = !pickedSet.has(el.dataset.row);
433
+ toggleRow(row);
434
+ setAnnounce(`${nameOf(row)} ${on ? "selected" : "deselected"}`);
435
+ return;
436
+ } else if ((e.key === "ArrowRight" || e.key === "ArrowLeft") && row && expandable(row)) {
437
+ e.preventDefault();
438
+ toggleOpen(el.dataset.row, e.key === "ArrowRight");
439
+ return;
440
+ }
441
+ if (to === null) return;
442
+ e.preventDefault();
443
+ const next = list[Math.max(0, Math.min(list.length - 1, to))];
444
+ if (next) {
445
+ next.focus();
446
+ setActiveId(next.dataset.row);
447
+ }
448
+ }
449
+
450
+ // What a row (or card) needs to be clicked, focused and moved through.
451
+ function rowProps(row, id) {
452
+ return {
453
+ tabIndex: keyboard ? (id === tabStop ? 0 : -1) : clickable ? 0 : undefined,
454
+ onClick: clickable ? (e) => onRowActivate(e, row) : undefined,
455
+ onKeyDown: clickable ? (e) => onRowActivate(e, row) : undefined,
456
+ onFocus: keyboard ? (e) => e.target === e.currentTarget && setActiveId(id) : undefined,
457
+ };
458
+ }
459
+ // The ring is a layer over the whole row, above sticky cells (an outline
460
+ // on the row would be painted under them).
461
+ const focusRing =
462
+ "relative outline-hidden focus-visible:after:pointer-events-none focus-visible:after:absolute focus-visible:after:inset-0 focus-visible:after:z-[3] focus-visible:after:ring-2 focus-visible:after:ring-inset focus-visible:after:ring-ring";
463
+
464
+ // ---- sticky first column -------------------------------------------------
465
+
466
+ const stickyBg = variant === "plain" ? "bg-background" : "bg-card";
467
+ const offsets = { expander: 0, select: expanding ? EXPANDER_W : 0, first: (expanding ? EXPANDER_W : 0) + (canSelect ? SELECT_W : 0) };
468
+ // Classes and style for a cell in a leading slot ("expander" | "select" |
469
+ // "first"). `layer` is what the cell paints: "head", "foot", "body".
470
+ function stick(slot, layer, { tone, selected, bar } = {}) {
471
+ if (!stickyFirstColumn) return { className: undefined, style: undefined };
472
+ const edge = slot === "first";
473
+ const paint =
474
+ layer === "head"
475
+ ? stickyBg
476
+ : layer === "foot"
477
+ ? cx(stickyBg, "bg-linear-to-r from-muted/50 to-muted/50")
478
+ : layer === "sub"
479
+ ? "bg-muted"
480
+ : cx(selected ? "bg-muted" : stickyBg, tone && STICKY_TONES[tone] ? STICKY_TONES[tone] : !selected && STICKY_HOVER);
481
+ return {
482
+ className: cx("sticky", layer === "head" ? "z-[2]" : "z-[1]", paint, edge && (bar ? BAR_EDGE : STICKY_EDGE)),
483
+ style: { left: `${offsets[slot]}rem` },
484
+ };
485
+ }
486
+
487
+ function checkbox(row) {
488
+ return (
489
+ <Checkbox
490
+ checked={pickedSet.has(keyOf(row))}
491
+ onChange={() => toggleRow(row)}
492
+ aria-label={`Select ${nameOf(row)}`}
493
+ className="px-1"
494
+ />
495
+ );
496
+ }
497
+
498
+ // A row's actions, split: the ones drawn as buttons in the row
499
+ // (`inlineActions` of them from the front, or any with `inline: true`)
500
+ // and the rest, which stay in the "…" menu. Separators and labels stay
501
+ // in the menu, tidied so none leads, trails or doubles.
502
+ function splitActions(row) {
503
+ const items = (hasActions ? rowActions(row) ?? NONE : NONE).filter(Boolean);
504
+ let left = inlineActions === true ? Infinity : Math.max(0, Number(inlineActions) || 0);
505
+ const inline = [];
506
+ const menu = [];
507
+ for (const a of items) {
508
+ const plain = a.type === undefined || a.type === "item";
509
+ const pick = plain && a.inline !== false && (a.inline === true || left > 0);
510
+ if (pick) {
511
+ inline.push(a);
512
+ if (a.inline !== true) left--;
513
+ } else menu.push(a);
514
+ }
515
+ const tidy = menu.filter((a, i, all) => {
516
+ if (a.type !== "separator") return true;
517
+ const prev = all.slice(0, i).filter((x) => x.type !== "separator").length;
518
+ const next = all[i + 1];
519
+ return prev > 0 && next && next.type !== "separator";
520
+ });
521
+ return { inline, menu: inline.length ? tidy : items };
522
+ }
523
+
524
+ // An inline action may be async: its button spins, the row's other
525
+ // buttons wait, and { ok: false, error } (or a throw) is said above the
526
+ // table. The menu's items keep their own behaviour.
527
+ async function runRowAction(row, action) {
528
+ const id = keyOf(row);
529
+ setRowError(null);
530
+ setRowBusy({ id, label: action.label });
531
+ let result;
532
+ try {
533
+ result = await action.onSelect?.(row);
534
+ } catch (e) {
535
+ result = { ok: false, error: e?.message ?? String(e) };
536
+ }
537
+ setRowBusy(null);
538
+ if (result && result.ok === false) {
539
+ setRowError(`${action.label} failed for ${nameOf(row)}: ${result.error ?? "nothing was changed"}`);
540
+ return;
541
+ }
542
+ setAnnounce(`${action.label}: done for ${nameOf(row)}`);
543
+ }
544
+
545
+ function inlineButton(row, a, card) {
546
+ const id = keyOf(row);
547
+ const busy = rowBusy?.id === id;
548
+ const props = {
549
+ variant: a.tone === "danger" ? "danger" : "secondary",
550
+ loading: busy && rowBusy.label === a.label,
551
+ disabled: (busy && rowBusy.label !== a.label) || a.disabled,
552
+ onClick: () => runRowAction(row, a),
553
+ };
554
+ // On a card there is room for the words; in a row, the glyph and a tip.
555
+ if (card || !a.icon) {
556
+ return (
557
+ <Button key={a.label} {...props} size="md" icon={a.icon}>
558
+ {a.label}
559
+ </Button>
560
+ );
561
+ }
562
+ return (
563
+ <Tooltip key={a.label} label={a.label}>
564
+ <IconButton {...props} icon={a.icon} label={`${a.label}: ${nameOf(row)}`} size="md" />
565
+ </Tooltip>
566
+ );
567
+ }
568
+
569
+ function rowMenu(row, split = splitActions(row)) {
570
+ return <Menu items={split.menu} label={`Actions for ${nameOf(row)}`} size="md" />;
571
+ }
572
+
573
+ function actionsCell(row) {
574
+ const split = splitActions(row);
575
+ if (!split.inline.length) return rowMenu(row, split);
576
+ return (
577
+ <div className="flex items-center justify-end gap-1.5">
578
+ {split.inline.map((a) => inlineButton(row, a, false))}
579
+ {rowMenu(row, split)}
580
+ </div>
581
+ );
582
+ }
583
+
584
+ const empty = !loading && (manual ? total === 0 && !narrowed : all.length === 0) ? (
585
+ <Empty icon={emptyIcon} title={emptyTitle} body={emptyBody} action={emptyAction} />
586
+ ) : null;
587
+ const noMatches = !loading && !empty && visible.length === 0 ? (
588
+ <Empty
589
+ icon="search"
590
+ title="No matches"
591
+ body={narrowed ? "Nothing matches the search and filters." : `No ${noun}s on this page.`}
592
+ action={
593
+ narrowed ? (
594
+ <Button size="sm" icon="x" onClick={clearFilters}>
595
+ Clear filters
596
+ </Button>
597
+ ) : null
598
+ }
599
+ />
600
+ ) : null;
601
+
602
+ const skeletonRows = Array.from({ length: Math.min(per, 8) }, (_, i) => i);
603
+
604
+ // The rows on this page, in runs of one group each (one run, no heading,
605
+ // when the table is not grouped). A group's summary and subtotal are
606
+ // over all of its rows, not only this page's; its subtotal shows on the
607
+ // page where the group ends.
608
+ const groupByKey = useMemo(() => new Map((groups ?? NONE).map((g) => [g.key, g])), [groups]);
609
+ const runs = [];
610
+ for (const row of visible) {
611
+ const k = grouped ? groupKeyOf(groupValueOf(groupBy, row)) : null;
612
+ let run = runs[runs.length - 1];
613
+ if (!run || run.key !== k) runs.push((run = { key: k, group: groupByKey.get(k), rows: [] }));
614
+ run.rows.push(row);
615
+ }
616
+ const labelOf = (g) => (groupLabel ? groupLabel(g.value, g.rows) : g.key === "" ? "None" : g.key);
617
+ const totalsMap = typeof totals === "function" ? totals(manual ? all : matched) : totals;
618
+ const showTotals = isColumnMap(totalsMap) && !loading && visible.length > 0;
619
+
620
+ // One aligned row of per-column values (a subtotal, the totals): blank
621
+ // under the leading and trailing controls, the label in the first
622
+ // column when the map has nothing for it.
623
+ function alignedCells(map, labelText, layer) {
624
+ return (
625
+ <>
626
+ {expanding ? <TableCell className={cx("w-10 px-1", stick("expander", layer).className)} style={stick("expander", layer).style} /> : null}
627
+ {canSelect ? <TableCell className={cx("w-12", stick("select", layer).className)} style={stick("select", layer).style} /> : null}
628
+ {shown.map((c, j) => {
629
+ const s = j === 0 ? stick("first", layer) : {};
630
+ const has = Object.prototype.hasOwnProperty.call(map, c.key);
631
+ return (
632
+ <TableCell key={c.key} numeric={c.align === "end" || c.numeric} className={cx(s.className, c.className)} style={s.style}>
633
+ {has ? map[c.key] : j === 0 ? labelText : null}
634
+ </TableCell>
635
+ );
636
+ })}
637
+ {hasActions ? <TableCell className="w-12" /> : null}
638
+ </>
639
+ );
640
+ }
641
+
642
+ function dataRow(row) {
643
+ const id = keyOf(row);
644
+ const tone = toneOf(row);
645
+ const tint = tone ? TONES[tone] : null;
646
+ const selected = pickedSet.has(id);
647
+ const canOpen = expandable(row);
648
+ const open = canOpen && openSet.has(id);
649
+ const xid = `${uid}-x-${domId(id)}`;
650
+ const sx = stick("expander", "body", { tone, selected });
651
+ const ss = stick("select", "body", { tone, selected });
652
+ const sf = stick("first", "body", { tone, selected, bar: tone === "highlighted" && lead === 0 });
653
+ const barFirst = tone === "highlighted" && lead === 0;
654
+ const out = [
655
+ <TableRow
656
+ key={id}
657
+ data-row={id}
658
+ data-tone={tone && tone !== "default" ? tone : undefined}
659
+ data-open={open ? "" : undefined}
660
+ selected={selected}
661
+ {...rowProps(row, id)}
662
+ className={cx(
663
+ clickable && "cursor-pointer",
664
+ (clickable || keyboard) && cx(focusRing, "focus-visible:bg-muted"),
665
+ open && "border-b-0",
666
+ rowClassName?.(row),
667
+ )}
668
+ >
669
+ {expanding ? (
670
+ <TableCell className={cx("w-10 px-1 py-0", stickyFirstColumn ? sx.className : tint, tone === "highlighted" && BAR)} style={sx.style}>
671
+ {canOpen ? (
672
+ // raw: a bare icon toggle whose glyph turns; no kit Button draws a rotating icon.
673
+ <button
674
+ type="button"
675
+ aria-expanded={open}
676
+ aria-controls={open ? xid : undefined}
677
+ aria-label={`Details for ${nameOf(row)}`}
678
+ onClick={() => toggleOpen(id)}
679
+ className="inline-flex size-8 items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
680
+ >
681
+ <Icon name="chevron-right" className={cx("transition-transform duration-150", open && "rotate-90")} />
682
+ </button>
683
+ ) : null}
684
+ </TableCell>
685
+ ) : null}
686
+ {canSelect ? (
687
+ <TableCell className={cx("w-12 py-0", stickyFirstColumn ? ss.className : tint, tone === "highlighted" && !expanding && BAR)} style={ss.style}>
688
+ {checkbox(row)}
689
+ </TableCell>
690
+ ) : null}
691
+ {shown.map((c, j) => {
692
+ const s = j === 0 ? sf : {};
693
+ return (
694
+ <TableCell
695
+ key={c.key}
696
+ numeric={c.align === "end" || c.numeric}
697
+ className={cx(j === 0 && stickyFirstColumn ? s.className : tint, barFirst && j === 0 && !stickyFirstColumn && BAR, c.className)}
698
+ style={s.style}
699
+ >
700
+ {c.cell ? c.cell(row) : row?.[c.key] ?? ""}
701
+ </TableCell>
702
+ );
703
+ })}
704
+ {hasActions ? <TableCell className={cx(inlining ? "whitespace-nowrap py-1.5" : "w-12 py-0", "text-right", tint)}>{actionsCell(row)}</TableCell> : null}
705
+ </TableRow>,
706
+ ];
707
+ if (open) {
708
+ out.push(
709
+ <TableRow key={`${id}-x`} className="hover:bg-transparent">
710
+ {/* raw: a bare cell — TableCell's padding would widen the table past the box. */}
711
+ <td id={xid} colSpan={width} className="bg-muted/30 p-0 align-top">
712
+ <div className={cx("sticky left-0 w-[100cqw] px-4 py-3", expanding && "sm:pl-12")}>{renderExpanded(row)}</div>
713
+ </td>
714
+ </TableRow>,
715
+ );
716
+ }
717
+ return out;
718
+ }
719
+
720
+ function groupHead(run, i) {
721
+ const g = run.group ?? { key: run.key, value: run.key, rows: run.rows };
722
+ const summary = groupSummary ? groupSummary(g.rows) : null;
723
+ return (
724
+ <TableRow key={`${run.key}-h`} data-group={run.key} className={cx("bg-muted/40 hover:bg-muted/40", i > 0 && "border-t")}>
725
+ <th scope="rowgroup" colSpan={width} className="h-10 px-3 text-left align-middle text-sm font-normal">
726
+ <div className="sticky left-3 flex w-[calc(100cqw-1.5rem)] items-center justify-between gap-3">
727
+ <span className="flex min-w-0 items-center gap-2">
728
+ <span className="font-semibold text-foreground">{labelOf(g)}</span>
729
+ <span data-nums className="text-xs text-muted-foreground">
730
+ {g.rows.length.toLocaleString()}
731
+ </span>
732
+ </span>
733
+ {summary !== null && summary !== undefined && summary !== false ? (
734
+ <span data-nums className="shrink-0 whitespace-nowrap text-muted-foreground">
735
+ {summary}
736
+ </span>
737
+ ) : null}
738
+ </div>
739
+ </th>
740
+ </TableRow>
741
+ );
742
+ }
743
+
744
+ function groupFoot(run) {
745
+ const g = run.group;
746
+ if (!groupFooter || !g || run.rows[run.rows.length - 1] !== g.rows[g.rows.length - 1]) return null;
747
+ const content = groupFooter(g.rows);
748
+ if (content === null || content === undefined || content === false) return null;
749
+ return (
750
+ <TableRow key={`${run.key}-f`} data-subtotal={run.key} className="bg-muted font-semibold hover:bg-muted">
751
+ {isColumnMap(content) ? (
752
+ alignedCells(content, "Subtotal", "sub")
753
+ ) : (
754
+ <TableCell colSpan={width} className="text-right">
755
+ {content}
756
+ </TableCell>
757
+ )}
758
+ </TableRow>
759
+ );
760
+ }
761
+
762
+ // The table's scroll box is a size container: a details row or a group
763
+ // heading spans the whole (maybe wider) table but is drawn at the width
764
+ // you can see (100cqw), pinned to its left edge.
765
+ const table = (
766
+ <div className="min-w-0 max-w-full" onKeyDown={keyboard ? onGridKey : undefined}>
767
+ <Table variant={variant} aria-label={label} aria-busy={loading || undefined} className="@container">
768
+ <TableHeader>
769
+ <TableRow>
770
+ {expanding ? (
771
+ <TableHead className={cx("w-10 px-1", stick("expander", "head").className)} style={stick("expander", "head").style}>
772
+ <span className="sr-only">Details</span>
773
+ </TableHead>
774
+ ) : null}
775
+ {canSelect ? (
776
+ <TableHead className={cx("w-12 py-0", stick("select", "head").className)} style={stick("select", "head").style}>
777
+ <Checkbox
778
+ checked={allOnPage}
779
+ ref={(el) => {
780
+ if (el) el.indeterminate = someOnPage;
781
+ }}
782
+ disabled={loading || visible.length === 0}
783
+ onChange={togglePage}
784
+ aria-label={allOnPage ? "Deselect this page" : "Select this page"}
785
+ className="px-1"
786
+ />
787
+ </TableHead>
788
+ ) : null}
789
+ {shown.map((c, j) => {
790
+ const at = (order ?? NONE).findIndex((s) => s.key === c.key);
791
+ const dir = at >= 0 ? order[at].dir : "none";
792
+ const s = j === 0 ? stick("first", "head") : {};
793
+ return (
794
+ <TableHead
795
+ key={c.key}
796
+ numeric={c.align === "end" || c.numeric}
797
+ sort={c.sortable ? dir : undefined}
798
+ onSort={c.sortable ? (e) => pressSort(e, c.key) : undefined}
799
+ style={c.width || s.style ? { ...(c.width ? { width: c.width } : null), ...s.style } : undefined}
800
+ sortIndex={c.sortable && at >= 0 && order.length > 1 ? at + 1 : undefined}
801
+ className={cx(s.className, c.headerClassName)}
802
+ title={c.sortable && multiSort ? "Shift-click to sort by more than one column" : undefined}
803
+ >
804
+ {c.header}
805
+ </TableHead>
806
+ );
807
+ })}
808
+ {hasActions ? (
809
+ <TableHead className={inlining ? "text-right" : "w-12"}>
810
+ <span className="sr-only">Actions</span>
811
+ </TableHead>
812
+ ) : null}
813
+ </TableRow>
814
+ </TableHeader>
815
+ {loading ? (
816
+ <TableBody>
817
+ {skeletonRows.map((i) => (
818
+ <TableRow key={`s${i}`}>
819
+ {expanding ? <TableCell className="w-10 px-1" /> : null}
820
+ {canSelect ? (
821
+ <TableCell className="py-0">
822
+ <Skeleton shape="rect" className="ml-1 size-4 rounded-sm" />
823
+ </TableCell>
824
+ ) : null}
825
+ {shown.map((c, j) => (
826
+ <TableCell key={c.key} numeric={c.align === "end" || c.numeric}>
827
+ <Skeleton className={cx("h-4", c.align === "end" || c.numeric ? "ml-auto w-16" : j === 0 ? "w-32" : "w-24")} />
828
+ </TableCell>
829
+ ))}
830
+ {hasActions ? <TableCell /> : null}
831
+ </TableRow>
832
+ ))}
833
+ </TableBody>
834
+ ) : grouped ? (
835
+ // One <tbody> per group: its heading is a rowgroup header.
836
+ runs.map((run, i) => (
837
+ <TableBody key={run.key}>
838
+ {groupHead(run, i)}
839
+ {run.rows.flatMap(dataRow)}
840
+ {groupFoot(run)}
841
+ </TableBody>
842
+ ))
843
+ ) : (
844
+ <TableBody>{visible.flatMap(dataRow)}</TableBody>
845
+ )}
846
+ {showTotals ? (
847
+ <TableFooter>
848
+ <TableRow data-totals="" className="font-semibold hover:bg-transparent">
849
+ {alignedCells(totalsMap, totalsLabel, "foot")}
850
+ </TableRow>
851
+ </TableFooter>
852
+ ) : null}
853
+ </Table>
854
+ </div>
855
+ );
856
+
857
+ // Phone cards: the same rows, one card each, below 640px. Group headings
858
+ // and subtotals, a row's details and the totals come along.
859
+ function pairs(map) {
860
+ return shown
861
+ .filter((c) => Object.prototype.hasOwnProperty.call(map, c.key))
862
+ .map((c) => (
863
+ <div key={c.key} className="min-w-0">
864
+ <dt className="text-xs font-normal text-muted-foreground">{c.header}</dt>
865
+ <dd data-nums className="truncate">
866
+ {map[c.key]}
867
+ </dd>
868
+ </div>
869
+ ));
870
+ }
871
+
872
+ function card(row) {
873
+ const id = keyOf(row);
874
+ const tone = toneOf(row);
875
+ const rest = cardTitle ? shown : shown.slice(1);
876
+ const canOpen = expandable(row);
877
+ const open = canOpen && openSet.has(id);
878
+ const xid = `${uid}-cx-${domId(id)}`;
879
+ const split = hasActions ? splitActions(row) : { inline: NONE, menu: NONE };
880
+ return (
881
+ <li
882
+ key={id}
883
+ data-row={id}
884
+ data-tone={tone && tone !== "default" ? tone : undefined}
885
+ data-selected={pickedSet.has(id) ? "" : undefined}
886
+ {...rowProps(row, id)}
887
+ className={cx(
888
+ "rounded-lg border border-border bg-card p-3 text-sm text-card-foreground data-[selected]:ring-2 data-[selected]:ring-ring",
889
+ tone && CARD_TONES[tone],
890
+ clickable && "cursor-pointer",
891
+ (clickable || keyboard) && "outline-none focus-visible:ring-2 focus-visible:ring-ring",
892
+ rowClassName?.(row),
893
+ )}
894
+ >
895
+ <div className="flex items-start gap-1">
896
+ {canSelect ? <div className="-my-2 -ml-1 shrink-0">{checkbox(row)}</div> : null}
897
+ <div className="min-w-0 flex-1 font-medium">{titleOf(row)}</div>
898
+ {canOpen ? (
899
+ // raw: the same details toggle, on a card.
900
+ <button
901
+ type="button"
902
+ aria-expanded={open}
903
+ aria-controls={open ? xid : undefined}
904
+ aria-label={`Details for ${nameOf(row)}`}
905
+ onClick={() => toggleOpen(id)}
906
+ className="-my-1 inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
907
+ >
908
+ <Icon name="chevron-down" className={cx("transition-transform duration-150", open && "rotate-180")} />
909
+ </button>
910
+ ) : null}
911
+ {hasActions ? <div className="-my-1 -mr-1 shrink-0">{rowMenu(row, split)}</div> : null}
912
+ </div>
913
+ {rest.length ? (
914
+ <dl className="mt-2 grid grid-cols-2 gap-x-3 gap-y-2">
915
+ {rest.map((c) => (
916
+ <div key={c.key} className="min-w-0">
917
+ <dt className="text-xs text-muted-foreground">{c.header}</dt>
918
+ <dd className="truncate">{c.cell ? c.cell(row) : row?.[c.key] ?? ""}</dd>
919
+ </div>
920
+ ))}
921
+ </dl>
922
+ ) : null}
923
+ {split.inline.length ? <div className="mt-3 flex flex-wrap gap-2">{split.inline.map((a) => inlineButton(row, a, true))}</div> : null}
924
+ {open ? (
925
+ <div id={xid} className="mt-3 border-t border-border pt-3">
926
+ {renderExpanded(row)}
927
+ </div>
928
+ ) : null}
929
+ </li>
930
+ );
931
+ }
932
+
933
+ const cards = (
934
+ <ul className="grid gap-2" aria-label={label} aria-busy={loading || undefined} onKeyDown={keyboard ? onGridKey : undefined}>
935
+ {loading
936
+ ? skeletonRows.slice(0, 4).map((i) => (
937
+ <li key={`s${i}`} className="grid gap-2 rounded-lg border border-border bg-card p-3">
938
+ <Skeleton className="h-4 w-1/2" />
939
+ <Skeleton className="h-3 w-3/4" />
940
+ <Skeleton className="h-3 w-2/3" />
941
+ </li>
942
+ ))
943
+ : runs.flatMap((run) => {
944
+ if (!grouped) return run.rows.map(card);
945
+ const g = run.group ?? { key: run.key, value: run.key, rows: run.rows };
946
+ const summary = groupSummary ? groupSummary(g.rows) : null;
947
+ const ends = run.group && run.rows[run.rows.length - 1] === g.rows[g.rows.length - 1];
948
+ const foot = groupFooter && ends ? groupFooter(g.rows) : null;
949
+ return [
950
+ <li key={`${run.key}-h`} data-group={run.key} className="flex items-baseline justify-between gap-3 px-1 pt-2 text-sm">
951
+ <span className="flex min-w-0 items-baseline gap-2">
952
+ <span className="truncate font-semibold">{labelOf(g)}</span>
953
+ <span data-nums className="text-xs text-muted-foreground">
954
+ {g.rows.length.toLocaleString()}
955
+ </span>
956
+ </span>
957
+ {summary !== null && summary !== undefined && summary !== false ? (
958
+ <span data-nums className="shrink-0 text-muted-foreground">
959
+ {summary}
960
+ </span>
961
+ ) : null}
962
+ </li>,
963
+ ...run.rows.map(card),
964
+ foot !== null && foot !== undefined && foot !== false ? (
965
+ <li key={`${run.key}-f`} data-subtotal={run.key} className="rounded-lg bg-muted px-3 py-2 text-sm font-semibold">
966
+ {isColumnMap(foot) ? <dl className="grid grid-cols-2 gap-x-3 gap-y-1">{pairs(foot)}</dl> : foot}
967
+ </li>
968
+ ) : null,
969
+ ];
970
+ })}
971
+ {showTotals ? (
972
+ <li data-totals="" className="rounded-lg border border-border bg-muted/50 px-3 py-2 text-sm font-semibold">
973
+ {totalsLabel ? <p className="mb-1">{totalsLabel}</p> : null}
974
+ <dl className="grid grid-cols-2 gap-x-3 gap-y-1">{pairs(totalsMap)}</dl>
975
+ </li>
976
+ ) : null}
977
+ </ul>
978
+ );
979
+
980
+ const body = empty ?? noMatches;
981
+ const hideable = cols.filter((c) => c.hideable !== false);
982
+ const from = total === 0 ? 0 : (pageNow - 1) * per + 1;
983
+ const to = Math.min(total, pageNow * per);
984
+
985
+ return (
986
+ <div className={cx("grid min-w-0 max-w-full gap-3", className)}>
987
+ {error ? <Notice tone="danger">{error}</Notice> : null}
988
+
989
+ {/* The toolbar: search, facets, the surface's own, the columns menu. */}
990
+ <div className="flex min-w-0 flex-wrap items-center gap-2">
991
+ {searchable ? (
992
+ <div className="w-full sm:w-64">
993
+ <InputGroup
994
+ icon="search"
995
+ type="search"
996
+ value={query ?? ""}
997
+ onChange={(e) => changeQuery(e.target.value)}
998
+ placeholder={searchPlaceholder}
999
+ aria-label={`Search ${label}`}
1000
+ disabled={!manual && all.length === 0 && !loading}
1001
+ />
1002
+ </div>
1003
+ ) : null}
1004
+ {facetList.map((f) => {
1005
+ const options = facetOptions(f, all, filterState, facetList, query, cols);
1006
+ return (
1007
+ <div key={f.key} className="min-w-0 flex-1 basis-40 sm:w-52 sm:flex-none">
1008
+ <Combobox
1009
+ multiple
1010
+ searchable={options.length > 7}
1011
+ options={options}
1012
+ value={(filterState[f.key] ?? NONE).map(String)}
1013
+ onValueChange={(values) => changeFacet(f.key, values)}
1014
+ placeholder={f.label}
1015
+ label={f.label}
1016
+ disabled={!manual && all.length === 0}
1017
+ />
1018
+ </div>
1019
+ );
1020
+ })}
1021
+ {narrowed ? (
1022
+ <Button variant="quiet" size="sm" icon="x" onClick={clearFilters}>
1023
+ Reset
1024
+ </Button>
1025
+ ) : null}
1026
+ <div className="ml-auto flex flex-wrap items-center gap-2">
1027
+ {toolbar}
1028
+ {hideable.length > 1 ? (
1029
+ <Menu
1030
+ label="Columns, show or hide"
1031
+ size="md"
1032
+ trigger={
1033
+ <>
1034
+ <Icon name="eye" />
1035
+ <span>Columns</span>
1036
+ </>
1037
+ }
1038
+ className="border border-border"
1039
+ items={[
1040
+ { type: "label", label: "Show columns" },
1041
+ ...hideable.map((c) => {
1042
+ const on = !hiddenSet.has(c.key);
1043
+ return {
1044
+ type: "checkbox",
1045
+ label: typeof c.header === "string" ? c.header : humanize(c.key),
1046
+ checked: on,
1047
+ // The last one shown stays: a table of no columns is nothing.
1048
+ disabled: on && shown.length <= 1,
1049
+ onCheckedChange: (next) =>
1050
+ setHidden((h) => {
1051
+ const list = (h ?? []).map(String);
1052
+ return next ? list.filter((k) => k !== c.key) : [...list, c.key];
1053
+ }),
1054
+ };
1055
+ }),
1056
+ ]}
1057
+ />
1058
+ ) : null}
1059
+ </div>
1060
+ </div>
1061
+
1062
+ {/* The bulk bar: what is selected, and what can be done to it. */}
1063
+ {canSelect && count > 0 ? (
1064
+ <div
1065
+ role="region"
1066
+ aria-label="Selected rows"
1067
+ className="flex flex-wrap items-center gap-2 rounded-lg border border-border bg-muted px-3 py-2 text-sm"
1068
+ >
1069
+ <span data-nums className="font-medium">
1070
+ {count.toLocaleString()} selected
1071
+ </span>
1072
+ {!manual && allOnPage && count < total ? (
1073
+ <Button variant="link" size="sm" onClick={selectAllMatches}>
1074
+ Select all {total.toLocaleString()}
1075
+ </Button>
1076
+ ) : null}
1077
+ <Button variant="quiet" size="sm" onClick={clearSelection}>
1078
+ Clear
1079
+ </Button>
1080
+ <div className="ml-auto flex flex-wrap items-center gap-2">
1081
+ {bulkItems.map((a) => (
1082
+ <Button
1083
+ key={a.label}
1084
+ size="sm"
1085
+ icon={a.icon}
1086
+ variant={a.tone === "danger" ? "danger" : "secondary"}
1087
+ loading={running === a.label}
1088
+ disabled={Boolean(running && running !== a.label) || a.disabled}
1089
+ onClick={() => runBulk(a)}
1090
+ >
1091
+ {a.label}
1092
+ </Button>
1093
+ ))}
1094
+ </div>
1095
+ </div>
1096
+ ) : null}
1097
+ {bulkError ? (
1098
+ <Notice tone="danger" onDismiss={() => setBulkError(null)}>
1099
+ {bulkError}
1100
+ </Notice>
1101
+ ) : null}
1102
+ {rowError ? (
1103
+ <Notice tone="danger" onDismiss={() => setRowError(null)}>
1104
+ {rowError}
1105
+ </Notice>
1106
+ ) : null}
1107
+ {bulkNote ? (
1108
+ <Notice tone="info" onDismiss={() => setBulkNote(null)}>
1109
+ {bulkNote}
1110
+ </Notice>
1111
+ ) : null}
1112
+
1113
+ {body ?? (mobile === "cards" ? (
1114
+ <>
1115
+ <div className="grid gap-1 sm:hidden">
1116
+ {canSelect && !loading && visible.length ? (
1117
+ <Checkbox
1118
+ checked={allOnPage}
1119
+ ref={(el) => {
1120
+ if (el) el.indeterminate = someOnPage;
1121
+ }}
1122
+ onChange={togglePage}
1123
+ label="Select this page"
1124
+ className="px-1 text-muted-foreground"
1125
+ />
1126
+ ) : null}
1127
+ {cards}
1128
+ </div>
1129
+ <div className="hidden sm:block">{table}</div>
1130
+ </>
1131
+ ) : (
1132
+ table
1133
+ ))}
1134
+
1135
+ {footer}
1136
+
1137
+ {paginate && !body && (total > 0 || loading) ? (
1138
+ <div className="flex flex-wrap items-center gap-x-4 gap-y-2">
1139
+ <p data-nums className="w-full text-sm text-muted-foreground sm:w-auto" id={`${uid}-range`}>
1140
+ {loading ? "Loading…" : `${from.toLocaleString()}–${to.toLocaleString()} of ${plural(total, noun)}`}
1141
+ </p>
1142
+ <Pagination
1143
+ page={pageNow}
1144
+ pageCount={pageCount}
1145
+ onPageChange={setPage}
1146
+ pageSize={per}
1147
+ pageSizes={pageSizes}
1148
+ onPageSizeChange={(n) => {
1149
+ setSize(n);
1150
+ if (pageNow !== 1) setPage(1);
1151
+ }}
1152
+ size="sm"
1153
+ label={`${label} pages`}
1154
+ className="min-w-0 flex-1"
1155
+ />
1156
+ </div>
1157
+ ) : null}
1158
+
1159
+ <p aria-live="polite" className="sr-only">
1160
+ {announce}
1161
+ </p>
1162
+ </div>
1163
+ );
1164
+ }