makersclaw 0.2.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.
- package/THIRD-PARTY-NOTICES.md +180 -0
- package/package.json +3 -2
- package/r/annotate.json +62 -0
- package/r/approval-request.json +52 -0
- package/r/audio-player.json +74 -0
- package/r/auth-form.json +55 -0
- package/r/board.json +10 -6
- package/r/calendar-view.json +105 -0
- package/r/cart.json +62 -0
- package/r/chart.json +83 -0
- package/r/chat.json +95 -0
- package/r/code-editor.json +94 -0
- package/r/comment-box.json +64 -0
- package/r/contact-section.json +53 -0
- package/r/core.json +205 -8
- package/r/csv-import.json +63 -0
- package/r/data-table.json +69 -0
- package/r/diagram.json +74 -0
- package/r/doc-editor.json +82 -0
- package/r/doc-page.json +61 -0
- package/r/email-render.json +73 -0
- package/r/faq.json +50 -0
- package/r/file-upload.json +54 -0
- package/r/flow-canvas.json +93 -0
- package/r/footer.json +52 -0
- package/r/gantt.json +70 -0
- package/r/job-panel.json +12 -7
- package/r/json-editor.json +66 -0
- package/r/map.json +89 -0
- package/r/menubar.json +52 -0
- package/r/mind-map.json +67 -0
- package/r/navigation-menu.json +52 -0
- package/r/order-detail.json +54 -0
- package/r/org-chart.json +66 -0
- package/r/page-builder.json +105 -0
- package/r/payment-methods.json +51 -0
- package/r/pdf-document.json +79 -0
- package/r/pdf-viewer.json +88 -0
- package/r/product-detail.json +76 -0
- package/r/product-grid.json +65 -0
- package/r/record-form.json +67 -0
- package/r/record-notes.json +55 -0
- package/r/registry.json +3411 -29
- package/r/search-dialog.json +51 -0
- package/r/settings-list.json +66 -0
- package/r/setup-checklist.json +51 -0
- package/r/share-dialog.json +52 -0
- package/r/sidebar.json +69 -0
- package/r/signature-pad.json +72 -0
- package/r/sketch-canvas.json +99 -0
- package/r/spreadsheet.json +90 -0
- package/r/stat-cards.json +66 -0
- package/r/task-list.json +63 -0
- package/r/task-model.json +98 -0
- package/r/testimonials.json +52 -0
- package/r/transaction-list.json +53 -0
- package/r/whiteboard.json +66 -0
- package/registry/blocks/annotate/README.md +179 -0
- package/registry/blocks/annotate/annotate-data.js +250 -0
- package/registry/blocks/annotate/annotate.jsx +646 -0
- package/registry/blocks/annotate/block.json +10 -0
- package/registry/blocks/annotate/demo.jsx +135 -0
- package/registry/blocks/annotate/index.js +15 -0
- package/registry/blocks/approval-request/README.md +116 -0
- package/registry/blocks/approval-request/approval-request.jsx +386 -0
- package/registry/blocks/approval-request/block.json +10 -0
- package/registry/blocks/approval-request/demo.jsx +127 -0
- package/registry/blocks/approval-request/index.js +4 -0
- package/registry/blocks/audio-player/README.md +167 -0
- package/registry/blocks/audio-player/audio-player.jsx +623 -0
- package/registry/blocks/audio-player/block.json +14 -0
- package/registry/blocks/audio-player/colors.js +109 -0
- package/registry/blocks/audio-player/demo.jsx +209 -0
- package/registry/blocks/audio-player/index.js +6 -0
- package/registry/blocks/audio-player/time.js +67 -0
- package/registry/blocks/audio-player/wavesurfer.js +9 -0
- package/registry/blocks/auth-form/README.md +126 -0
- package/registry/blocks/auth-form/auth-form.jsx +568 -0
- package/registry/blocks/auth-form/block.json +10 -0
- package/registry/blocks/auth-form/demo.jsx +131 -0
- package/registry/blocks/auth-form/index.js +3 -0
- package/registry/blocks/board/README.md +55 -5
- package/registry/blocks/board/block.json +2 -2
- package/registry/blocks/board/board.jsx +230 -86
- package/registry/blocks/board/demo.jsx +103 -24
- package/registry/blocks/calendar-view/README.md +180 -0
- package/registry/blocks/calendar-view/agenda.jsx +147 -0
- package/registry/blocks/calendar-view/block.json +76 -0
- package/registry/blocks/calendar-view/calendar-model.js +442 -0
- package/registry/blocks/calendar-view/calendar.jsx +627 -0
- package/registry/blocks/calendar-view/demo.jsx +195 -0
- package/registry/blocks/calendar-view/draft.jsx +49 -0
- package/registry/blocks/calendar-view/index.js +39 -0
- package/registry/blocks/calendar-view/month.jsx +346 -0
- package/registry/blocks/calendar-view/paint.js +69 -0
- package/registry/blocks/calendar-view/time-grid.jsx +527 -0
- package/registry/blocks/calendar-view/tray.jsx +87 -0
- package/registry/blocks/cart/README.md +146 -0
- package/registry/blocks/cart/block.json +10 -0
- package/registry/blocks/cart/cart.jsx +597 -0
- package/registry/blocks/cart/demo.jsx +98 -0
- package/registry/blocks/cart/index.js +4 -0
- package/registry/blocks/cart/money.js +28 -0
- package/registry/blocks/chart/README.md +316 -0
- package/registry/blocks/chart/block.json +10 -0
- package/registry/blocks/chart/cartesian.jsx +642 -0
- package/registry/blocks/chart/core.jsx +262 -0
- package/registry/blocks/chart/demo.jsx +357 -0
- package/registry/blocks/chart/heatmap.jsx +336 -0
- package/registry/blocks/chart/index.js +8 -0
- package/registry/blocks/chart/pie.jsx +245 -0
- package/registry/blocks/chart/sparkline.jsx +88 -0
- package/registry/blocks/chat/README.md +273 -0
- package/registry/blocks/chat/attachment.jsx +95 -0
- package/registry/blocks/chat/block.json +10 -0
- package/registry/blocks/chat/chat.jsx +132 -0
- package/registry/blocks/chat/composer.jsx +364 -0
- package/registry/blocks/chat/demo.jsx +578 -0
- package/registry/blocks/chat/header.jsx +34 -0
- package/registry/blocks/chat/index.js +9 -0
- package/registry/blocks/chat/message.jsx +450 -0
- package/registry/blocks/chat/scroller.jsx +247 -0
- package/registry/blocks/chat/text.jsx +104 -0
- package/registry/blocks/code-editor/README.md +210 -0
- package/registry/blocks/code-editor/block.json +22 -0
- package/registry/blocks/code-editor/code-editor.jsx +534 -0
- package/registry/blocks/code-editor/codemirror.js +38 -0
- package/registry/blocks/code-editor/demo.jsx +205 -0
- package/registry/blocks/code-editor/index.js +8 -0
- package/registry/blocks/code-editor/json.js +317 -0
- package/registry/blocks/code-editor/sql.js +136 -0
- package/registry/blocks/code-editor/text.js +101 -0
- package/registry/blocks/code-editor/theme.js +186 -0
- package/registry/blocks/comment-box/README.md +123 -0
- package/registry/blocks/comment-box/block.json +10 -0
- package/registry/blocks/comment-box/comment-body.jsx +217 -0
- package/registry/blocks/comment-box/comment-box.jsx +198 -0
- package/registry/blocks/comment-box/comment-time.jsx +77 -0
- package/registry/blocks/comment-box/demo.jsx +141 -0
- package/registry/blocks/comment-box/index.js +8 -0
- package/registry/blocks/contact-section/README.md +152 -0
- package/registry/blocks/contact-section/block.json +11 -0
- package/registry/blocks/contact-section/contact-section.jsx +509 -0
- package/registry/blocks/contact-section/demo.jsx +93 -0
- package/registry/blocks/contact-section/index.js +4 -0
- package/registry/blocks/csv-import/README.md +133 -0
- package/registry/blocks/csv-import/block.json +13 -0
- package/registry/blocks/csv-import/csv-import.jsx +479 -0
- package/registry/blocks/csv-import/demo.jsx +106 -0
- package/registry/blocks/csv-import/import.js +200 -0
- package/registry/blocks/csv-import/index.js +7 -0
- package/registry/blocks/data-table/README.md +386 -0
- package/registry/blocks/data-table/block.json +10 -0
- package/registry/blocks/data-table/data-table.jsx +1164 -0
- package/registry/blocks/data-table/demo.jsx +588 -0
- package/registry/blocks/data-table/index.js +5 -0
- package/registry/blocks/data-table/model.js +169 -0
- package/registry/blocks/diagram/README.md +108 -0
- package/registry/blocks/diagram/block.json +13 -0
- package/registry/blocks/diagram/demo.jsx +113 -0
- package/registry/blocks/diagram/diagram.jsx +481 -0
- package/registry/blocks/diagram/index.js +7 -0
- package/registry/blocks/diagram/mermaid.js +52 -0
- package/registry/blocks/diagram/palette.js +313 -0
- package/registry/blocks/diagram/source.js +98 -0
- package/registry/blocks/doc-editor/README.md +215 -0
- package/registry/blocks/doc-editor/block.json +15 -0
- package/registry/blocks/doc-editor/demo.jsx +225 -0
- package/registry/blocks/doc-editor/doc-editor.jsx +291 -0
- package/registry/blocks/doc-editor/doc.js +309 -0
- package/registry/blocks/doc-editor/index.js +7 -0
- package/registry/blocks/doc-editor/schema.jsx +56 -0
- package/registry/blocks/doc-editor/shadcn-classes.js +110 -0
- package/registry/blocks/doc-editor/theme.js +82 -0
- package/registry/blocks/doc-page/README.md +119 -0
- package/registry/blocks/doc-page/autosave.js +104 -0
- package/registry/blocks/doc-page/block.json +10 -0
- package/registry/blocks/doc-page/demo.jsx +137 -0
- package/registry/blocks/doc-page/doc-page.jsx +276 -0
- package/registry/blocks/doc-page/index.js +4 -0
- package/registry/blocks/email-render/README.md +151 -0
- package/registry/blocks/email-render/block.json +14 -0
- package/registry/blocks/email-render/demo.jsx +153 -0
- package/registry/blocks/email-render/doc-email.jsx +210 -0
- package/registry/blocks/email-render/email.jsx +221 -0
- package/registry/blocks/email-render/index.js +10 -0
- package/registry/blocks/email-render/render.js +50 -0
- package/registry/blocks/email-render/theme.js +215 -0
- package/registry/blocks/faq/README.md +101 -0
- package/registry/blocks/faq/block.json +11 -0
- package/registry/blocks/faq/demo.jsx +89 -0
- package/registry/blocks/faq/faq.jsx +220 -0
- package/registry/blocks/faq/index.js +4 -0
- package/registry/blocks/file-upload/README.md +138 -0
- package/registry/blocks/file-upload/block.json +10 -0
- package/registry/blocks/file-upload/demo.jsx +204 -0
- package/registry/blocks/file-upload/file-upload.jsx +635 -0
- package/registry/blocks/file-upload/index.js +4 -0
- package/registry/blocks/flow-canvas/README.md +420 -0
- package/registry/blocks/flow-canvas/block.json +13 -0
- package/registry/blocks/flow-canvas/board.js +819 -0
- package/registry/blocks/flow-canvas/chrome.jsx +649 -0
- package/registry/blocks/flow-canvas/context.jsx +35 -0
- package/registry/blocks/flow-canvas/demo.jsx +249 -0
- package/registry/blocks/flow-canvas/edges.jsx +107 -0
- package/registry/blocks/flow-canvas/flow-canvas.jsx +1329 -0
- package/registry/blocks/flow-canvas/index.js +41 -0
- package/registry/blocks/flow-canvas/nodes.jsx +392 -0
- package/registry/blocks/flow-canvas/theme.js +174 -0
- package/registry/blocks/footer/README.md +133 -0
- package/registry/blocks/footer/block.json +11 -0
- package/registry/blocks/footer/demo.jsx +119 -0
- package/registry/blocks/footer/footer.jsx +406 -0
- package/registry/blocks/footer/index.js +4 -0
- package/registry/blocks/gantt/README.md +220 -0
- package/registry/blocks/gantt/block.json +10 -0
- package/registry/blocks/gantt/demo.jsx +148 -0
- package/registry/blocks/gantt/gantt.jsx +923 -0
- package/registry/blocks/gantt/index.js +7 -0
- package/registry/blocks/gantt/rows.jsx +468 -0
- package/registry/blocks/gantt/schedule.js +278 -0
- package/registry/blocks/job-panel/README.md +80 -0
- package/registry/blocks/job-panel/block.json +3 -3
- package/registry/blocks/job-panel/demo.jsx +230 -3
- package/registry/blocks/job-panel/job-panel.jsx +169 -3
- package/registry/blocks/json-editor/README.md +133 -0
- package/registry/blocks/json-editor/block.json +10 -0
- package/registry/blocks/json-editor/demo.jsx +92 -0
- package/registry/blocks/json-editor/index.js +7 -0
- package/registry/blocks/json-editor/json-editor.jsx +305 -0
- package/registry/blocks/json-editor/json-tree.jsx +393 -0
- package/registry/blocks/json-editor/tree.js +225 -0
- package/registry/blocks/map/README.md +221 -0
- package/registry/blocks/map/block.json +14 -0
- package/registry/blocks/map/demo.jsx +201 -0
- package/registry/blocks/map/geo.js +198 -0
- package/registry/blocks/map/index.js +8 -0
- package/registry/blocks/map/list.jsx +128 -0
- package/registry/blocks/map/map-view.jsx +715 -0
- package/registry/blocks/map/maplibre.js +30 -0
- package/registry/blocks/map/marks.jsx +146 -0
- package/registry/blocks/map/theme.js +142 -0
- package/registry/blocks/menubar/README.md +109 -0
- package/registry/blocks/menubar/block.json +10 -0
- package/registry/blocks/menubar/demo.jsx +128 -0
- package/registry/blocks/menubar/index.js +4 -0
- package/registry/blocks/menubar/menubar.jsx +199 -0
- package/registry/blocks/mind-map/README.md +206 -0
- package/registry/blocks/mind-map/block.json +10 -0
- package/registry/blocks/mind-map/demo.jsx +103 -0
- package/registry/blocks/mind-map/index.js +31 -0
- package/registry/blocks/mind-map/mind-map.jsx +1125 -0
- package/registry/blocks/mind-map/topic.jsx +143 -0
- package/registry/blocks/mind-map/tree.js +645 -0
- package/registry/blocks/navigation-menu/README.md +124 -0
- package/registry/blocks/navigation-menu/block.json +10 -0
- package/registry/blocks/navigation-menu/demo.jsx +84 -0
- package/registry/blocks/navigation-menu/index.js +4 -0
- package/registry/blocks/navigation-menu/navigation-menu.jsx +448 -0
- package/registry/blocks/order-detail/README.md +109 -0
- package/registry/blocks/order-detail/block.json +10 -0
- package/registry/blocks/order-detail/demo.jsx +88 -0
- package/registry/blocks/order-detail/index.js +4 -0
- package/registry/blocks/order-detail/order-detail.jsx +354 -0
- package/registry/blocks/org-chart/README.md +145 -0
- package/registry/blocks/org-chart/block.json +10 -0
- package/registry/blocks/org-chart/demo.jsx +94 -0
- package/registry/blocks/org-chart/index.js +7 -0
- package/registry/blocks/org-chart/org-chart.jsx +631 -0
- package/registry/blocks/org-chart/org.js +245 -0
- package/registry/blocks/org-chart/person.jsx +96 -0
- package/registry/blocks/page-builder/README.md +257 -0
- package/registry/blocks/page-builder/block.json +14 -0
- package/registry/blocks/page-builder/demo.jsx +145 -0
- package/registry/blocks/page-builder/grapes.js +18 -0
- package/registry/blocks/page-builder/index.js +11 -0
- package/registry/blocks/page-builder/page-builder.jsx +704 -0
- package/registry/blocks/page-builder/page-view.jsx +33 -0
- package/registry/blocks/page-builder/page.js +118 -0
- package/registry/blocks/page-builder/panels.jsx +588 -0
- package/registry/blocks/page-builder/sections.js +209 -0
- package/registry/blocks/page-builder/theme.js +87 -0
- package/registry/blocks/page-builder/thumbs.jsx +189 -0
- package/registry/blocks/page-builder/tokens.js +329 -0
- package/registry/blocks/payment-methods/README.md +104 -0
- package/registry/blocks/payment-methods/block.json +10 -0
- package/registry/blocks/payment-methods/demo.jsx +71 -0
- package/registry/blocks/payment-methods/index.js +4 -0
- package/registry/blocks/payment-methods/payment-methods.jsx +302 -0
- package/registry/blocks/pdf-document/README.md +213 -0
- package/registry/blocks/pdf-document/block.json +13 -0
- package/registry/blocks/pdf-document/demo.jsx +136 -0
- package/registry/blocks/pdf-document/index.js +33 -0
- package/registry/blocks/pdf-document/money.js +83 -0
- package/registry/blocks/pdf-document/pdf-document.jsx +144 -0
- package/registry/blocks/pdf-document/primitives.jsx +271 -0
- package/registry/blocks/pdf-document/quote.jsx +179 -0
- package/registry/blocks/pdf-document/theme.js +193 -0
- package/registry/blocks/pdf-viewer/README.md +191 -0
- package/registry/blocks/pdf-viewer/block.json +14 -0
- package/registry/blocks/pdf-viewer/demo.jsx +252 -0
- package/registry/blocks/pdf-viewer/helpers.js +270 -0
- package/registry/blocks/pdf-viewer/index.js +8 -0
- package/registry/blocks/pdf-viewer/notes.jsx +195 -0
- package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1368 -0
- package/registry/blocks/pdf-viewer/pdfjs.js +37 -0
- package/registry/blocks/pdf-viewer/ssr-shim-end.js +6 -0
- package/registry/blocks/pdf-viewer/ssr-shim.js +44 -0
- package/registry/blocks/product-detail/README.md +217 -0
- package/registry/blocks/product-detail/block.json +10 -0
- package/registry/blocks/product-detail/demo.jsx +233 -0
- package/registry/blocks/product-detail/index.js +7 -0
- package/registry/blocks/product-detail/price.jsx +115 -0
- package/registry/blocks/product-detail/product-detail.jsx +348 -0
- package/registry/blocks/product-detail/product-media.jsx +102 -0
- package/registry/blocks/product-detail/product-options.jsx +144 -0
- package/registry/blocks/product-grid/README.md +162 -0
- package/registry/blocks/product-grid/block.json +10 -0
- package/registry/blocks/product-grid/demo.jsx +255 -0
- package/registry/blocks/product-grid/index.js +5 -0
- package/registry/blocks/product-grid/product-card.jsx +232 -0
- package/registry/blocks/product-grid/product-grid.jsx +235 -0
- package/registry/blocks/record-form/README.md +196 -0
- package/registry/blocks/record-form/block.json +10 -0
- package/registry/blocks/record-form/demo.jsx +525 -0
- package/registry/blocks/record-form/index.js +4 -0
- package/registry/blocks/record-form/record-fields.jsx +457 -0
- package/registry/blocks/record-form/record-form.jsx +415 -0
- package/registry/blocks/record-form/rules.js +81 -0
- package/registry/blocks/record-notes/README.md +139 -0
- package/registry/blocks/record-notes/block.json +10 -0
- package/registry/blocks/record-notes/demo.jsx +179 -0
- package/registry/blocks/record-notes/index.js +4 -0
- package/registry/blocks/record-notes/record-notes.jsx +266 -0
- package/registry/blocks/search-dialog/README.md +108 -0
- package/registry/blocks/search-dialog/block.json +10 -0
- package/registry/blocks/search-dialog/demo.jsx +116 -0
- package/registry/blocks/search-dialog/index.js +3 -0
- package/registry/blocks/search-dialog/search-dialog.jsx +235 -0
- package/registry/blocks/settings-list/README.md +171 -0
- package/registry/blocks/settings-list/block.json +10 -0
- package/registry/blocks/settings-list/demo.jsx +333 -0
- package/registry/blocks/settings-list/index.js +4 -0
- package/registry/blocks/settings-list/row-state.jsx +93 -0
- package/registry/blocks/settings-list/settings-list.jsx +349 -0
- package/registry/blocks/settings-list/settings-matrix.jsx +118 -0
- package/registry/blocks/setup-checklist/README.md +142 -0
- package/registry/blocks/setup-checklist/block.json +10 -0
- package/registry/blocks/setup-checklist/demo.jsx +159 -0
- package/registry/blocks/setup-checklist/index.js +4 -0
- package/registry/blocks/setup-checklist/setup-checklist.jsx +446 -0
- package/registry/blocks/share-dialog/README.md +113 -0
- package/registry/blocks/share-dialog/block.json +10 -0
- package/registry/blocks/share-dialog/demo.jsx +140 -0
- package/registry/blocks/share-dialog/index.js +3 -0
- package/registry/blocks/share-dialog/share-dialog.jsx +384 -0
- package/registry/blocks/sidebar/README.md +190 -0
- package/registry/blocks/sidebar/block.json +43 -0
- package/registry/blocks/sidebar/demo.jsx +577 -0
- package/registry/blocks/sidebar/index.js +4 -0
- package/registry/blocks/sidebar/sidebar-panel.jsx +149 -0
- package/registry/blocks/sidebar/sidebar.jsx +864 -0
- package/registry/blocks/signature-pad/README.md +137 -0
- package/registry/blocks/signature-pad/block.json +13 -0
- package/registry/blocks/signature-pad/colors.js +115 -0
- package/registry/blocks/signature-pad/demo.jsx +94 -0
- package/registry/blocks/signature-pad/index.js +6 -0
- package/registry/blocks/signature-pad/pad.js +7 -0
- package/registry/blocks/signature-pad/signature-pad.jsx +434 -0
- package/registry/blocks/signature-pad/signature.js +68 -0
- package/registry/blocks/sketch-canvas/README.md +280 -0
- package/registry/blocks/sketch-canvas/block.json +13 -0
- package/registry/blocks/sketch-canvas/demo.jsx +159 -0
- package/registry/blocks/sketch-canvas/excalidraw.js +40 -0
- package/registry/blocks/sketch-canvas/index.js +9 -0
- package/registry/blocks/sketch-canvas/palette.js +226 -0
- package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1188 -0
- package/registry/blocks/sketch-canvas/sketch.js +179 -0
- package/registry/blocks/sketch-canvas/ssr-shim-end.js +6 -0
- package/registry/blocks/sketch-canvas/ssr-shim.js +45 -0
- package/registry/blocks/sketch-canvas/stamps.js +104 -0
- package/registry/blocks/sketch-canvas/theme.js +131 -0
- package/registry/blocks/spreadsheet/README.md +307 -0
- package/registry/blocks/spreadsheet/block.json +13 -0
- package/registry/blocks/spreadsheet/cells.jsx +455 -0
- package/registry/blocks/spreadsheet/chrome.jsx +325 -0
- package/registry/blocks/spreadsheet/demo.jsx +177 -0
- package/registry/blocks/spreadsheet/grid.js +387 -0
- package/registry/blocks/spreadsheet/index.js +34 -0
- package/registry/blocks/spreadsheet/spreadsheet.jsx +1732 -0
- package/registry/blocks/spreadsheet/types.js +473 -0
- package/registry/blocks/stat-cards/README.md +173 -0
- package/registry/blocks/stat-cards/block.json +10 -0
- package/registry/blocks/stat-cards/demo.jsx +257 -0
- package/registry/blocks/stat-cards/index.js +4 -0
- package/registry/blocks/stat-cards/stat-cards.jsx +307 -0
- package/registry/blocks/stat-cards/usage.jsx +243 -0
- package/registry/blocks/task-list/README.md +176 -0
- package/registry/blocks/task-list/block.json +10 -0
- package/registry/blocks/task-list/demo.jsx +134 -0
- package/registry/blocks/task-list/index.js +5 -0
- package/registry/blocks/task-list/task-list.jsx +907 -0
- package/registry/blocks/task-model/README.md +455 -0
- package/registry/blocks/task-model/block.json +167 -0
- package/registry/blocks/task-model/changes.js +89 -0
- package/registry/blocks/task-model/dates.js +371 -0
- package/registry/blocks/task-model/demo.jsx +215 -0
- package/registry/blocks/task-model/dnd.jsx +240 -0
- package/registry/blocks/task-model/index.js +97 -0
- package/registry/blocks/task-model/model.js +919 -0
- package/registry/blocks/task-model/parts.jsx +449 -0
- package/registry/blocks/task-model/toolbar.jsx +271 -0
- package/registry/blocks/testimonials/README.md +139 -0
- package/registry/blocks/testimonials/block.json +11 -0
- package/registry/blocks/testimonials/demo.jsx +137 -0
- package/registry/blocks/testimonials/index.js +4 -0
- package/registry/blocks/testimonials/testimonials.jsx +430 -0
- package/registry/blocks/transaction-list/README.md +126 -0
- package/registry/blocks/transaction-list/block.json +10 -0
- package/registry/blocks/transaction-list/demo.jsx +79 -0
- package/registry/blocks/transaction-list/index.js +4 -0
- package/registry/blocks/transaction-list/transaction-list.jsx +383 -0
- package/registry/blocks/whiteboard/README.md +179 -0
- package/registry/blocks/whiteboard/block.json +10 -0
- package/registry/blocks/whiteboard/demo.jsx +169 -0
- package/registry/blocks/whiteboard/index.js +6 -0
- package/registry/blocks/whiteboard/templates.js +150 -0
- package/registry/blocks/whiteboard/whiteboard.jsx +721 -0
- package/src/install.mjs +18 -0
- package/src/shadcn.mjs +8 -0
- package/templates/next/files/README.md +145 -11
- package/templates/next/files/app/globals.css +55 -4
- package/templates/next/files/check/run.mjs +3 -3
- package/templates/next/files/components/ui/avatar.jsx +141 -0
- package/templates/next/files/components/ui/breadcrumb.jsx +149 -0
- package/templates/next/files/components/ui/button-group.jsx +70 -0
- package/templates/next/files/components/ui/button.jsx +132 -12
- package/templates/next/files/components/ui/calendar.jsx +609 -0
- package/templates/next/files/components/ui/carousel.jsx +281 -0
- package/templates/next/files/components/ui/collapsible.jsx +208 -0
- package/templates/next/files/components/ui/combobox.jsx +608 -0
- package/templates/next/files/components/ui/command.jsx +577 -0
- package/templates/next/files/components/ui/context-menu.jsx +191 -0
- package/templates/next/files/components/ui/date-picker.jsx +360 -0
- package/templates/next/files/components/ui/distribution.jsx +117 -0
- package/templates/next/files/components/ui/feedback.jsx +143 -0
- package/templates/next/files/components/ui/field.jsx +115 -3
- package/templates/next/files/components/ui/file-input.jsx +234 -0
- package/templates/next/files/components/ui/icon.jsx +130 -8
- package/templates/next/files/components/ui/image.jsx +143 -0
- package/templates/next/files/components/ui/index.js +47 -2
- package/templates/next/files/components/ui/input-group.jsx +213 -0
- package/templates/next/files/components/ui/input-otp.jsx +170 -0
- package/templates/next/files/components/ui/item.jsx +206 -0
- package/templates/next/files/components/ui/menu.jsx +454 -82
- package/templates/next/files/components/ui/notice.jsx +126 -14
- package/templates/next/files/components/ui/pagination.jsx +156 -0
- package/templates/next/files/components/ui/popover.jsx +284 -0
- package/templates/next/files/components/ui/radio.jsx +193 -0
- package/templates/next/files/components/ui/rating.jsx +131 -0
- package/templates/next/files/components/ui/resizable.jsx +338 -0
- package/templates/next/files/components/ui/scroll-area.jsx +123 -0
- package/templates/next/files/components/ui/separator.jsx +119 -0
- package/templates/next/files/components/ui/sheet.jsx +96 -11
- package/templates/next/files/components/ui/slider.jsx +174 -0
- package/templates/next/files/components/ui/stat.jsx +574 -0
- package/templates/next/files/components/ui/steps.jsx +338 -0
- package/templates/next/files/components/ui/swatch.jsx +33 -0
- package/templates/next/files/components/ui/table.jsx +130 -0
- package/templates/next/files/components/ui/toast.jsx +440 -0
- package/templates/next/files/components/ui/toggle.jsx +195 -0
- package/templates/next/files/components/ui/tooltip.jsx +270 -0
- 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
|
+
}
|