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,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "stat-cards",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Stat cards",
|
|
6
|
+
"description": "A row of key numbers, each with its name and its change: one bordered strip, or a card each with a change in the corner or after the value, a \"from\" value, a status dot and a goal meter, a sparkline, a \"View more\" footer; or a usage tile per amount against its limit (a bar or a ring, a split by kind with what is free, a warning line near the cap, an action and a limit the founder can change); or a plain list of values with their change. Loading, empty and error states.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/core.json",
|
|
9
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/chart.json"
|
|
10
|
+
],
|
|
11
|
+
"dependencies": [],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "registry/blocks/stat-cards/index.js",
|
|
15
|
+
"type": "registry:component",
|
|
16
|
+
"target": "@components/makersclaw/blocks/stat-cards/index.js",
|
|
17
|
+
"content": "// The StatCards block — `makersclaw add stat-cards`. Fixed: compose it\n// through its props (README.md beside this file); a tile it cannot be is\n// your own component in components/, built from the kit's Stat and Meter.\nexport { StatCards } from \"./stat-cards\";\n"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"path": "registry/blocks/stat-cards/stat-cards.jsx",
|
|
21
|
+
"type": "registry:component",
|
|
22
|
+
"target": "@components/makersclaw/blocks/stat-cards/stat-cards.jsx",
|
|
23
|
+
"content": "\"use client\";\n\nimport { Button, Card, Delta, Empty, Item, ItemGroup, Meter, Notice, Skeleton, Stat, StatGroup, StatusDot, cx } from \"../../ui\";\nimport { Sparkline } from \"../chart\";\nimport { LimitSheet, MoreLink, UsageTile, useLimitEditor } from \"./usage\";\n\n// StatCards — a row of key numbers, each with its name and what changed.\n// Four shapes over one list of items:\n//\n// strip (default) one bordered strip, a rule between the numbers\n// cards each number its own card: a change, a \"from\" value, a status\n// dot and a goal, a sparkline, a \"View more\" footer\n// usage amounts against a limit: a bar or a ring, a split by kind with\n// what is left, a warning line near the cap, an action per tile\n// and a limit the founder can change (onLimitChange)\n// list label · value · change, one row each, in a bordered list\n//\n// Data in as props, saves out as callbacks. README.md beside this file\n// lists every prop.\n\nconst LG_COLS = { 1: \"lg:grid-cols-1\", 2: \"lg:grid-cols-2\", 3: \"lg:grid-cols-3\", 4: \"lg:grid-cols-4\" };\nconst SM_COLS = { 1: \"sm:grid-cols-1\", 2: \"sm:grid-cols-2\", 3: \"sm:grid-cols-2\", 4: \"sm:grid-cols-2\" };\nconst STATUS_WORDS = { success: \"On track\", warning: \"Needs attention\", danger: \"At risk\", info: \"In progress\", neutral: \"No status\" };\n// A long unbroken value (\"$287,654.00\") breaks rather than push its cell wider.\nconst WRAP = \"[overflow-wrap:anywhere]\";\n\nconst keyOf = (item, i) => String(item?.id ?? item?.label ?? i);\n\n// A delta prop as Delta's props: a bare number is a percent.\nfunction deltaProps(item, variant) {\n const d = item?.delta;\n if (d === null || d === undefined || d === false) return null;\n const base = typeof d === \"number\" ? { value: d, suffix: \"%\" } : { ...d };\n return { variant, invert: item.invert, ...base };\n}\n\n// Up is good unless inverted; the sparkline wears that verdict.\nfunction trendTone(item) {\n if (item.trendTone) return item.trendTone;\n const d = item.delta;\n const n = typeof d === \"number\" ? d : typeof d?.value === \"number\" ? d.value : 0;\n if (!n) return \"chart-1\";\n const good = item.invert || d?.invert ? n < 0 : n > 0;\n return good ? \"success\" : \"danger\";\n}\n\nfunction statusOf(item) {\n const s = item?.status;\n if (!s) return null;\n if (typeof s === \"string\") return { tone: s, label: STATUS_WORDS[s] ?? s };\n return { tone: s.tone ?? \"neutral\", label: s.label ?? STATUS_WORDS[s.tone] ?? \"\" };\n}\n\n// One number: the label row (with a status dot or a corner change), the\n// value, then an optional goal. Used bare in a strip cell and inside a card.\nfunction StatBody({ item, deltaPlacement, deltaVariant, size }) {\n const status = statusOf(item);\n const d = deltaProps(item, deltaVariant);\n const corner = d && deltaPlacement === \"corner\" && !status;\n const unit = [item.unit, item.previous !== undefined && item.previous !== null ? `from ${item.previous}` : null].filter(Boolean).join(\" \");\n const goal = item.goal;\n return (\n <div className={cx(\"min-w-0\", WRAP)}>\n <div className=\"flex min-w-0 items-start justify-between gap-3\">\n <span className=\"min-w-0 truncate text-sm font-medium text-muted-foreground\">\n {item.label}\n {item.sublabel ? <span className=\"font-normal\"> · {item.sublabel}</span> : null}\n </span>\n {status ? <StatusDot tone={status.tone} label={status.label} size=\"sm\" className=\"shrink-0\" /> : null}\n {corner ? <Delta {...d} className=\"shrink-0\" /> : null}\n </div>\n <Stat value={item.value} unit={unit || undefined} delta={corner ? undefined : d ?? undefined} caption={item.caption} size={size} />\n {goal && typeof goal.value === \"number\" ? (\n <Meter\n className=\"mt-4\"\n size=\"thin\"\n value={goal.value}\n max={goal.max ?? 100}\n label={goal.label ?? \"Goals achieved\"}\n valueText={goal.valueText ?? `${goal.value}/${goal.max ?? 100}`}\n valueTone=\"neutral\"\n tone={goal.tone ?? (status && status.tone !== \"neutral\" ? status.tone : undefined)}\n direction=\"higher\"\n />\n ) : null}\n </div>\n );\n}\n\nfunction hasTrend(item) {\n return Array.isArray(item.trend) && item.trend.length > 0;\n}\n\nfunction StatCard({ item, deltaPlacement, deltaVariant, size, linkLabel }) {\n const link = item.href || item.onSelect;\n return (\n <Card as=\"div\" padding=\"none\" className=\"flex min-w-0 flex-col\">\n <div className=\"p-4\">\n <StatBody item={item} deltaPlacement={deltaPlacement} deltaVariant={deltaVariant} size={size} />\n </div>\n {hasTrend(item) ? (\n <Sparkline\n data={item.trend}\n label={item.trendLabel ?? `${item.label}, trend`}\n tone={trendTone(item)}\n height={48}\n dot={false}\n className=\"-mt-1\"\n />\n ) : null}\n {link ? (\n <div className=\"mt-auto flex justify-end border-t border-border px-4 py-2.5\">\n <MoreLink item={item} fallback={linkLabel} />\n </div>\n ) : null}\n </Card>\n );\n}\n\nfunction Loading({ variant, count, cols }) {\n const n = Array.from({ length: count }, (_, i) => i);\n if (variant === \"list\") {\n return (\n <div aria-busy=\"true\" aria-label=\"Loading\" className=\"grid gap-3 rounded-lg border border-border bg-card p-4\">\n {n.map((i) => (\n <div key={i} className=\"flex items-center justify-between gap-4\">\n <Skeleton className=\"h-4 w-28\" />\n <Skeleton className=\"h-4 w-20\" />\n </div>\n ))}\n </div>\n );\n }\n const tile = (i) => (\n <div key={i} className=\"grid gap-3\">\n <Skeleton className=\"h-4 w-24\" />\n <Skeleton className=\"h-7 w-32\" />\n {variant === \"usage\" ? <Skeleton className=\"h-2 w-full\" /> : null}\n </div>\n );\n if (variant === \"strip\") {\n return (\n <div aria-busy=\"true\" aria-label=\"Loading\">\n <StatGroup columns={cols}>{n.map(tile)}</StatGroup>\n </div>\n );\n }\n return (\n <div aria-busy=\"true\" aria-label=\"Loading\" className={cx(\"grid grid-cols-1 gap-4\", SM_COLS[cols], LG_COLS[cols])}>\n {n.map((i) => (\n <Card key={i} as=\"div\">\n {tile(i)}\n </Card>\n ))}\n </div>\n );\n}\n\nexport function StatCards({\n items,\n variant = \"strip\",\n columns,\n title,\n description,\n actions,\n label,\n // stats\n deltaPlacement,\n deltaVariant,\n size,\n linkLabel = \"View more\",\n // usage\n meter = \"bar\",\n format,\n warnAt = 90,\n onLimitChange,\n limitLabel = \"Change limit\",\n limitHint,\n // states\n loading = false,\n loadingCount = 4,\n error,\n onRetry,\n emptyTitle = \"No numbers yet\",\n emptyBody,\n emptyAction,\n className,\n}) {\n const list = (items ?? []).filter(Boolean);\n const cols = Math.min(4, Math.max(1, columns ?? Math.min(list.length || loadingCount || 1, 4)));\n const editor = useLimitEditor(onLimitChange, format);\n // Each shape's own defaults, as the source drew it.\n const placement = deltaPlacement ?? (variant === \"strip\" ? \"corner\" : \"inline\");\n const dVariant = deltaVariant ?? (variant === \"list\" ? \"badge\" : \"text\");\n const statSize = size ?? (variant === \"strip\" ? \"md\" : \"lg\");\n\n let body;\n if (loading) {\n body = <Loading variant={variant} count={Math.max(1, loadingCount)} cols={cols} />;\n } else if (!list.length) {\n body = <Empty icon=\"chart\" title={emptyTitle} body={emptyBody} action={emptyAction} className=\"rounded-lg border border-dashed border-border\" />;\n } else if (variant === \"list\") {\n body = (\n <ItemGroup variant=\"outline\" aria-label={label ?? title}>\n {list.map((item, i) => (\n <Item\n key={keyOf(item, i)}\n size=\"sm\"\n title={item.label}\n href={item.href}\n onClick={item.onSelect && !item.href ? () => item.onSelect(item) : undefined}\n meta={\n <span className=\"flex items-center gap-3 text-sm\">\n <span data-nums className=\"font-medium text-foreground\">\n {item.value}\n {item.unit ? <span className=\"ml-1 text-xs font-normal text-muted-foreground\">{item.unit}</span> : null}\n </span>\n {deltaProps(item, dVariant) ? (\n <>\n <span aria-hidden=\"true\" className=\"h-4 w-px bg-border\" />\n <Delta {...deltaProps(item, dVariant)} />\n </>\n ) : null}\n </span>\n }\n />\n ))}\n </ItemGroup>\n );\n } else if (variant === \"usage\") {\n body = (\n <div role=\"list\" aria-label={label ?? title} className={cx(\"grid grid-cols-1 gap-4\", SM_COLS[cols], LG_COLS[cols])}>\n {list.map((item, i) => (\n <div role=\"listitem\" key={keyOf(item, i)} className=\"min-w-0\">\n <UsageTile\n item={item}\n meter={item.meter ?? meter}\n format={item.format ?? format}\n warnAt={item.warnAt ?? warnAt}\n linkLabel={linkLabel}\n limitLabel={item.limitLabel ?? limitLabel}\n onEditLimit={onLimitChange && item.editable !== false ? () => editor.open(item) : null}\n />\n </div>\n ))}\n </div>\n );\n } else if (variant === \"cards\") {\n body = (\n <div role=\"list\" aria-label={label ?? title} className={cx(\"grid grid-cols-1 gap-4\", SM_COLS[cols], LG_COLS[cols])}>\n {list.map((item, i) => (\n <div role=\"listitem\" key={keyOf(item, i)} className=\"flex min-w-0 flex-col [&>*]:flex-1\">\n <StatCard item={item} deltaPlacement={placement} deltaVariant={dVariant} size={statSize} linkLabel={linkLabel} />\n </div>\n ))}\n </div>\n );\n } else {\n body = (\n <StatGroup columns={cols} label={label ?? title}>\n {list.map((item, i) => (\n <div key={keyOf(item, i)} className=\"grid gap-2\">\n <StatBody item={item} deltaPlacement={placement} deltaVariant={dVariant} size={statSize} />\n {hasTrend(item) ? (\n <Sparkline data={item.trend} label={item.trendLabel ?? `${item.label}, trend`} tone={trendTone(item)} height={32} dot={false} />\n ) : null}\n {item.href || item.onSelect ? (\n <div>\n <MoreLink item={item} fallback={linkLabel} />\n </div>\n ) : null}\n </div>\n ))}\n </StatGroup>\n );\n }\n\n return (\n <section aria-label={title ? undefined : label} className={cx(\"grid min-w-0 gap-4\", className)}>\n {title || description || actions ? (\n <header className=\"flex flex-wrap items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n {title ? <h2 className=\"text-lg font-semibold leading-tight text-foreground\">{title}</h2> : null}\n {description ? <div className=\"mt-1 text-sm text-muted-foreground\">{description}</div> : null}\n </div>\n {actions ? <div className=\"flex shrink-0 flex-wrap gap-2\">{actions}</div> : null}\n </header>\n ) : null}\n {error ? (\n <Notice\n tone=\"danger\"\n action={\n onRetry ? (\n <Button size=\"sm\" icon=\"refresh\" onClick={onRetry}>\n Retry\n </Button>\n ) : null\n }\n >\n {error}\n </Notice>\n ) : null}\n {body}\n <LimitSheet editor={editor} label={limitLabel} hint={limitHint} />\n </section>\n );\n}\n"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"path": "registry/blocks/stat-cards/usage.jsx",
|
|
27
|
+
"type": "registry:component",
|
|
28
|
+
"target": "@components/makersclaw/blocks/stat-cards/usage.jsx",
|
|
29
|
+
"content": "\"use client\";\n\nimport { useId, useState } from \"react\";\nimport { Button, Card, Delta, Distribution, Field, Icon, InputGroup, Meter, Notice, Sheet, Stat, cx } from \"../../ui\";\n\n// The `usage` shape of StatCards: an amount against a limit, one tile\n// each — a bar or a ring, or a split by kind with what is left, a warning\n// line near the cap, an action along the tile's foot, and a limit the\n// founder can change in a Sheet (the save goes out through\n// onLimitChange and may fail). The LIST of meters with a summary strip is\n// not this block: that is a different job.\n\nconst EN = \"en-US\";\nconst plain = (n) => (typeof n === \"number\" ? n.toLocaleString(EN, { maximumFractionDigits: 2 }) : String(n ?? \"\"));\nconst finite = (n) => typeof n === \"number\" && Number.isFinite(n);\nconst NOTE_TONES = { muted: \"text-muted-foreground\", success: \"text-success\", warning: \"text-warning\", danger: \"text-destructive\", info: \"text-info\" };\n\n// \"View more →\": an href is a link, onSelect a button.\nexport function MoreLink({ item, fallback }) {\n const text = item.linkLabel ?? fallback;\n const common = { variant: \"link\", size: \"sm\", \"aria-label\": `${text}: ${item.label}` };\n const body = (\n <>\n {text}\n <Icon name=\"arrow-right\" size={14} className=\"ml-1 inline-block align-[-0.125em]\" />\n </>\n );\n if (item.href) return <Button href={item.href} {...common}>{body}</Button>;\n return (\n <Button onClick={() => item.onSelect?.(item)} {...common}>\n {body}\n </Button>\n );\n}\n\n// The numbers a tile shows, worked out once.\nfunction readUsage(item, format, warnAt) {\n const fmt = format ?? plain;\n const u = item.unit ? ` ${item.unit}` : \"\";\n const used = finite(item.used) ? item.used : 0;\n const capped = finite(item.limit) && item.limit > 0;\n const pct = capped ? (used / item.limit) * 100 : null;\n const usedText = item.value ?? `${fmt(used)}${u}`;\n const limitText = item.limitText ?? (capped ? `${fmt(item.limit)}${u}` : \"Unlimited\");\n const near = pct !== null && pct >= warnAt;\n const noteTone = item.noteTone ?? (pct !== null && pct > 100 ? \"danger\" : near ? \"warning\" : \"muted\");\n // A limit is shown when there is one, when it is named (\"Unlimited\",\n // \"$150 budget\"), or when limit is null: no cap, said so.\n const showLimit = capped || item.limit === null || item.limitText !== undefined;\n return { fmt, u, used, capped, pct, usedText, limitText, near, noteTone, showLimit };\n}\n\nconst pctText = (pct) => `${pct < 10 && pct > 0 ? Math.round(pct * 10) / 10 : Math.round(pct)}%`;\n\nexport function UsageTile({ item, meter, format, warnAt, linkLabel, limitLabel, onEditLimit }) {\n const r = readUsage(item, format, warnAt);\n const parts = Array.isArray(item.parts) && item.parts.length ? item.parts : null;\n const thresholds = item.thresholds ?? [{ at: warnAt, tone: \"warning\" }];\n const ring = meter === \"ring\" && r.capped && !parts;\n const d = item.delta === null || item.delta === undefined ? null : typeof item.delta === \"number\" ? { value: item.delta, suffix: \"%\" } : item.delta;\n const buttons = [\n item.action ? { key: \"action\", label: item.action.label, icon: item.action.icon, run: () => item.action.onSelect?.(item) } : null,\n onEditLimit ? { key: \"limit\", label: limitLabel, icon: \"pencil\", run: onEditLimit } : null,\n ].filter(Boolean);\n\n return (\n <Card as=\"div\" padding=\"none\" className=\"flex h-full min-w-0 flex-col\">\n <div className=\"grid flex-1 content-start gap-3 p-4 [overflow-wrap:anywhere]\">\n {ring ? (\n <Meter\n shape=\"ring\"\n size=\"md\"\n value={r.used}\n max={item.limit}\n label={item.label}\n format={r.fmt}\n unit={item.unit}\n valueText={item.valueText ?? `${r.usedText} of ${r.limitText}`}\n caption={item.caption}\n thresholds={thresholds}\n />\n ) : (\n <>\n <div className=\"flex min-w-0 items-start justify-between gap-3\">\n <span className=\"min-w-0 truncate text-sm font-medium text-muted-foreground\">{item.label}</span>\n {d ? <Delta invert={item.invert} variant=\"badge\" {...d} className=\"shrink-0\" /> : null}\n </div>\n <Stat value={r.usedText} unit={r.showLimit ? `/ ${r.limitText}` : item.suffix} caption={item.caption} size=\"md\" className=\"-mt-2\" />\n {parts ? (\n <Distribution\n segments={parts}\n total={r.capped ? item.limit : undefined}\n restLabel={r.capped ? item.restLabel ?? \"Free\" : undefined}\n legend\n size=\"thin\"\n label={`${item.label}: ${r.usedText} of ${r.limitText}`}\n formatValue={(v) => `${r.fmt(v)}${r.u}`}\n formatLegend={({ value, share, rest }) => (r.capped || rest ? `${r.fmt(value)}${r.u}` : `${r.fmt(value)}${r.u} · ${share}`)}\n />\n ) : r.capped ? (\n <Meter\n value={r.used}\n max={item.limit}\n format={r.fmt}\n unit={item.unit}\n valueText={null}\n aria-label={item.label}\n thresholds={thresholds}\n caption={`${pctText(r.pct)} used`}\n />\n ) : null}\n </>\n )}\n {item.note ? (\n <p className={cx(\"text-sm\", NOTE_TONES[r.noteTone] ?? NOTE_TONES.muted)}>\n {r.noteTone === \"warning\" || r.noteTone === \"danger\" ? <Icon name=\"warning\" size={14} className=\"mr-1 inline-block align-[-0.125em]\" /> : null}\n {item.note}\n </p>\n ) : null}\n </div>\n {item.href || item.onSelect ? (\n <div className=\"flex justify-end border-t border-border px-4 py-2.5\">\n <MoreLink item={item} fallback={linkLabel} />\n </div>\n ) : null}\n {buttons.length ? (\n <div className=\"flex border-t border-border bg-muted/40\">\n {buttons.map((b, i) => (\n <Button\n key={b.key}\n variant=\"quiet\"\n align=\"start\"\n icon={b.icon}\n onClick={b.run}\n aria-label={`${b.label}: ${item.label}`}\n className={cx(\"min-w-0 flex-1 rounded-none\", i > 0 && \"border-l border-l-border\")}\n >\n <span className=\"truncate\">{b.label}</span>\n </Button>\n ))}\n </div>\n ) : null}\n </Card>\n );\n}\n\n// The limit editor's state: which tile, the draft, the save in flight and\n// why it failed.\nexport function useLimitEditor(onLimitChange, format) {\n const [item, setItem] = useState(null);\n const [draft, setDraft] = useState(\"\");\n const [saving, setSaving] = useState(false);\n const [error, setError] = useState(null);\n const [invalid, setInvalid] = useState(null);\n\n function open(it) {\n setItem(it);\n setDraft(finite(it.limit) ? String(it.limit) : \"\");\n setError(null);\n setInvalid(null);\n }\n function close() {\n if (saving) return;\n setItem(null);\n }\n async function save() {\n const n = Number(String(draft).replace(/[,\\s]/g, \"\"));\n if (!String(draft).trim() || !Number.isFinite(n) || n <= 0) {\n setInvalid(\"Enter a number above zero.\");\n return;\n }\n setInvalid(null);\n setError(null);\n setSaving(true);\n let result;\n try {\n result = await onLimitChange?.(item, n);\n } catch (e) {\n result = { ok: false, error: e?.message ?? String(e) };\n }\n setSaving(false);\n if (result && result.ok === false) {\n setError(result.error ?? \"The limit was not changed.\");\n return;\n }\n setItem(null);\n }\n return { item, draft, setDraft, saving, error, invalid, open, close, save, format };\n}\n\nexport function LimitSheet({ editor, label, hint }) {\n const id = useId();\n const item = editor.item;\n const unit = item?.unit;\n const fmt = editor.format ?? plain;\n return (\n <Sheet\n open={Boolean(item)}\n onClose={editor.close}\n side=\"center\"\n title={item ? `${item.limitLabel ?? label}: ${item.label}` : label}\n description={\n item\n ? item.limitHint ?? hint ?? `Now ${finite(item.limit) ? `${fmt(item.limit)}${unit ? ` ${unit}` : \"\"}` : \"unlimited\"}; ${fmt(finite(item.used) ? item.used : 0)}${unit ? ` ${unit}` : \"\"} used.`\n : undefined\n }\n footer={\n <>\n <Button onClick={editor.close} disabled={editor.saving}>\n Cancel\n </Button>\n <Button variant=\"primary\" type=\"submit\" form={`${id}-form`} loading={editor.saving}>\n Save\n </Button>\n </>\n }\n >\n <form\n id={`${id}-form`}\n className=\"grid gap-3\"\n onSubmit={(e) => {\n e.preventDefault();\n editor.save();\n }}\n >\n {editor.error ? <Notice tone=\"danger\">{editor.error}</Notice> : null}\n <Field id={`${id}-limit`} label={item?.limitField ?? \"New limit\"} error={editor.invalid}>\n <InputGroup\n id={`${id}-limit`}\n inputMode=\"decimal\"\n autoComplete=\"off\"\n value={editor.draft}\n onChange={(e) => editor.setDraft(e.target.value)}\n prefix={item?.limitPrefix}\n suffix={unit}\n invalid={Boolean(editor.invalid)}\n aria-describedby={editor.invalid ? `${id}-limit-error` : undefined}\n />\n </Field>\n </form>\n </Sheet>\n );\n}\n"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"path": "registry/blocks/stat-cards/README.md",
|
|
33
|
+
"type": "registry:file",
|
|
34
|
+
"target": "@components/makersclaw/blocks/stat-cards/README.md",
|
|
35
|
+
"content": "# Stat cards\n\nThe few numbers at the top of a dashboard, each with its name and what\nchanged. Four shapes over one list of items: a bordered `strip`; `cards`,\none per number, with a change in the corner or after the value, a \"from\"\nvalue, a status dot and a goal meter, a sparkline, a \"View more\" footer;\n`usage`, one tile per amount against its limit, as a bar or a ring, a\nsplit by kind with what is free, a warning line near the cap, an action\nalong the foot and a limit the founder can change; and a plain `list` of\nvalues with their change. Loading, empty and error states are built in.\n\n```jsx\nimport { StatCards } from \"../components/blocks/stat-cards\"; // a Makersclaw app (\"./blocks/stat-cards\" from components/)\nimport { StatCards } from \"@/components/makersclaw/blocks/stat-cards\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component, and\nload the numbers in the server surface (README §4). A sparkline is drawn\nby the free `chart` block, so `makersclaw add stat-cards` lays that one\ntoo.\n\n## The data\n\nOne item per number, already formatted where it is shown as text:\n\n```js\n{\n id: \"mrr\", // key; default the label\n label: \"Monthly recurring revenue\",\n value: \"$48.6K\", // a string you formatted, or a number (gets thousands separators)\n unit: \"ms\", // after the value, smaller and muted\n sublabel: \"ORBT\", // after the label, muted: a ticker, a region code\n delta: 5.2, // a percent; or Delta's props: { value, suffix, display, label, invert }\n invert: true, // lower is better (churn, cost): down turns green\n previous: \"$46.2K\", // shown after the value as \"from $46.2K\"\n caption: \"Paid plans only\", // a muted line under the value\n status: \"warning\", // a dot and a word at the top right: success · warning · danger · info,\n // or { tone, label: \"Needs attention\" }\n goal: { value: 3, max: 5, label: \"Goals achieved\" }, // a thin meter in the status tone\n trend: [12, 14, 13, 17], // a sparkline under the value (numbers, oldest first)\n trendLabel: \"MRR, last 16 weeks\", // what a screen reader hears for it\n trendTone: \"chart-2\", // default: green when the change is good, red when bad\n href: \"/revenue\", // a \"View more →\" footer link; or onSelect(item) for a button\n linkLabel: \"View region\",\n}\n```\n\nIn `usage`, an item is an amount against a limit:\n\n```js\n{\n id: \"egress\",\n label: \"Egress\",\n used: 188, // a number, in the limit's unit\n limit: 200, // the cap; null means none (\"/ Unlimited\"); left out, no limit is shown\n unit: \"GB\",\n value: \"188 GB\", // how `used` reads; default the number and the unit\n limitText: \"$150 budget\", // how the limit reads; default the number and the unit\n suffix: \"this month\", // after the value when there is no limit\n format: (n) => `$${n}`, // every number shown, default thousands separators\n parts: [{ label: \"Writes\", value: 7410000 }, { label: \"Reads\", value: 2410000 }],\n // a split bar with a legend in place of the meter; with a\n // limit, the rest shows as \"Free\" (restLabel)\n note: \"Past 200 GB is billed per GB.\", // a line under the bar: warning near the cap\n noteTone: \"muted\", // override: muted · success · warning · danger · info\n delta: 9.5, // a badge beside the label\n thresholds: [{ at: 75, tone: \"warning\" }], // default: warning at warnAt\n action: { label: \"Upgrade\", icon: \"zap\", onSelect: (item) => … }, // a button along the foot\n editable: false, // no \"Change limit\" button on this tile\n limitLabel: \"Change budget\", limitPrefix: \"$\", // this tile's limit button and field\n meter: \"ring\", // this tile's shape, over the block's `meter`\n}\n```\n\nOver the limit the bar stays full and turns red; a reader hears \"over by\".\n\n## Props\n\n- `items` — the numbers, as above. Required.\n- `variant` — `\"strip\"` (default) · `\"cards\"` · `\"usage\"` · `\"list\"`.\n- `columns` — how many across from `lg` up, 1 to 4; default the number of\n items, at most 4. Cards and usage tiles are one column on a phone and\n two from `sm`; a strip is two on a phone.\n- `title` · `description` · `actions` — a header over the numbers\n (\"Plan usage\", \"You are on the Team plan. Compare plans\", a button).\n- `label` — the group's name for a screen reader when there is no title.\n\nStats (`strip`, `cards`, `list`)\n\n- `deltaPlacement` — `\"inline\"` (after the value) or `\"corner\"` (at the\n label's right). Default corner in a strip, inline elsewhere. A tile with\n a `status` keeps its change inline.\n- `deltaVariant` — `\"text\"` or `\"badge\"` (a tinted pill). Default badge\n in a list, text elsewhere.\n- `size` — the value's size: `\"sm\"` · `\"md\"` (default in a strip) · `\"lg\"`\n (default in cards).\n- `linkLabel` — the footer link's words, default `\"View more\"`.\n\nUsage\n\n- `meter` — `\"bar\"` (default) or `\"ring\"`, for every tile without its own\n `meter`. A tile with `parts` is always a split bar.\n- `format(n)` — every number shown, unless a tile has its own.\n- `warnAt` — the percent of the limit where the bar and the note turn\n warning, default 90.\n- `onLimitChange(item, limit)` — adds a \"Change limit\" button to each tile\n (not those with `editable: false`). It opens a dialog with the limit in\n a number field; Save calls this with the new number. Return (or resolve\n to) `{ ok: false, error }`, or throw, and the dialog stays open and says\n why; anything else closes it. The block does not change the number on\n screen: pass the new `limit` back in `items`.\n- `limitLabel` — the button's and dialog's words, default `\"Change limit\"`.\n- `limitHint` — the dialog's description, default the current limit and\n what is used.\n\nStates\n\n- `loading` — skeleton tiles in the shape of the variant; `loadingCount`\n of them (default 4).\n- `error` — a danger notice above the numbers (they still show, if any);\n `onRetry` adds a Retry button to it.\n- `emptyTitle` · `emptyBody` · `emptyAction` — when `items` is empty.\n- `className` — layout around it (width, margin). Never colour.\n\n## Example\n\n```jsx\n// components/Overview.jsx — client: the numbers the server read\n\"use client\";\nimport { StatCards } from \"./blocks/stat-cards\";\nimport { updateRow } from \"../lib/actions\";\n\nexport default function Overview({ kpis, usage }) {\n return (\n <div className=\"grid gap-6\">\n <StatCards\n items={[\n { id: \"mrr\", label: \"MRR\", value: kpis.mrr, delta: kpis.mrrChange, trend: kpis.mrrWeeks, href: \"/revenue\" },\n { id: \"churn\", label: \"Logo churn\", value: kpis.churn, delta: kpis.churnChange, invert: true },\n { id: \"trials\", label: \"Active trials\", value: kpis.trials, previous: kpis.trialsLastWeek, delta: kpis.trialsChange },\n ]}\n variant=\"cards\"\n />\n <StatCards\n variant=\"usage\"\n title=\"Plan usage\"\n items={usage.map((u) => ({ id: u.id, label: u.name, used: u.used, limit: u.limit, unit: u.unit }))}\n onLimitChange={(item, limit) => updateRow({ source: \"public.quotas\", id: item.id, patch: { limit } })}\n limitLabel=\"Change budget\"\n />\n </div>\n );\n}\n```\n\n## What it will not do\n\n- Fetch or compute: numbers come in formatted, changes already worked out.\n- Draw a chart worth reading on its own: that is the `chart` block.\n- Score one subject against thresholds with an overall status, or list\n many meters under a shared summary: those are other blocks.\n- Colour a number for decoration: tones are for status (a change's\n direction, a tile near its cap, a status dot).\n\nA tile it cannot be is your own component in `components/`, built from\nthe kit's `Stat`, `Meter` and `Distribution` — not an edit to this block.\nIts files are fixed: `npm run check` refuses a change, and `makersclaw add\nstat-cards` restores them.\n\n## Credits\n\nPorted from blocks.so's stats (https://github.com/ephraimduncan/blocks,\nMIT): a rewrite on the Makersclaw kit, not their code.\n"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"categories": [
|
|
39
|
+
"stats",
|
|
40
|
+
"kpi",
|
|
41
|
+
"metrics",
|
|
42
|
+
"numbers",
|
|
43
|
+
"dashboard",
|
|
44
|
+
"overview",
|
|
45
|
+
"delta",
|
|
46
|
+
"change",
|
|
47
|
+
"trend",
|
|
48
|
+
"sparkline",
|
|
49
|
+
"usage",
|
|
50
|
+
"limit",
|
|
51
|
+
"quota",
|
|
52
|
+
"plan",
|
|
53
|
+
"budget",
|
|
54
|
+
"goal",
|
|
55
|
+
"status",
|
|
56
|
+
"ring",
|
|
57
|
+
"meter"
|
|
58
|
+
],
|
|
59
|
+
"docs": "Installed to components/makersclaw/blocks/stat-cards. Import: import { StatCards } from \"@/components/makersclaw/blocks/stat-cards\". Props and an example: components/makersclaw/blocks/stat-cards/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
60
|
+
"meta": {
|
|
61
|
+
"makersclaw": {
|
|
62
|
+
"version": "0.3.0",
|
|
63
|
+
"useWhen": "The few numbers at the top of a dashboard or a report: revenue, users, conversion, spend against budget, seats and storage against a plan. Not for a trend worth reading on its own (a chart), many rows of records (a table), or one subject scored against thresholds (a scorecard)."
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
package/r/task-list.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "task-list",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Task list",
|
|
6
|
+
"description": "The List view of work, like ClickUp's or Linear's: tasks under headings that fold (group by status, assignee, priority, project, due date or any field), a round check to complete each, the title edited in place, subtasks indented under their parent with their own fold, the fields you choose (assignees, due date, priority, tags, project, estimate) as inline pickers, a quick-add row at the end of every group, drag to reorder and to move between groups (saving the new status, assignee or due date), multi-select with a bulk bar (status, assign, due, priority, delete with undo), a toolbar with search, filter chips, group by, sort and fields, and friendly empty states with starter templates. One async onChange saves every change; failures roll back with a notice.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/core.json",
|
|
9
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/task-model.json"
|
|
10
|
+
],
|
|
11
|
+
"dependencies": [],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "registry/blocks/task-list/index.js",
|
|
15
|
+
"type": "registry:component",
|
|
16
|
+
"target": "@components/makersclaw/blocks/task-list/index.js",
|
|
17
|
+
"content": "// The TaskList block — `makersclaw add task-list` (it brings task-model).\n// Fixed: compose it through its props (README.md beside this file); a\n// list it cannot be is your own component in components/, built from\n// task-model's parts.\nexport { TaskList } from \"./task-list\";\n"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"path": "registry/blocks/task-list/task-list.jsx",
|
|
21
|
+
"type": "registry:component",
|
|
22
|
+
"target": "@components/makersclaw/blocks/task-list/task-list.jsx",
|
|
23
|
+
"content": "\"use client\";\n\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { Button, Checkbox, Empty, Icon, IconButton, Input, Menu, Notice, Skeleton, cx, toast } from \"../../ui\";\nimport {\n AssigneeStack,\n CompleteToggle,\n DEFAULT_VIEW,\n DragHandle,\n DueLabel,\n GroupLabel,\n LIFTED,\n NONE,\n PriorityFlag,\n ProgressLine,\n SortableGroup,\n StatusPill,\n SubtaskCount,\n TagList,\n TaskDragProvider,\n ViewToolbar,\n applyView,\n createTaskContext,\n deleteChange,\n draftTask,\n flattenTree,\n formatDay,\n isDone,\n movePatch,\n placeChange,\n subtaskCounts,\n sumOf,\n toggleDoneChange,\n useDropTarget,\n useSortableTask,\n useTaskChanges,\n} from \"../task-model\";\n\n// TaskList — the List view of work: tasks under headings that fold (by\n// status, assignee, priority, project, due date… — any field), each row\n// a round check to complete it, its title edited in place, its subtasks\n// indented under it, the fields the view shows (people, due date,\n// priority…) as pickers, and a menu. A quick-add row ends every group;\n// rows drag to reorder and to another group (which saves the group's\n// field: a new status, a new assignee, a new due date). Tick several and\n// a bar acts on them together; a delete can be undone from its toast.\n//\n// Everything saves through one async `onChange(change)` answering { ok }\n// (the change shapes are task-model's); the list shows each change at\n// once and takes it back, with a notice, when the save fails.\n//\n// README.md beside this file lists every prop.\n\nfunction useViewState(value, defaultValue, onChange) {\n const [own, setOwn] = useState(() => ({ ...DEFAULT_VIEW, ...defaultValue }));\n const controlled = value !== undefined;\n const view = controlled ? { ...DEFAULT_VIEW, ...value } : own;\n const set = useCallback(\n (next) => {\n if (!controlled) setOwn(next);\n onChange?.(next);\n },\n [controlled, onChange],\n );\n return [view, set];\n}\n\nconst sid = (group, id) => `${group}::${id}`;\n\nexport function TaskList({\n tasks,\n statuses,\n people,\n projects,\n fields,\n today,\n weekStartsOn,\n onChange,\n onOpen,\n view: viewProp,\n defaultView,\n onViewChange,\n toolbar = true,\n toolbarExtra,\n addLabel = \"Add task\",\n starters,\n emptyTitle = \"No tasks yet\",\n emptyBody = \"Add the first thing that needs doing. Press Enter to add the next one.\",\n loading = false,\n error: readError,\n label = \"Tasks\",\n className,\n}) {\n const uid = useId();\n const regionRef = useRef(null);\n const ctx = useMemo(\n () => createTaskContext({ statuses, people, projects, fields, today, weekStartsOn }),\n [statuses, people, projects, fields, today, weekStartsOn],\n );\n const editable = typeof onChange === \"function\";\n const { tasks: all, commit, error, clearError } = useTaskChanges(tasks, onChange);\n const [view, setView] = useViewState(viewProp, defaultView, onViewChange);\n const shown = useMemo(() => applyView(all, view, ctx), [all, view, ctx]);\n const counts = useMemo(() => subtaskCounts(all, ctx), [all, ctx]);\n const titleOf = useMemo(() => new Map(all.map((t) => [t.id, t.title])), [all]);\n const manual = !view.sort?.length;\n const by = view.groupBy && view.groupBy !== \"none\" ? view.groupBy : null;\n const show = new Set(view.fields ?? []);\n\n const [folded, setFolded] = useState(() => new Set());\n const [collapsed, setCollapsed] = useState(() => new Set());\n const [selected, setSelected] = useState(() => new Set());\n const lastPicked = useRef(null);\n const [editing, setEditing] = useState(null);\n const [adding, setAdding] = useState(null); // { group, parentId? }\n const [overGroup, setOverGroup] = useState(null);\n const [announce, setAnnounce] = useState(\"\");\n const [started, setStarted] = useState(false);\n\n // Drop selections of tasks that are gone.\n useEffect(() => {\n setSelected((s) => {\n if (!s.size) return s;\n const ids = new Set(all.map((t) => t.id));\n const next = new Set([...s].filter((id) => ids.has(id)));\n return next.size === s.size ? s : next;\n });\n }, [all]);\n\n // Rows per group, nested, in reading order: the model for the screen,\n // for shift-click ranges and for the arrow keys.\n const groups = useMemo(\n () =>\n shown.groups.map((g) => {\n const rows = flattenTree(g.tasks, { collapsed });\n // Each top-level row with the rows under it: dragged as one.\n const blocks = [];\n for (const r of rows) {\n if (r.depth === 0) blocks.push({ root: r, rest: [] });\n else blocks[blocks.length - 1]?.rest.push(r);\n }\n return { ...g, rows, blocks, roots: blocks.map((b) => b.root.task) };\n }),\n [shown.groups, collapsed],\n );\n const order = useMemo(() => groups.flatMap((g) => (folded.has(g.key) ? [] : g.rows.map((r) => r.task.id))), [groups, folded]);\n const selectMode = selected.size > 0;\n\n const say = (text) => setAnnounce(text);\n const name = (t) => `“${t.title || \"Untitled\"}”`;\n\n function groupPatch(key) {\n if (!by) return {};\n return movePatch({ assignees: [], tags: [] }, by, NONE, key, ctx);\n }\n\n function update(task, patch, words) {\n return commit({ type: \"update\", id: task.id, patch }, { label: name(task) }).then((r) => {\n if (r.ok !== false && words) say(words);\n return r;\n });\n }\n\n async function create(title, group, parentId) {\n const patch = groupPatch(group);\n if (patch === null) return { ok: false };\n const g = groups.find((x) => x.key === group);\n const siblings = parentId ? all.filter((t) => t.parentId === parentId) : g?.roots ?? [];\n const task = draftTask(title, ctx, { ...patch, ...(parentId ? { parentId } : {}) }, { after: siblings[siblings.length - 1] });\n if (parentId) setCollapsed((s) => (s.has(parentId) ? new Set([...s].filter((x) => x !== parentId)) : s));\n const r = await commit({ type: \"create\", task }, { label: name(task) });\n if (r.ok !== false) say(`Added ${name(task)}${g && by ? ` to ${g.label}` : \"\"}`);\n return r;\n }\n\n function remove(ids) {\n const change = deleteChange(all, ids);\n const n = ids.length;\n const one = n === 1 ? all.find((t) => t.id === ids[0]) : null;\n setSelected(new Set());\n return commit(change, { label: one ? name(one) : `${n} tasks`, undo: one ? `Deleted ${name(one)}` : `Deleted ${n} tasks` });\n }\n\n function toggleSelect(id, { range = false } = {}) {\n setSelected((s) => {\n const next = new Set(s);\n if (range && lastPicked.current && order.includes(lastPicked.current)) {\n const a = order.indexOf(lastPicked.current);\n const b = order.indexOf(id);\n const [lo, hi] = a < b ? [a, b] : [b, a];\n for (const x of order.slice(lo, hi + 1)) next.add(x);\n } else if (next.has(id)) next.delete(id);\n else next.add(id);\n return next;\n });\n lastPicked.current = id;\n }\n\n function onDragEnd({ active, over }) {\n setOverGroup(null);\n if (!active || !over || !editable) return;\n const task = active.data.task;\n const from = active.data.group;\n const to = over.data.group;\n if (!task || to == null) return;\n const g = groups.find((x) => x.key === to);\n if (!g) return;\n const patch = from === to ? {} : movePatch(task, by, from, to, ctx);\n if (patch === null) {\n toast.info(`${name(task)} stays where it was`, { description: `Tasks cannot be moved into ${g.label}. Give it a due date instead.` });\n say(`${name(task)} cannot go to ${g.label}`);\n return;\n }\n let change;\n if (manual) {\n const ids = g.roots.map((t) => t.id);\n const list = g.roots.filter((t) => t.id !== task.id);\n let index = list.length;\n if (over.data.task) {\n const overIndex = ids.indexOf(over.data.task.id);\n const activeIndex = ids.indexOf(task.id);\n index = from === to && activeIndex >= 0 && activeIndex < overIndex ? overIndex : list.findIndex((t) => t.id === over.data.task.id);\n if (index < 0) index = list.length;\n }\n if (from === to && ids.indexOf(task.id) === index) return;\n change = placeChange(list, task, index, patch);\n } else {\n if (from === to) return;\n change = { type: \"update\", id: task.id, patch };\n }\n commit(change, { label: name(task) }).then((r) => {\n if (r.ok !== false) say(from === to ? `${name(task)} moved` : `${name(task)} moved to ${g.label}`);\n });\n }\n\n const startAdding = (group, parentId) => {\n setStarted(true);\n if (group && folded.has(group)) setFolded((s) => new Set([...s].filter((x) => x !== group)));\n setAdding({ group: group ?? groups.find((g) => groupPatch(g.key) !== null)?.key ?? \"all\", parentId });\n };\n\n const empty = !loading && all.length === 0 && !started;\n const noMatches = !loading && all.length > 0 && shown.tasks.length === 0;\n // Only \"hide done\" is on and it hid everything: that is good news.\n const allDone = noMatches && view.hideDone && !view.search && !Object.values(view.filters ?? {}).some((v) => v?.length);\n const clearView = () => setView({ ...view, search: \"\", filters: {}, hideDone: false });\n\n return (\n <div className={cx(\"grid min-w-0 grid-cols-1 gap-3\", className)}>\n {toolbar && !empty ? (\n <ViewToolbar\n view={view}\n onViewChange={setView}\n ctx={ctx}\n tasks={all}\n count={shown.tasks.length}\n extra={\n <>\n {toolbarExtra}\n {editable ? (\n <Button variant=\"primary\" size=\"sm\" icon=\"plus\" onClick={() => startAdding(adding?.group)}>\n <span className=\"sr-only sm:not-sr-only\">{addLabel}</span>\n </Button>\n ) : null}\n </>\n }\n />\n ) : null}\n {readError ? <Notice tone=\"danger\">{readError}</Notice> : null}\n {error ? (\n <Notice tone=\"danger\" onDismiss={clearError}>\n {error}\n </Notice>\n ) : null}\n\n {loading ? (\n <div className=\"grid gap-2 rounded-lg border border-border bg-card p-4\" aria-busy=\"true\" aria-label=\"Loading tasks\">\n <Skeleton className=\"w-32\" />\n {[0, 1, 2, 3, 4].map((i) => (\n <div key={i} className=\"flex items-center gap-3 py-1.5\">\n <Skeleton shape=\"circle\" className=\"size-4\" />\n <Skeleton className={[\"w-2/3\", \"w-1/2\", \"w-3/5\", \"w-2/5\", \"w-1/2\"][i]} />\n </div>\n ))}\n </div>\n ) : empty ? (\n <div className=\"rounded-lg bg-card\">\n <Empty\n icon=\"circle-check\"\n title={emptyTitle}\n body={emptyBody}\n action={\n editable ? (\n <div className=\"flex flex-wrap items-center justify-center gap-2\">\n <Button variant=\"primary\" icon=\"plus\" onClick={() => startAdding()}>\n {addLabel}\n </Button>\n {(starters ?? []).map((s) => (\n <Button\n key={s.label}\n variant=\"secondary\"\n icon={s.icon ?? \"sparkles\"}\n onClick={() => {\n setStarted(true);\n let prev = null;\n const made = s.tasks.map((t) => (prev = draftTask(t.title, ctx, t, { after: prev })));\n commit({ type: \"create\", tasks: made }, { label: `“${s.label}”` });\n say(`Added ${made.length} tasks from ${s.label}`);\n }}\n >\n {s.label}\n </Button>\n ))}\n </div>\n ) : null\n }\n />\n </div>\n ) : noMatches ? (\n <div className=\"rounded-lg bg-card\">\n {allDone ? (\n <Empty\n icon=\"circle-check\"\n title=\"All done\"\n body=\"Every task here is finished. Nice work.\"\n action={\n <Button variant=\"secondary\" onClick={clearView}>\n Show done tasks\n </Button>\n }\n />\n ) : (\n <Empty\n icon=\"search\"\n title=\"No tasks match\"\n body={view.search ? `Nothing has “${view.search}” with these filters.` : \"Nothing fits these filters.\"}\n action={\n <Button variant=\"secondary\" onClick={clearView}>\n Clear search and filters\n </Button>\n }\n />\n )}\n </div>\n ) : (\n <TaskDragProvider\n disabled={!editable}\n onDragOver={({ over }) => setOverGroup(over?.data?.group ?? null)}\n onDragEnd={onDragEnd}\n onDragCancel={() => setOverGroup(null)}\n place={(o) => (o.data.task ? `${name(o.data.task)} in ${groups.find((g) => g.key === o.data.group)?.label ?? \"the list\"}` : o.data.label)}\n overlay={(a) => (\n <div className={cx(LIFTED, \"flex w-[min(28rem,85vw)] items-center gap-2 px-3 py-2 text-sm\")}>\n <StatusPill status={a.data.task?.status} ctx={ctx} hideLabel />\n <span className=\"min-w-0 flex-1 truncate font-medium\">{a.data.task?.title}</span>\n <AssigneeStack ids={a.data.task?.assignees} ctx={ctx} max={2} />\n </div>\n )}\n >\n <div ref={regionRef} role=\"region\" aria-label={label} className=\"rounded-lg border border-border bg-card text-card-foreground\">\n {groups.map((g, gi) => (\n <Group\n key={g.key}\n group={g}\n first={gi === 0}\n single={!by}\n by={by}\n ctx={ctx}\n uid={uid}\n folded={folded.has(g.key)}\n onFold={() => setFolded((s) => (s.has(g.key) ? new Set([...s].filter((x) => x !== g.key)) : new Set([...s, g.key])))}\n highlight={overGroup === g.key}\n canAdd={editable && groupPatch(g.key) !== null}\n addLabel={addLabel}\n adding={adding}\n setAdding={setAdding}\n onCreate={create}\n startAdding={startAdding}\n >\n <SortableGroup ids={g.roots.map((t) => sid(g.key, t.id))}>\n {g.blocks.map(({ root, rest }) => (\n <SortableRow\n key={root.task.id}\n row={root}\n group={g.key}\n draggable={editable}\n rowProps={rowProps(root)}\n handlers={rowHandlers(root)}\n regionRef={regionRef}\n >\n {rest.length || adding?.parentId === root.task.id ? (\n <ul>\n {rest.map((r) => (\n <Row key={r.task.id} row={r} {...rowProps(r)} handlers={rowHandlers(r)} regionRef={regionRef}>\n {adding?.parentId === r.task.id ? <SubtaskAdd parent={r} onCreate={create} onClose={() => setAdding(null)} group={g.key} /> : null}\n </Row>\n ))}\n {adding?.parentId === root.task.id ? <SubtaskAdd parent={root} onCreate={create} onClose={() => setAdding(null)} group={g.key} /> : null}\n </ul>\n ) : null}\n </SortableRow>\n ))}\n </SortableGroup>\n </Group>\n ))}\n </div>\n </TaskDragProvider>\n )}\n\n {selected.size > 0 ? (\n <BulkBar\n count={selected.size}\n ctx={ctx}\n editable={editable}\n onClear={() => setSelected(new Set())}\n onSelectAll={() => setSelected(new Set(order))}\n allSelected={selected.size > 0 && selected.size === order.length}\n apply={(patch, words) => {\n const ids = [...selected];\n commit({ type: \"update\", ids, patch }, { label: `${ids.length} tasks` }).then((r) => {\n if (r.ok !== false) say(words);\n });\n }}\n onDelete={() => remove([...selected])}\n />\n ) : null}\n\n <p aria-live=\"polite\" className=\"sr-only\">\n {announce}\n </p>\n </div>\n );\n\n function rowProps(r) {\n return {\n ctx,\n show,\n editable,\n counts,\n parentTitle: r.depth === 0 && r.task.parentId ? titleOf.get(r.task.parentId) : null,\n selectMode,\n selected: selected.has(r.task.id),\n editing: editing === r.task.id,\n };\n }\n\n function rowHandlers(r) {\n const t = r.task;\n return {\n onToggleDone: () => {\n const change = toggleDoneChange(t, ctx);\n commit(change, { label: name(t) }).then((res) => {\n if (res.ok !== false) say(isDone(t, ctx) ? `${name(t)} reopened` : `${name(t)} done`);\n });\n },\n onRename: (title) => {\n setEditing(null);\n const next = title.trim();\n if (next && next !== t.title) update(t, { title: next }, `Renamed to “${next}”`);\n },\n onEdit: () => setEditing(t.id),\n onCancelEdit: () => setEditing(null),\n onPatch: (patch) => update(t, patch),\n onOpen: onOpen ? () => onOpen(t) : null,\n onSelect: (range) => toggleSelect(t.id, { range }),\n onFold: r.hasChildren\n ? () => setCollapsed((s) => (s.has(t.id) ? new Set([...s].filter((x) => x !== t.id)) : new Set([...s, t.id])))\n : null,\n menu: [\n onOpen && { label: \"Open\", icon: \"maximize\", onSelect: () => onOpen(t) },\n editable && { label: \"Rename\", icon: \"pencil\", onSelect: () => setEditing(t.id) },\n editable && r.depth < 2 && { label: \"Add subtask\", icon: \"plus\", onSelect: () => startAdding(groupOfRow(t.id), t.id) },\n { label: selected.has(t.id) ? \"Deselect\" : \"Select\", icon: \"check\", onSelect: () => toggleSelect(t.id) },\n editable && {\n label: \"Status\",\n icon: \"circle-dashed\",\n items: ctx.statuses.filter((s) => s.id !== t.status).map((s) => ({ label: s.label, onSelect: () => update(t, { status: s.id }, `${name(t)} is ${s.label}`) })),\n },\n editable && t.parentId && { label: \"Make it a task\", icon: \"arrow-left\", onSelect: () => update(t, { parentId: null }, `${name(t)} is its own task`) },\n editable && {\n label: \"Duplicate\",\n icon: \"copy\",\n onSelect: () => {\n const { id, ...rest } = t;\n commit({ type: \"create\", task: draftTask(`${t.title} (copy)`, ctx, { ...rest, title: `${t.title} (copy)` }, { after: t }) }, { label: name(t) });\n },\n },\n editable && { type: \"separator\" },\n editable && { label: \"Delete\", icon: \"trash\", tone: \"danger\", onSelect: () => remove([t.id]) },\n ],\n };\n }\n\n function groupOfRow(id) {\n return groups.find((g) => g.rows.some((r) => r.task.id === id))?.key;\n }\n}\n\n// ---- a group ------------------------------------------------------------\n\nfunction Group({ group, first, single, by, ctx, uid, folded, onFold, highlight, canAdd, addLabel, adding, setAdding, onCreate, startAdding, children }) {\n const drop = useDropTarget(`group::${group.key}`, { group: group.key, label: group.label });\n const headingId = `${uid}-g-${group.key}`;\n const est = sumOf(group.tasks);\n const addingHere = adding && adding.group === group.key;\n return (\n <section\n ref={drop.setNodeRef}\n aria-labelledby={single ? undefined : headingId}\n aria-label={single ? \"All tasks\" : undefined}\n className={cx(\"transition-colors duration-150\", !first && \"border-t border-border\", (highlight || drop.isOver) && \"bg-accent/40\")}\n >\n {!single ? (\n <header className={cx(\"group/head sticky top-0 z-10 flex min-h-11 items-center gap-1 bg-card pe-2 ps-1\", first && \"rounded-t-lg\")}>\n {/* raw: the whole heading folds the group; a kit Button would\n centre it and weigh it as an action. */}\n <button\n type=\"button\"\n aria-expanded={!folded}\n onClick={onFold}\n className=\"flex min-h-11 min-w-0 flex-1 items-center gap-2 rounded-md px-2 text-left outline-none hover:bg-accent/60 focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <Icon name=\"chevron-right\" size={14} className={cx(\"shrink-0 text-muted-foreground transition-transform duration-150 motion-reduce:transition-none\", !folded && \"rotate-90\")} />\n <span id={headingId} className=\"min-w-0\">\n <GroupLabel group={group} by={by} ctx={ctx} />\n </span>\n <span data-nums className=\"text-xs text-muted-foreground\">\n {group.tasks.length}\n </span>\n </button>\n {est > 0 ? (\n <span data-nums className=\"hidden text-xs text-muted-foreground sm:inline\" title=\"Estimated hours\">\n {Math.round(est * 10) / 10}h\n </span>\n ) : null}\n {canAdd ? (\n <IconButton\n icon=\"plus\"\n size=\"sm\"\n label={`${addLabel} to ${group.label}`}\n className=\"opacity-100 sm:opacity-0 sm:transition-opacity sm:duration-150 sm:group-hover/head:opacity-100 sm:focus-visible:opacity-100\"\n onClick={() => startAdding(group.key)}\n />\n ) : null}\n </header>\n ) : null}\n {!folded ? (\n <>\n <ul className=\"grid grid-cols-1\">\n {children}\n {group.tasks.length === 0 && !addingHere ? (\n <li className=\"px-4 py-2 text-xs text-muted-foreground sm:ps-12\">{canAdd ? \"Nothing here — drop a task or add one.\" : \"Nothing here.\"}</li>\n ) : null}\n </ul>\n {canAdd ? <QuickAdd group={group} adding={adding} setAdding={setAdding} onCreate={onCreate} addLabel={addLabel} uid={uid} /> : null}\n </>\n ) : null}\n </section>\n );\n}\n\nfunction QuickAdd({ group, adding, setAdding, onCreate, addLabel, uid }) {\n const open = adding && adding.group === group.key && !adding.parentId;\n const [draft, setDraft] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const inputId = `${uid}-add-${group.key}`;\n if (!open) {\n return (\n <div className=\"px-2 pb-1.5 sm:ps-9\">\n <Button variant=\"quiet\" size=\"sm\" icon=\"plus\" align=\"start\" onClick={() => setAdding({ group: group.key })}>\n {addLabel}\n </Button>\n </div>\n );\n }\n async function submit() {\n const title = draft.trim();\n if (!title || busy) return;\n setBusy(true);\n const r = await onCreate(title, group.key);\n setBusy(false);\n if (r?.ok !== false) setDraft(\"\");\n }\n return (\n <div className=\"flex items-center gap-2 px-3 pb-2 sm:ps-12\">\n <label htmlFor={inputId} className=\"sr-only\">\n New task in {group.label}\n </label>\n <Input\n id={inputId}\n autoFocus\n value={draft}\n disabled={busy}\n placeholder=\"Task name, then Enter\"\n onChange={(e) => setDraft(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && !e.nativeEvent.isComposing) {\n e.preventDefault();\n submit();\n } else if (e.key === \"Escape\") {\n setDraft(\"\");\n setAdding(null);\n }\n }}\n onBlur={() => {\n if (!draft.trim()) setAdding(null);\n }}\n />\n <Button size=\"sm\" variant=\"secondary\" disabled={!draft.trim() || busy} loading={busy} onMouseDown={(e) => e.preventDefault()} onClick={submit}>\n Add\n </Button>\n </div>\n );\n}\n\n// ---- a row --------------------------------------------------------------\n\nfunction SortableRow({ row, group, draggable, rowProps, handlers, regionRef, children }) {\n const s = useSortableTask(sid(group, row.task.id), { task: row.task, group, label: row.task.title }, { disabled: !draggable || rowProps.editing });\n return (\n <Row\n row={row}\n {...rowProps}\n handlers={handlers}\n regionRef={regionRef}\n nodeRef={s.setNodeRef}\n style={s.style}\n dragging={s.isDragging}\n grip={draggable ? <DragHandle handle={s.handle} handleRef={s.setHandleRef} label={row.task.title || \"task\"} /> : null}\n >\n {children}\n </Row>\n );\n}\n\nfunction Row({ row, ctx, show, editable, counts, parentTitle, selectMode, selected, editing, handlers, regionRef, nodeRef, style, dragging, grip, children }) {\n const t = row.task;\n const done = isDone(t, ctx);\n const sub = counts.get(t.id);\n const titleRef = useRef(null);\n const selId = useId();\n const [draft, setDraft] = useState(t.title);\n // Enter, Escape and blur each end an edit; only the first one counts.\n const settled = useRef(false);\n useEffect(() => {\n if (editing) {\n setDraft(t.title);\n settled.current = false;\n }\n }, [editing, t.title]);\n const finish = (save) => {\n if (settled.current) return;\n settled.current = true;\n if (save) handlers.onRename(draft);\n else handlers.onCancelEdit();\n requestAnimationFrame(() => titleRef.current?.focus());\n };\n\n // ↑ ↓ walk the titles; x selects; Enter (a click) renames.\n function onTitleKey(e) {\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n const all = [...(regionRef.current?.querySelectorAll(\"[data-task-title]\") ?? [])];\n const i = all.indexOf(e.currentTarget);\n const next = all[i + (e.key === \"ArrowDown\" ? 1 : -1)];\n if (next) {\n e.preventDefault();\n next.focus();\n }\n } else if (e.key === \"x\" && !e.metaKey && !e.ctrlKey) {\n e.preventDefault();\n handlers.onSelect(e.shiftKey);\n }\n }\n\n const indent = row.depth * 24;\n return (\n <li\n ref={nodeRef}\n style={style}\n className={cx(\n \"group/row relative border-t border-border/60 bg-card [ul.grid>&:first-child]:border-t-0\",\n dragging && \"z-10 opacity-40\",\n selected && \"bg-primary/5\",\n )}\n >\n <div className=\"flex min-h-11 items-center gap-0.5 pe-1 ps-1 sm:pe-2\">\n <span className=\"flex w-6 shrink-0 justify-center\">{grip}</span>\n {selectMode ? (\n <Checkbox\n id={selId}\n aria-label={`Select ${t.title || \"task\"}`}\n checked={selected}\n onChange={(e) => handlers.onSelect(e.nativeEvent?.shiftKey)}\n className=\"w-6 shrink-0 justify-center\"\n />\n ) : null}\n <span style={{ width: indent }} className=\"shrink-0\" aria-hidden=\"true\" />\n {row.hasChildren ? (\n <IconButton\n icon=\"chevron-right\"\n size=\"sm\"\n label={row.collapsed ? `Show ${row.childCount} subtasks` : `Hide subtasks`}\n aria-expanded={!row.collapsed}\n onClick={handlers.onFold}\n className={cx(\"-mx-1.5 [&>svg]:transition-transform [&>svg]:duration-150 motion-reduce:[&>svg]:transition-none\", !row.collapsed && \"[&>svg]:rotate-90\")}\n />\n ) : row.depth > 0 ? (\n <span className=\"w-3 shrink-0\" aria-hidden=\"true\" />\n ) : null}\n <CompleteToggle done={done} onToggle={editable ? handlers.onToggleDone : undefined} label={t.title || \"task\"} size={row.depth > 0 ? \"sm\" : \"md\"} />\n <div className=\"flex min-w-0 flex-1 flex-wrap items-center gap-x-3 gap-y-0.5 py-1 sm:flex-nowrap sm:py-0\">\n <div className=\"flex min-w-0 basis-full items-center gap-2 sm:flex-1 sm:basis-auto\">\n {editing ? (\n <Input\n aria-label=\"Task name\"\n autoFocus\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n onFocus={(e) => e.target.select()}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && !e.nativeEvent.isComposing) {\n e.preventDefault();\n finish(true);\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n finish(false);\n }\n }}\n onBlur={() => {\n if (settled.current) return;\n settled.current = true;\n handlers.onRename(draft);\n }}\n className=\"flex-1\"\n />\n ) : (\n // raw: the title is text that becomes a field on click — a\n // kit Button would centre and weigh it as an action.\n <button\n ref={titleRef}\n type=\"button\"\n data-task-title\n onClick={(e) => {\n if (e.shiftKey || e.metaKey || e.ctrlKey) handlers.onSelect(e.shiftKey);\n else if (editable) handlers.onEdit();\n else handlers.onOpen?.();\n }}\n onKeyDown={onTitleKey}\n title={editable ? \"Click to rename\" : undefined}\n className={cx(\n \"min-w-0 truncate rounded-sm text-left text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n editable && \"cursor-text\",\n done ? \"text-muted-foreground line-through decoration-muted-foreground/60\" : \"text-foreground\",\n row.depth === 0 && \"font-medium\",\n )}\n >\n {t.title || <span className=\"text-muted-foreground\">Untitled</span>}\n </button>\n )}\n {!editing && sub ? <SubtaskCount done={sub.done} total={sub.total} className=\"shrink-0\" /> : null}\n {!editing && parentTitle && row.depth === 0 ? (\n <span className=\"hidden min-w-0 shrink truncate text-xs text-muted-foreground sm:inline\" title={`Subtask of ${parentTitle}`}>\n in {parentTitle}\n </span>\n ) : null}\n {!editing && handlers.onOpen ? (\n <IconButton\n icon=\"maximize\"\n size=\"sm\"\n label={`Open ${t.title || \"task\"}`}\n onClick={handlers.onOpen}\n className=\"-my-1 ms-auto opacity-0 transition-opacity duration-150 group-hover/row:opacity-100 focus-visible:opacity-100 [@media(hover:none)]:hidden\"\n />\n ) : null}\n </div>\n {show.has(\"tags\") ? <TagList tags={t.tags} className=\"shrink-0\" /> : null}\n {show.has(\"project\") && t.project ? (\n <span className=\"max-w-32 shrink-0 truncate text-xs text-muted-foreground\">{ctx.projectById.get(String(t.project))?.name ?? t.project}</span>\n ) : null}\n {show.has(\"estimate\") && t.estimate ? (\n <span data-nums className=\"shrink-0 text-xs text-muted-foreground\">\n {t.estimate}h\n </span>\n ) : null}\n {show.has(\"start\") && t.start ? (\n <span data-nums className=\"shrink-0 text-xs text-muted-foreground\">\n from {formatDay(t.start, ctx.today)}\n </span>\n ) : null}\n {show.has(\"progress\") && typeof t.progress === \"number\" ? (\n <span className=\"w-16 shrink-0\">\n <ProgressLine value={t.progress} />\n </span>\n ) : null}\n {show.has(\"assignees\") ? (\n <AssigneeStack ids={t.assignees} ctx={ctx} onChange={editable ? (ids) => handlers.onPatch({ assignees: ids }) : undefined} className=\"shrink-0\" />\n ) : null}\n {show.has(\"due\") ? (\n <DueLabel value={t.due} done={done} ctx={ctx} onChange={editable ? (due) => handlers.onPatch({ due }) : undefined} className=\"shrink-0 sm:w-24 sm:justify-start\" />\n ) : null}\n {show.has(\"priority\") ? (\n <PriorityFlag value={t.priority} onChange={editable ? (priority) => handlers.onPatch({ priority }) : undefined} className=\"shrink-0\" />\n ) : null}\n {show.has(\"status\") ? <StatusPill status={t.status} ctx={ctx} onChange={editable ? (status) => handlers.onPatch({ status }) : undefined} className=\"shrink-0\" /> : null}\n </div>\n <Menu items={handlers.menu} label={`Actions for ${t.title || \"task\"}`} className=\"shrink-0 sm:opacity-0 sm:transition-opacity sm:duration-150 sm:group-hover/row:opacity-100 sm:focus-visible:opacity-100 sm:aria-expanded:opacity-100\" />\n </div>\n {children}\n </li>\n );\n}\n\n// ---- the bulk bar -------------------------------------------------------\n\nfunction BulkBar({ count, ctx, editable, onClear, onSelectAll, allSelected, apply, onDelete }) {\n return (\n <div role=\"toolbar\" aria-label={`${count} selected`} className=\"sticky bottom-3 z-30 mx-auto flex w-full max-w-2xl flex-wrap items-center gap-1 rounded-lg border border-border bg-popover p-1.5 text-popover-foreground shadow-lg\">\n <span data-nums className=\"px-2 text-sm font-medium\">\n {count} selected\n </span>\n {!allSelected ? (\n <Button variant=\"link\" size=\"sm\" tone=\"muted\" onClick={onSelectAll}>\n Select all\n </Button>\n ) : null}\n <span className=\"flex-1\" />\n {editable && count > 0 ? (\n <>\n <Menu\n label=\"Set status\"\n trigger={\n <span className=\"inline-flex items-center gap-1.5 text-sm\">\n <Icon name=\"circle-dashed\" size={16} />\n <span className=\"hidden sm:inline\">Status</span>\n </span>\n }\n items={ctx.statuses.map((s) => ({ label: s.label, onSelect: () => apply({ status: s.id }, `${count} tasks set to ${s.label}`) }))}\n />\n <Menu\n label=\"Assign\"\n trigger={\n <span className=\"inline-flex items-center gap-1.5 text-sm\">\n <Icon name=\"user\" size={16} />\n <span className=\"hidden sm:inline\">Assign</span>\n </span>\n }\n items={[\n ...ctx.people.map((p) => ({ label: p.name, onSelect: () => apply({ assignees: [p.id] }, `${count} tasks assigned to ${p.name}`) })),\n { type: \"separator\" },\n { label: \"Unassign\", onSelect: () => apply({ assignees: [] }, `${count} tasks unassigned`) },\n ]}\n />\n <DueLabel value={null} ctx={ctx} quiet={false} placeholder=\"Due\" onChange={(due) => apply({ due }, due ? `${count} tasks due ${formatDay(due, ctx.today)}` : `${count} tasks have no due date`)} className=\"px-2 text-sm\" />\n <Menu\n label=\"Set priority\"\n trigger={\n <span className=\"inline-flex items-center gap-1.5 text-sm\">\n <Icon name=\"flag\" size={16} />\n <span className=\"hidden sm:inline\">Priority</span>\n </span>\n }\n items={[\n ...[\"urgent\", \"high\", \"medium\", \"low\"].map((p) => ({ label: p[0].toUpperCase() + p.slice(1), onSelect: () => apply({ priority: p }, `${count} tasks set to ${p}`) })),\n { label: \"No priority\", onSelect: () => apply({ priority: null }, `${count} tasks have no priority`) },\n ]}\n />\n <IconButton icon=\"trash\" size=\"sm\" label={`Delete ${count} tasks`} onClick={onDelete} className=\"text-destructive\" />\n </>\n ) : null}\n <IconButton icon=\"x\" size=\"sm\" label=\"Clear selection\" onClick={onClear} />\n </div>\n );\n}\n\nfunction SubtaskAdd({ parent, group, onCreate, onClose }) {\n const [draft, setDraft] = useState(\"\");\n const [busy, setBusy] = useState(false);\n async function submit() {\n const title = draft.trim();\n if (!title || busy) return;\n setBusy(true);\n const r = await onCreate(title, group, parent.task.id);\n setBusy(false);\n if (r?.ok !== false) setDraft(\"\");\n }\n return (\n <li className=\"flex items-center gap-2 border-t border-border/60 py-1.5 pe-3\" style={{ paddingInlineStart: 64 + (parent.depth + 1) * 24 }}>\n <Input\n aria-label={`New subtask of ${parent.task.title || \"task\"}`}\n autoFocus\n value={draft}\n disabled={busy}\n placeholder=\"Subtask name, then Enter\"\n onChange={(e) => setDraft(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && !e.nativeEvent.isComposing) {\n e.preventDefault();\n submit();\n } else if (e.key === \"Escape\") onClose();\n }}\n onBlur={() => {\n if (!draft.trim()) onClose();\n }}\n />\n </li>\n );\n}\n"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"path": "registry/blocks/task-list/README.md",
|
|
27
|
+
"type": "registry:file",
|
|
28
|
+
"target": "@components/makersclaw/blocks/task-list/README.md",
|
|
29
|
+
"content": "# Task list\n\nThe List view of work. Tasks sit under headings that fold — by status,\nassignee, priority, project, due date or any field. Each row has a round\ncheck to complete it, a title you click to rename in place, its subtasks\nindented under it (with their own fold and a \"2/5\" count), and the fields\nyou choose — people, due date, priority, tags, project, estimate — as\npickers right in the row. Every group ends in \"Add task\": type, Enter, and\nthe next one is ready. Drag a row by its grip to reorder it, or into\nanother group to change its status, assignee or due date. Tick several\n(the row menu's \"Select\", or x on a focused title, shift for a range) and\na bar sets their status, assignee, due date or priority together, or\ndeletes them — with Undo in the toast. Search, filters (as chips), group,\nsort and fields live in the toolbar above.\n\nEvery change saves through one async `onChange(change)` and shows at\nonce; when the save fails the list puts it back and says why.\n\n```jsx\nimport { TaskList } from \"../components/blocks/task-list\"; // a Makersclaw app (\"./blocks/task-list\" from components/)\nimport { TaskList } from \"@/components/makersclaw/blocks/task-list\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component, and\nload the tasks in the server surface (README §4). It is built on\n`task-model` (added with it): the task shape, the change shapes and the\nhelpers are documented there.\n\n## The data\n\nTasks in task-model's shape — `{ id, title, status, assignees, due?,\npriority?, parentId?, tags, project?, estimate?, order?, … }` — with the\nstatuses, people and projects they refer to. A task with `parentId` is a\nsubtask, drawn under its parent when both are in the same group (else in\nits own group with \"in Parent\" beside it). `order` is the hand-made order\ndrag-to-reorder saves; tasks without one keep the order given.\n\n## Props\n\n- `tasks` — the tasks. Pass `useFixture(rows, SAMPLE).rows` and the\n examples change on screen and save nothing (they are not rows).\n- `statuses` — `[{ id, label, tone?, hue?, done? }]` in order; `done`\n marks what the check completes to. Default: To do, In progress, In\n review, Done.\n- `people` — `[{ id, name, src? }]`: who can be assigned.\n- `projects` — `[{ id, name, hue? }]`.\n- `fields` — custom fields (`{ key, label, kind, options? }`), grouped and\n filtered like the built-in ones.\n- `onChange(change)` — save a change; return `{ ok: true }` or\n `{ ok: false, error }` (a throw counts as failed). A change is\n task-model's `create` / `update` / `delete` / `batch`; in a Makersclaw\n app, loop over `changeWrites(change)` into insertRow / updateRow /\n deleteRow (task-model README, \"A change\"). Without `onChange` the list\n only shows: no checks, pickers, drag or quick-add.\n- `onOpen(task)` — an \"Open\" button on each row (on hover; in the row's\n menu on a phone) and in the menu: open your `Sheet` with the task.\n Clicking a title renames it; with no `onChange`, it opens.\n- `view` · `defaultView` · `onViewChange(view)` — what the list shows:\n `{ search, filters, groupBy, sort, fields, hideDone }` (task-model's\n `DEFAULT_VIEW`: grouped by status, showing assignees, due and priority).\n Pass `defaultView` to start somewhere else; pass `view` + `onViewChange`\n to keep it yourself (saved views, the URL).\n- `today` — `\"YYYY-MM-DD\"`: what \"Today\" and \"Overdue\" are measured from.\n Default the browser's today; fix it in a demo.\n- `weekStartsOn` — 0 (Sunday) … 6; default Monday.\n- `toolbar` — default true; false hides search, filter, group, sort and\n fields (and the Add task button).\n- `toolbarExtra` — your controls at the toolbar's end, before \"Add task\".\n- `addLabel` — default `\"Add task\"`.\n- `starters` — templates the empty state offers:\n `[{ label: \"Product launch\", icon?: \"sparkles\", tasks: [{ title, priority?, … }] }]`.\n A press creates them all in one change.\n- `emptyTitle` · `emptyBody` — the empty state's words.\n- `loading` — skeleton rows in place of the list.\n- `error` — a danger Notice above the list (a failed read).\n- `label` — what the list is called to a screen reader, default `\"Tasks\"`.\n- `className` — layout around it (width, margin). Never colour.\n\n## What it does, precisely\n\n- Completing: the round check moves a task to the first `done` status,\n and back to the first open one.\n- Grouped by assignee (or tags), a task with two people is under both;\n dragging it from one to another swaps that one person.\n- Grouped by due date, \"Overdue\" takes no drops (the toast says to give a\n date instead); dropping on \"Tomorrow\" sets tomorrow, and a start date\n moves with it.\n- Reordering needs the manual sort (Sort: Manual). With another sort, a\n drag still moves a task to another group.\n- Deleting a task deletes its subtasks; Undo brings all of them back.\n- Keyboard: Tab reaches every control; on a title, ↑ ↓ move between\n tasks, Enter renames (Enter saves, Escape cancels), x selects (shift\n for a range). The grip picks a row up with Space; arrows move it; Space\n drops; Escape cancels.\n- On a phone the row's fields wrap under its title, the grip and menu\n stay visible, and the bulk bar sits at the bottom of the list.\n\n## Example\n\n```jsx\n// surfaces/tasks.jsx — server: load, hand over\nimport { loadRows } from \"../lib/data\";\nimport Tasks from \"../components/Tasks\";\n\nexport default async function TasksSurface() {\n const { rows, error } = await loadRows({ source: \"public.tasks\", orderBy: \"created_at asc\" });\n const people = await loadRows({ source: \"public.people\" });\n return <Tasks tasks={rows} people={people.rows} error={error} />;\n}\n```\n\nThe list orders each group by `order` itself (Sort: Manual), so the read\norder only breaks ties.\n\n```jsx\n// components/Tasks.jsx — client: the list, the task sheet\n\"use client\";\nimport { useState } from \"react\";\nimport { deleteRow, insertRow, updateRow } from \"../lib/actions\";\nimport { Sheet } from \"./ui\";\nimport { TaskList } from \"./blocks/task-list\";\nimport { changeWrites } from \"./blocks/task-model\";\n\nconst STATUSES = [\n { id: \"todo\", label: \"To do\", tone: \"neutral\" },\n { id: \"doing\", label: \"In progress\", tone: \"info\" },\n { id: \"done\", label: \"Done\", tone: \"success\", done: true },\n];\n\nconst COLUMNS = [\"id\", \"title\", \"status\", \"assignees\", \"due\", \"priority\", \"tags\", \"parentId\", \"order\"];\n\nasync function save(change) {\n for (const w of changeWrites(change, { columns: COLUMNS, keepNewIds: false })) {\n const r =\n w.op === \"insert\" ? await insertRow({ source: \"public.tasks\", values: w.values, detail: w.values.title })\n : w.op === \"update\" ? await updateRow({ source: \"public.tasks\", id: w.id, patch: w.patch })\n : await deleteRow({ source: \"public.tasks\", id: w.id });\n if (r?.ok === false) return r;\n }\n return { ok: true };\n}\n\nexport default function Tasks({ tasks, people, error }) {\n const [open, setOpen] = useState(null);\n return (\n <>\n <TaskList tasks={tasks} statuses={STATUSES} people={people} onChange={save} onOpen={setOpen} error={error}\n starters={[{ label: \"Weekly planning\", tasks: [{ title: \"Review last week\" }, { title: \"Pick three priorities\" }] }]} />\n <Sheet open={Boolean(open)} onClose={() => setOpen(null)} title={open?.title}>{/* the task's fields */}</Sheet>\n </>\n );\n}\n```\n\nGrouped by person, only open work, showing the project:\n\n```jsx\n<TaskList tasks={tasks} people={people} projects={projects} onChange={save}\n defaultView={{ groupBy: \"assignees\", hideDone: true, fields: [\"due\", \"priority\", \"project\"] }} />\n```\n\n## What it will not do\n\n- Edit a task's description, comments or custom fields in the row: open\n it (`onOpen`) in a Sheet.\n- Drag a subtask out of its parent: use its menu (\"Make it a task\"), or\n \"Add subtask\" on the new parent.\n- Nest deeper than three levels from the menu (the data may be deeper;\n it is drawn).\n- Virtualise thousands of rows: filter, or page them in the surface.\n- Fetch: tasks come in as props; the page re-reads after a save.\n\nA list it cannot be is your own component in `components/`, built from\ntask-model's parts, toolbar and drag primitives — not an edit to this\nblock. Its files are fixed: `npm run check` refuses a change, and\n`makersclaw add task-list` restores them.\n"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"categories": [
|
|
33
|
+
"tasks",
|
|
34
|
+
"task list",
|
|
35
|
+
"to-do",
|
|
36
|
+
"todo",
|
|
37
|
+
"list view",
|
|
38
|
+
"checklist",
|
|
39
|
+
"clickup",
|
|
40
|
+
"linear",
|
|
41
|
+
"asana",
|
|
42
|
+
"things",
|
|
43
|
+
"subtasks",
|
|
44
|
+
"group",
|
|
45
|
+
"fold",
|
|
46
|
+
"inline edit",
|
|
47
|
+
"quick add",
|
|
48
|
+
"drag",
|
|
49
|
+
"reorder",
|
|
50
|
+
"bulk",
|
|
51
|
+
"select",
|
|
52
|
+
"undo",
|
|
53
|
+
"project management",
|
|
54
|
+
"sprint"
|
|
55
|
+
],
|
|
56
|
+
"docs": "Installed to components/makersclaw/blocks/task-list. Import: import { TaskList } from \"@/components/makersclaw/blocks/task-list\". Props and an example: components/makersclaw/blocks/task-list/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
57
|
+
"meta": {
|
|
58
|
+
"makersclaw": {
|
|
59
|
+
"version": "0.3.0",
|
|
60
|
+
"useWhen": "A screen that lists work to get done and moves it along: a team's tasks, a launch checklist, a sprint, a personal to-do list, onboarding steps, content to publish. Not for records that are not work (data-table), for stage columns side by side (board), or for dates on a grid (calendar)."
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|