makersclaw 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/cli.mjs +19 -6
- package/src/install.mjs +18 -0
- package/src/registry.mjs +38 -4
- package/src/shadcn.mjs +14 -1
- 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,923 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { Button, Empty, Icon, Input, Notice, Skeleton, Toggle, ToggleGroup, cx } from "../../ui";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_VIEW,
|
|
7
|
+
NONE,
|
|
8
|
+
TaskDragProvider,
|
|
9
|
+
ViewToolbar,
|
|
10
|
+
addDays,
|
|
11
|
+
applyView,
|
|
12
|
+
createTaskContext,
|
|
13
|
+
criticalPath,
|
|
14
|
+
dateConflicts,
|
|
15
|
+
deleteChange,
|
|
16
|
+
dependencyEdges,
|
|
17
|
+
descendantIds,
|
|
18
|
+
diffDays,
|
|
19
|
+
draftTask,
|
|
20
|
+
flattenTree,
|
|
21
|
+
formatDate,
|
|
22
|
+
isDone,
|
|
23
|
+
movePatch,
|
|
24
|
+
progressOf,
|
|
25
|
+
spanOf,
|
|
26
|
+
spanPatch,
|
|
27
|
+
toggleDoneChange,
|
|
28
|
+
useTaskChanges,
|
|
29
|
+
wouldCycle,
|
|
30
|
+
} from "../task-model";
|
|
31
|
+
import { ADD_ROW, Bar, GroupRow, ListRow, QuickAdd, ROW, TINTS, TimelineRow, Unscheduled, spanWords } from "./rows";
|
|
32
|
+
import {
|
|
33
|
+
ZOOMS,
|
|
34
|
+
afterBlockersPatch,
|
|
35
|
+
arrowPath,
|
|
36
|
+
ganttRange,
|
|
37
|
+
headerTiers,
|
|
38
|
+
isMilestone,
|
|
39
|
+
lengthOf,
|
|
40
|
+
planMove,
|
|
41
|
+
summarySpan,
|
|
42
|
+
weekendBands,
|
|
43
|
+
xOf,
|
|
44
|
+
zoomOf,
|
|
45
|
+
} from "./schedule";
|
|
46
|
+
|
|
47
|
+
// Gantt — tasks on a timeline, like Linear's timeline or TeamGantt: the
|
|
48
|
+
// task list on the left (groups that fold, subtasks under their parent),
|
|
49
|
+
// the bars on the right under a two-tier date header. Drag a bar to move
|
|
50
|
+
// it, an edge to change its start or end, the dot past its end onto
|
|
51
|
+
// another bar to make that one wait on it; every task that waits on a
|
|
52
|
+
// moved one slides along while you drag, and the finish date at the top
|
|
53
|
+
// says what the move cost. Milestones are diamonds, parents are summary
|
|
54
|
+
// brackets, dependencies are arrows (red when the dates disagree), and the
|
|
55
|
+
// critical path lights up on demand. Zoom day · week · month · quarter;
|
|
56
|
+
// a today line; the toolbar's search, filters, group and sort.
|
|
57
|
+
//
|
|
58
|
+
// Everything saves through one async `onChange(change)` answering { ok }
|
|
59
|
+
// (task-model's change shapes); the timeline shows each change at once
|
|
60
|
+
// and takes it back, with a notice, when the save fails.
|
|
61
|
+
//
|
|
62
|
+
// README.md beside this file lists every prop.
|
|
63
|
+
|
|
64
|
+
const HEADER = 52;
|
|
65
|
+
const ZOOM_ITEMS = Object.values(ZOOMS).map((z) => ({ value: z.key, label: z.label }));
|
|
66
|
+
const GROUP_FIELDS = ["project", "status", "assignees", "priority"];
|
|
67
|
+
|
|
68
|
+
function useControlled(value, fallback, onChange) {
|
|
69
|
+
const [own, setOwn] = useState(fallback);
|
|
70
|
+
const controlled = value !== undefined;
|
|
71
|
+
const set = useCallback(
|
|
72
|
+
(next) => {
|
|
73
|
+
if (!controlled) setOwn(next);
|
|
74
|
+
onChange?.(next);
|
|
75
|
+
},
|
|
76
|
+
[controlled, onChange],
|
|
77
|
+
);
|
|
78
|
+
return [controlled ? value : own, set];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function Gantt({
|
|
82
|
+
tasks,
|
|
83
|
+
statuses,
|
|
84
|
+
people,
|
|
85
|
+
projects,
|
|
86
|
+
fields,
|
|
87
|
+
today,
|
|
88
|
+
weekStartsOn,
|
|
89
|
+
weekend,
|
|
90
|
+
holidays,
|
|
91
|
+
onChange,
|
|
92
|
+
onOpen,
|
|
93
|
+
view: viewProp,
|
|
94
|
+
defaultView,
|
|
95
|
+
onViewChange,
|
|
96
|
+
zoom: zoomProp,
|
|
97
|
+
defaultZoom = "week",
|
|
98
|
+
onZoomChange,
|
|
99
|
+
colorBy = "project",
|
|
100
|
+
pushDependents = true,
|
|
101
|
+
defaultCritical = false,
|
|
102
|
+
newTaskLength = 3,
|
|
103
|
+
toolbar = true,
|
|
104
|
+
toolbarExtra,
|
|
105
|
+
addLabel = "Add task",
|
|
106
|
+
starters,
|
|
107
|
+
emptyTitle = "Nothing on the timeline yet",
|
|
108
|
+
emptyBody = "Add the first task and give it dates, then drag it into place. Link tasks to see what waits on what.",
|
|
109
|
+
height = 560,
|
|
110
|
+
loading = false,
|
|
111
|
+
error: readError,
|
|
112
|
+
label = "Timeline",
|
|
113
|
+
locale,
|
|
114
|
+
className,
|
|
115
|
+
}) {
|
|
116
|
+
const uid = useId();
|
|
117
|
+
const ctx = useMemo(
|
|
118
|
+
() => createTaskContext({ statuses, people, projects, fields, today, weekStartsOn, weekend, holidays }),
|
|
119
|
+
[statuses, people, projects, fields, today, weekStartsOn, weekend, holidays],
|
|
120
|
+
);
|
|
121
|
+
const editable = typeof onChange === "function";
|
|
122
|
+
const { tasks: all, commit, error, clearError } = useTaskChanges(tasks, onChange);
|
|
123
|
+
const [viewRaw, setViewRaw] = useControlled(viewProp, { ...DEFAULT_VIEW, groupBy: "project", sort: [{ key: "start", dir: "asc" }], ...defaultView }, onViewChange);
|
|
124
|
+
const view = useMemo(() => ({ ...DEFAULT_VIEW, ...viewRaw }), [viewRaw]);
|
|
125
|
+
const [zoom, setZoomRaw] = useControlled(zoomProp, ZOOMS[defaultZoom] ? defaultZoom : "week", onZoomChange);
|
|
126
|
+
const z = zoomOf(zoom);
|
|
127
|
+
const dw = z.dayWidth;
|
|
128
|
+
|
|
129
|
+
const [critical, setCritical] = useState(defaultCritical);
|
|
130
|
+
const [listOpen, setListOpen] = useState(null); // null: open from 640px, closed on a phone
|
|
131
|
+
const [narrow, setNarrow] = useState(false);
|
|
132
|
+
const [folded, setFolded] = useState(() => new Set());
|
|
133
|
+
const [collapsed, setCollapsed] = useState(() => new Set());
|
|
134
|
+
const [hot, setHot] = useState(null);
|
|
135
|
+
const [adding, setAdding] = useState(null);
|
|
136
|
+
const [drag, setDrag] = useState(null); // { id, kind, days, keys? }
|
|
137
|
+
const [linking, setLinking] = useState(false);
|
|
138
|
+
const [notice, setNotice] = useState(null);
|
|
139
|
+
const [announce, setAnnounce] = useState("");
|
|
140
|
+
const scrollRef = useRef(null);
|
|
141
|
+
const listRef = useRef(null);
|
|
142
|
+
const dragEndedAt = useRef(0);
|
|
143
|
+
const nudgeTimer = useRef(null);
|
|
144
|
+
const dragRef = useRef(null);
|
|
145
|
+
dragRef.current = drag;
|
|
146
|
+
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
const mq = window.matchMedia("(max-width: 639px)");
|
|
149
|
+
const on = () => setNarrow(mq.matches);
|
|
150
|
+
on();
|
|
151
|
+
mq.addEventListener("change", on);
|
|
152
|
+
return () => mq.removeEventListener("change", on);
|
|
153
|
+
}, []);
|
|
154
|
+
const showList = listOpen ?? !narrow;
|
|
155
|
+
|
|
156
|
+
const say = (text) => setAnnounce(text);
|
|
157
|
+
const name = (t) => `“${t?.title || "Untitled"}”`;
|
|
158
|
+
const byIdAll = useMemo(() => new Map(all.map((t) => [t.id, t])), [all]);
|
|
159
|
+
|
|
160
|
+
// ---- what a drag shows before it saves -------------------------------
|
|
161
|
+
const plan = useMemo(() => (drag ? planMove(all, drag.id, drag.kind, drag.days, { push: pushDependents }) : []), [all, drag, pushDependents]);
|
|
162
|
+
const display = useMemo(() => {
|
|
163
|
+
if (!plan.length) return all;
|
|
164
|
+
const patches = new Map(plan.map((p) => [p.id, p.patch]));
|
|
165
|
+
return all.map((t) => (patches.has(t.id) ? { ...t, ...patches.get(t.id) } : t));
|
|
166
|
+
}, [all, plan]);
|
|
167
|
+
const byId = useMemo(() => new Map(display.map((t) => [t.id, t])), [display]);
|
|
168
|
+
const moved = useMemo(() => new Set(plan.map((p) => p.id)), [plan]);
|
|
169
|
+
|
|
170
|
+
// ---- the view ----------------------------------------------------------
|
|
171
|
+
const shown = useMemo(() => applyView(all, view, ctx), [all, view, ctx]);
|
|
172
|
+
const by = view.groupBy && view.groupBy !== "none" ? view.groupBy : null;
|
|
173
|
+
const range = useMemo(() => ganttRange(all, zoom, ctx.today, { weekStartsOn: ctx.weekStartsOn }), [all, zoom, ctx]);
|
|
174
|
+
const from = range.from;
|
|
175
|
+
const width = range.days * dw;
|
|
176
|
+
const tiers = useMemo(() => (from ? headerTiers(from, range.to, zoom, { today: ctx.today, weekStartsOn: ctx.weekStartsOn, locale }) : { top: [], bottom: [] }), [from, range.to, zoom, ctx, locale]);
|
|
177
|
+
const bands = useMemo(() => (from ? weekendBands(from, range.to, zoom, ctx.weekend) : []), [from, range.to, zoom, ctx]);
|
|
178
|
+
|
|
179
|
+
// Rows in reading order, each with its height and top: the list pane and
|
|
180
|
+
// the timeline draw the same list, so they always line up.
|
|
181
|
+
const rows = useMemo(() => {
|
|
182
|
+
const out = [];
|
|
183
|
+
let y = 0;
|
|
184
|
+
const push = (r, h) => {
|
|
185
|
+
out.push({ ...r, y, height: h });
|
|
186
|
+
y += h;
|
|
187
|
+
};
|
|
188
|
+
const single = !by;
|
|
189
|
+
for (const g of shown.groups) {
|
|
190
|
+
const shut = folded.has(g.key);
|
|
191
|
+
if (!single) push({ kind: "group", key: `g:${g.key}`, group: g }, ROW);
|
|
192
|
+
if (!shut || single) {
|
|
193
|
+
for (const r of flattenTree(g.tasks, { collapsed })) {
|
|
194
|
+
const task = byId.get(r.task.id) ?? r.task;
|
|
195
|
+
const milestone = isMilestone(task);
|
|
196
|
+
const span = r.hasChildren ? summarySpan(task, display) : spanOf(task);
|
|
197
|
+
push(
|
|
198
|
+
{
|
|
199
|
+
kind: "task",
|
|
200
|
+
key: `${g.key}::${task.id}`,
|
|
201
|
+
group: g.key,
|
|
202
|
+
task,
|
|
203
|
+
depth: r.depth,
|
|
204
|
+
hasChildren: r.hasChildren,
|
|
205
|
+
collapsed: r.collapsed,
|
|
206
|
+
milestone,
|
|
207
|
+
span,
|
|
208
|
+
done: isDone(task, ctx),
|
|
209
|
+
progress: progressOf(task, display, ctx),
|
|
210
|
+
},
|
|
211
|
+
ROW,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (editable) push({ kind: "add", key: `add:${g.key}`, group: g }, ADD_ROW);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return { list: out, height: y };
|
|
218
|
+
}, [shown.groups, by, folded, collapsed, byId, display, ctx, editable]);
|
|
219
|
+
|
|
220
|
+
// Where each task's bar is (its first row, when it shows twice).
|
|
221
|
+
const boxes = useMemo(() => {
|
|
222
|
+
const m = new Map();
|
|
223
|
+
if (!from) return m;
|
|
224
|
+
for (const r of rows.list) {
|
|
225
|
+
if (r.kind !== "task" || !r.span || m.has(r.task.id)) continue;
|
|
226
|
+
const cy = r.y + ROW / 2;
|
|
227
|
+
if (r.milestone && !r.hasChildren) {
|
|
228
|
+
const cx = xOf(r.span.end, from, dw) + dw / 2;
|
|
229
|
+
m.set(r.task.id, { x0: cx - 8, x1: cx + 8, y: cy });
|
|
230
|
+
} else {
|
|
231
|
+
const x0 = xOf(r.span.start, from, dw);
|
|
232
|
+
m.set(r.task.id, { x0, x1: x0 + Math.max(lengthOf(r.span) * dw, 6), y: cy });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return m;
|
|
236
|
+
}, [rows, from, dw]);
|
|
237
|
+
|
|
238
|
+
const path = useMemo(() => (critical ? criticalPath(display) : new Set()), [critical, display]);
|
|
239
|
+
const conflicts = useMemo(() => new Set(dateConflicts(display).map((e) => `${e.from}>${e.to}`)), [display]);
|
|
240
|
+
const conflicted = useMemo(() => {
|
|
241
|
+
const m = new Map();
|
|
242
|
+
for (const k of conflicts) {
|
|
243
|
+
const [a, b] = k.split(">");
|
|
244
|
+
m.set(b, [...(m.get(b) ?? []), a]);
|
|
245
|
+
}
|
|
246
|
+
return m;
|
|
247
|
+
}, [conflicts]);
|
|
248
|
+
const edges = useMemo(() => dependencyEdges(display).filter((e) => boxes.has(e.from) && boxes.has(e.to)), [display, boxes]);
|
|
249
|
+
|
|
250
|
+
// The finish: the last day any shown task ends, and how a drag moves it.
|
|
251
|
+
const finish = useMemo(() => {
|
|
252
|
+
let base = null;
|
|
253
|
+
let now = null;
|
|
254
|
+
for (const t of shown.tasks) {
|
|
255
|
+
const a = spanOf(t);
|
|
256
|
+
const b = spanOf(byId.get(t.id));
|
|
257
|
+
if (a && (!base || a.end > base)) base = a.end;
|
|
258
|
+
if (b && (!now || b.end > now)) now = b.end;
|
|
259
|
+
}
|
|
260
|
+
return { day: now, delta: base && now ? diffDays(base, now) : 0 };
|
|
261
|
+
}, [shown.tasks, byId]);
|
|
262
|
+
const pathDays = useMemo(() => {
|
|
263
|
+
let lo = null;
|
|
264
|
+
let hi = null;
|
|
265
|
+
for (const id of path) {
|
|
266
|
+
const s = spanOf(byId.get(id));
|
|
267
|
+
if (!s) continue;
|
|
268
|
+
if (!lo || s.start < lo) lo = s.start;
|
|
269
|
+
if (!hi || s.end > hi) hi = s.end;
|
|
270
|
+
}
|
|
271
|
+
return lo ? diffDays(lo, hi) + 1 : 0;
|
|
272
|
+
}, [path, byId]);
|
|
273
|
+
|
|
274
|
+
// ---- scrolling ---------------------------------------------------------
|
|
275
|
+
const listWidth = () => (showList ? listRef.current?.offsetWidth ?? 0 : 0);
|
|
276
|
+
const scrollToDay = useCallback(
|
|
277
|
+
(day, { smooth = false, at = 1 / 3 } = {}) => {
|
|
278
|
+
const el = scrollRef.current;
|
|
279
|
+
if (!el || !from || !day) return;
|
|
280
|
+
const lw = showList ? listRef.current?.offsetWidth ?? 0 : 0;
|
|
281
|
+
const left = xOf(day, from, dw) - (el.clientWidth - lw) * at;
|
|
282
|
+
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
283
|
+
el.scrollTo({ left: Math.max(0, left), behavior: smooth && !reduced ? "smooth" : "auto" });
|
|
284
|
+
},
|
|
285
|
+
[from, dw, showList],
|
|
286
|
+
);
|
|
287
|
+
// Open on today, once there is a timeline.
|
|
288
|
+
const opened = useRef(false);
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
if (opened.current || !from) return;
|
|
291
|
+
opened.current = true;
|
|
292
|
+
scrollToDay(ctx.today);
|
|
293
|
+
}, [from, ctx.today, scrollToDay]);
|
|
294
|
+
// A new zoom keeps the day in the middle where it was.
|
|
295
|
+
const anchor = useRef(null);
|
|
296
|
+
function setZoom(next) {
|
|
297
|
+
const el = scrollRef.current;
|
|
298
|
+
if (el && from) {
|
|
299
|
+
const lw = listWidth();
|
|
300
|
+
const mid = el.scrollLeft + (el.clientWidth - lw) / 2;
|
|
301
|
+
anchor.current = addDays(from, Math.round(mid / dw));
|
|
302
|
+
}
|
|
303
|
+
setZoomRaw(next);
|
|
304
|
+
}
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
if (!anchor.current) return;
|
|
307
|
+
scrollToDay(anchor.current, { at: 1 / 2 });
|
|
308
|
+
anchor.current = null;
|
|
309
|
+
}, [zoom, scrollToDay]);
|
|
310
|
+
|
|
311
|
+
// ---- changes -----------------------------------------------------------
|
|
312
|
+
const commitPlan = useCallback(
|
|
313
|
+
(id, kind, days) => {
|
|
314
|
+
const steps = planMove(all, id, kind, days, { push: pushDependents });
|
|
315
|
+
if (!steps.length) return;
|
|
316
|
+
const task = byIdAll.get(id);
|
|
317
|
+
const family = new Set([id, ...descendantIds(all, id)]);
|
|
318
|
+
const pushed = steps.filter((s) => !family.has(s.id)).length;
|
|
319
|
+
const change = steps.length === 1 ? { type: "update", id: steps[0].id, patch: steps[0].patch } : { type: "batch", changes: steps.map((s) => ({ type: "update", id: s.id, patch: s.patch })) };
|
|
320
|
+
const after = spanOf({ ...task, ...steps.find((s) => s.id === id)?.patch }) ?? summarySpan({ ...task }, all);
|
|
321
|
+
commit(change, {
|
|
322
|
+
label: name(task),
|
|
323
|
+
undo: pushed ? `Moved ${name(task)} and ${pushed} task${pushed === 1 ? "" : "s"} waiting on it` : undefined,
|
|
324
|
+
}).then((r) => {
|
|
325
|
+
if (r?.ok !== false) say(`${name(task)} is now ${spanWords(after, locale)}${pushed ? `; ${pushed} waiting task${pushed === 1 ? "" : "s"} moved too` : ""}.`);
|
|
326
|
+
});
|
|
327
|
+
},
|
|
328
|
+
[all, byIdAll, commit, pushDependents, locale],
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const onPreview = useCallback((id, kind, days) => {
|
|
332
|
+
setDrag((d) => (d && d.id === id && d.kind === kind && d.days === days ? d : { id, kind, days }));
|
|
333
|
+
}, []);
|
|
334
|
+
|
|
335
|
+
// Keyboard: each arrow adds a day to a pending move; it saves when the
|
|
336
|
+
// keys rest (or on Enter), Escape drops it.
|
|
337
|
+
const onNudge = useCallback(
|
|
338
|
+
(id, kind, step, action) => {
|
|
339
|
+
const d = dragRef.current;
|
|
340
|
+
clearTimeout(nudgeTimer.current);
|
|
341
|
+
const flush = (x) => {
|
|
342
|
+
setDrag(null);
|
|
343
|
+
if (x?.days) commitPlan(x.id, x.kind, x.days);
|
|
344
|
+
};
|
|
345
|
+
if (action === "flush") return flush(d);
|
|
346
|
+
if (action === "cancel") {
|
|
347
|
+
setDrag(null);
|
|
348
|
+
say("Move cancelled.");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
let next;
|
|
352
|
+
if (d && d.keys && d.id === id && d.kind === kind) next = { ...d, days: d.days + step };
|
|
353
|
+
else {
|
|
354
|
+
if (d?.keys) flush(d);
|
|
355
|
+
next = { id, kind, days: step, keys: true };
|
|
356
|
+
}
|
|
357
|
+
setDrag(next);
|
|
358
|
+
nudgeTimer.current = setTimeout(() => flush(dragRef.current), 800);
|
|
359
|
+
},
|
|
360
|
+
[commitPlan],
|
|
361
|
+
);
|
|
362
|
+
useEffect(() => () => clearTimeout(nudgeTimer.current), []);
|
|
363
|
+
|
|
364
|
+
function onDragStart({ active }) {
|
|
365
|
+
if (active?.data?.kind === "link") setLinking(true);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function onDragEnd({ active, over, delta }) {
|
|
369
|
+
dragEndedAt.current = Date.now();
|
|
370
|
+
setLinking(false);
|
|
371
|
+
const a = active?.data ?? {};
|
|
372
|
+
if (a.kind === "link") {
|
|
373
|
+
setDrag(null);
|
|
374
|
+
const to = over?.data?.task;
|
|
375
|
+
if (to && to.id !== a.task.id) link(a.task.id, to.id);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const days = Math.round((delta?.x ?? 0) / dw);
|
|
379
|
+
setDrag(null);
|
|
380
|
+
if (a.task && days) commitPlan(a.task.id, a.kind, days);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function link(fromId, toId) {
|
|
384
|
+
const target = byIdAll.get(toId);
|
|
385
|
+
const source = byIdAll.get(fromId);
|
|
386
|
+
if (!target || !source) return;
|
|
387
|
+
if ((target.dependsOn ?? []).includes(fromId)) return;
|
|
388
|
+
if (wouldCycle(all, fromId, toId)) {
|
|
389
|
+
setNotice(`${name(source)} already waits on ${name(target)}, so ${name(target)} cannot wait on it.`);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
setNotice(null);
|
|
393
|
+
commit({ type: "update", id: toId, patch: { dependsOn: [...(target.dependsOn ?? []), fromId] } }, { label: name(target) }).then((r) => {
|
|
394
|
+
if (r?.ok !== false) say(`${name(target)} now waits on ${name(source)}.`);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function unlink(fromId, toId) {
|
|
399
|
+
const target = byIdAll.get(toId);
|
|
400
|
+
if (!target) return;
|
|
401
|
+
commit(
|
|
402
|
+
{ type: "update", id: toId, patch: { dependsOn: (target.dependsOn ?? []).filter((d) => d !== fromId) } },
|
|
403
|
+
{ label: name(target), undo: `${name(target)} no longer waits on ${name(byIdAll.get(fromId))}` },
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function groupPatch(key) {
|
|
408
|
+
if (!by) return {};
|
|
409
|
+
return movePatch({ assignees: [], tags: [] }, by, NONE, key, ctx);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function create(title, groupKey) {
|
|
413
|
+
const patch = groupPatch(groupKey);
|
|
414
|
+
if (patch === null) return { ok: false };
|
|
415
|
+
const start = ctx.today;
|
|
416
|
+
const task = draftTask(title, ctx, { ...patch, start, due: addDays(start, Math.max(1, newTaskLength) - 1) });
|
|
417
|
+
const r = await commit({ type: "create", task }, { label: name(task) });
|
|
418
|
+
if (r?.ok !== false) say(`Added ${name(task)}, starting today.`);
|
|
419
|
+
return r;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function schedule(task, dayIndex) {
|
|
423
|
+
const start = addDays(from, dayIndex);
|
|
424
|
+
const end = addDays(start, Math.max(1, isMilestone(task) ? 1 : newTaskLength) - 1);
|
|
425
|
+
commit({ type: "update", id: task.id, patch: spanPatch(task, start, end) }, { label: name(task) }).then((r) => {
|
|
426
|
+
if (r?.ok !== false) say(`${name(task)} is scheduled for ${spanWords({ start, end }, locale)}.`);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function remove(task) {
|
|
431
|
+
commit(deleteChange(all, [task.id]), { label: name(task), undo: `Deleted ${name(task)}` });
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function startFrom(s) {
|
|
435
|
+
let cursor = ctx.today;
|
|
436
|
+
let prev = null;
|
|
437
|
+
const made = [];
|
|
438
|
+
for (const item of s.tasks ?? []) {
|
|
439
|
+
const len = Math.max(1, item.milestone ? 1 : item.days ?? newTaskLength);
|
|
440
|
+
const start = item.parallel && prev ? prev.start : cursor;
|
|
441
|
+
const end = addDays(start, len - 1);
|
|
442
|
+
const t = draftTask(item.title, ctx, {
|
|
443
|
+
start: item.milestone ? undefined : start,
|
|
444
|
+
due: end,
|
|
445
|
+
...(item.milestone ? { milestone: true } : {}),
|
|
446
|
+
dependsOn: prev && !item.parallel ? [prev.id] : [],
|
|
447
|
+
});
|
|
448
|
+
made.push(t);
|
|
449
|
+
if (!item.parallel) cursor = addDays(end, 1);
|
|
450
|
+
prev = { id: t.id, start };
|
|
451
|
+
}
|
|
452
|
+
await commit({ type: "create", tasks: made }, { label: s.label });
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function clickGuard() {
|
|
456
|
+
return Date.now() - dragEndedAt.current < 250;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function menuFor(row) {
|
|
460
|
+
const t = row.task;
|
|
461
|
+
const items = [];
|
|
462
|
+
if (onOpen) items.push({ label: "Open", icon: "external", onSelect: () => onOpen(t) });
|
|
463
|
+
if (!editable) return items;
|
|
464
|
+
if (!row.span) items.push({ label: "Schedule from today", icon: "calendar", onSelect: () => schedule(t, diffDays(from, ctx.today)) });
|
|
465
|
+
const fix = conflicted.has(t.id) ? afterBlockersPatch(t, all) : null;
|
|
466
|
+
if (fix) items.push({ label: "Start after what it waits on", icon: "arrow-right", onSelect: () => commit({ type: "update", id: t.id, patch: fix }, { label: name(t) }) });
|
|
467
|
+
// "Waits on": what it waits on now, then the tasks nearest its start
|
|
468
|
+
// (a dozen; the dot on a bar links anything).
|
|
469
|
+
const mine = new Set(t.dependsOn ?? []);
|
|
470
|
+
const at = row.span?.start ?? ctx.today;
|
|
471
|
+
const near = (o) => Math.abs(diffDays(at, spanOf(o)?.start ?? at) ?? 0);
|
|
472
|
+
const others = shown.tasks
|
|
473
|
+
.filter((o) => o.id !== t.id && o.id !== t.parentId)
|
|
474
|
+
.sort((a, b) => (mine.has(b.id) ? 1 : 0) - (mine.has(a.id) ? 1 : 0) || near(a) - near(b))
|
|
475
|
+
.slice(0, Math.max(12, mine.size));
|
|
476
|
+
if (others.length) {
|
|
477
|
+
items.push({
|
|
478
|
+
label: "Waits on",
|
|
479
|
+
icon: "link",
|
|
480
|
+
items: others.map((o) => {
|
|
481
|
+
const on = (t.dependsOn ?? []).includes(o.id);
|
|
482
|
+
return {
|
|
483
|
+
type: "checkbox",
|
|
484
|
+
label: o.title || "Untitled",
|
|
485
|
+
checked: on,
|
|
486
|
+
disabled: !on && wouldCycle(all, o.id, t.id),
|
|
487
|
+
onCheckedChange: (next) => (next ? link(o.id, t.id) : unlink(o.id, t.id)),
|
|
488
|
+
};
|
|
489
|
+
}),
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
items.push({ label: row.done ? "Mark not done" : "Mark done", icon: "check", onSelect: () => commit(toggleDoneChange(t, ctx), { label: name(t) }) });
|
|
493
|
+
items.push({ type: "separator" });
|
|
494
|
+
items.push({ label: "Delete", icon: "trash", tone: "danger", onSelect: () => remove(t) });
|
|
495
|
+
return items;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function tintOf(task) {
|
|
499
|
+
if (colorBy === "status") {
|
|
500
|
+
const s = ctx.statusById.get(String(task.status));
|
|
501
|
+
return TINTS[s?.hue] ?? TINTS[s?.tone] ?? TINTS.neutral;
|
|
502
|
+
}
|
|
503
|
+
if (colorBy === "project") return TINTS[ctx.projectById.get(String(task.project))?.hue] ?? TINTS.primary;
|
|
504
|
+
return TINTS.primary;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const clearFilters = () => setViewRaw({ ...view, search: "", filters: {}, hideDone: false });
|
|
508
|
+
const firstGroup = shown.groups[0]?.key ?? "all";
|
|
509
|
+
|
|
510
|
+
// ---- render ------------------------------------------------------------
|
|
511
|
+
const addButton = editable ? (
|
|
512
|
+
<Button
|
|
513
|
+
variant="primary"
|
|
514
|
+
size="sm"
|
|
515
|
+
icon="plus"
|
|
516
|
+
onClick={() => {
|
|
517
|
+
setListOpen(true);
|
|
518
|
+
setFolded((s) => (s.has(firstGroup) ? new Set([...s].filter((k) => k !== firstGroup)) : s));
|
|
519
|
+
setAdding(firstGroup);
|
|
520
|
+
}}
|
|
521
|
+
>
|
|
522
|
+
<span className="sr-only sm:not-sr-only">{addLabel}</span>
|
|
523
|
+
</Button>
|
|
524
|
+
) : null;
|
|
525
|
+
|
|
526
|
+
const bar = (
|
|
527
|
+
<div className="flex flex-wrap items-center gap-2 border-b border-border px-2 py-2">
|
|
528
|
+
<Toggle
|
|
529
|
+
size="sm"
|
|
530
|
+
variant="quiet"
|
|
531
|
+
icon="panel-left"
|
|
532
|
+
label={showList ? "Hide the task list" : "Show the task list"}
|
|
533
|
+
pressed={showList}
|
|
534
|
+
onPressedChange={(on) => setListOpen(on)}
|
|
535
|
+
/>
|
|
536
|
+
<Button size="sm" variant="quiet" onClick={() => scrollToDay(ctx.today, { smooth: true })}>
|
|
537
|
+
Today
|
|
538
|
+
</Button>
|
|
539
|
+
<ToggleGroup aria-label="Zoom" size="sm" items={ZOOM_ITEMS} value={zoom} onChange={setZoom} className="order-last w-full sm:order-none sm:w-auto" />
|
|
540
|
+
<div className="ms-auto flex min-w-0 items-center gap-2">
|
|
541
|
+
{finish.day ? (
|
|
542
|
+
<p className="hidden items-center gap-1.5 text-sm text-muted-foreground md:flex">
|
|
543
|
+
Finishes <span className="font-medium text-foreground">{formatDate(finish.day, { weekday: "short", month: "short", day: "numeric" }, { locale })}</span>
|
|
544
|
+
{drag && finish.delta ? (
|
|
545
|
+
<span data-nums className={cx("rounded-full px-1.5 text-xs font-medium tabular-nums", finish.delta > 0 ? "bg-warning/15 text-warning" : "bg-success/15 text-success")}>
|
|
546
|
+
{finish.delta > 0 ? "+" : "−"}
|
|
547
|
+
{Math.abs(finish.delta)} {Math.abs(finish.delta) === 1 ? "day" : "days"}
|
|
548
|
+
</span>
|
|
549
|
+
) : null}
|
|
550
|
+
</p>
|
|
551
|
+
) : null}
|
|
552
|
+
<Toggle size="sm" variant="quiet" icon="zap" pressed={critical} onPressedChange={setCritical}>
|
|
553
|
+
<span className="sr-only sm:not-sr-only">Critical path</span>
|
|
554
|
+
</Toggle>
|
|
555
|
+
</div>
|
|
556
|
+
</div>
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
let content;
|
|
560
|
+
if (loading) {
|
|
561
|
+
content = (
|
|
562
|
+
<div aria-busy="true" className="grid gap-3 p-4" style={{ height: Math.min(height, 320) }}>
|
|
563
|
+
{[64, 48, 72, 40, 56].map((w, i) => (
|
|
564
|
+
<div key={i} className="flex items-center gap-4">
|
|
565
|
+
<Skeleton className="w-28 shrink-0 sm:w-56" />
|
|
566
|
+
<div className="min-w-0 flex-1" style={{ paddingInlineStart: `${i * 6}%`, paddingInlineEnd: `${100 - w - i * 6}%` }}>
|
|
567
|
+
<Skeleton shape="rect" className="h-6 w-full" />
|
|
568
|
+
</div>
|
|
569
|
+
</div>
|
|
570
|
+
))}
|
|
571
|
+
<span className="sr-only">Loading the timeline</span>
|
|
572
|
+
</div>
|
|
573
|
+
);
|
|
574
|
+
} else if (!all.length) {
|
|
575
|
+
content = (
|
|
576
|
+
<div className="p-4">
|
|
577
|
+
<Empty
|
|
578
|
+
icon="calendar"
|
|
579
|
+
title={emptyTitle}
|
|
580
|
+
body={emptyBody}
|
|
581
|
+
action={editable ? <FirstTask onCreate={(title) => create(title, firstGroup)} starters={starters} onStarter={startFrom} addLabel={addLabel} uid={uid} /> : null}
|
|
582
|
+
/>
|
|
583
|
+
</div>
|
|
584
|
+
);
|
|
585
|
+
} else if (!shown.tasks.length) {
|
|
586
|
+
content = (
|
|
587
|
+
<div className="p-4">
|
|
588
|
+
<Empty
|
|
589
|
+
icon="filter"
|
|
590
|
+
title="No tasks match"
|
|
591
|
+
body="Nothing on this timeline fits the search and filters."
|
|
592
|
+
action={
|
|
593
|
+
<Button size="sm" variant="secondary" onClick={clearFilters}>
|
|
594
|
+
Clear filters
|
|
595
|
+
</Button>
|
|
596
|
+
}
|
|
597
|
+
/>
|
|
598
|
+
</div>
|
|
599
|
+
);
|
|
600
|
+
} else {
|
|
601
|
+
const tx = from ? xOf(ctx.today, from, dw) : null;
|
|
602
|
+
const todayIn = tx !== null && tx >= 0 && tx < width;
|
|
603
|
+
content = (
|
|
604
|
+
<TaskDragProvider collision="pointer" onDragStart={onDragStart} onDragEnd={onDragEnd} onDragCancel={() => (setDrag(null), setLinking(false))} disabled={!editable}>
|
|
605
|
+
<div
|
|
606
|
+
ref={scrollRef}
|
|
607
|
+
className={cx("relative overflow-auto overscroll-x-contain", showList ? "[--gantt-list:11rem] sm:[--gantt-list:18rem]" : "[--gantt-list:0px]")}
|
|
608
|
+
style={{ maxHeight: `min(${height}px, 75vh)` }}
|
|
609
|
+
onPointerLeave={() => setHot(null)}
|
|
610
|
+
>
|
|
611
|
+
<div className="w-max min-w-full">
|
|
612
|
+
{/* The header: the list's heading, then months over days (or
|
|
613
|
+
years over months). Sticky to the top. */}
|
|
614
|
+
<div className="sticky top-0 z-40 flex border-b border-border bg-card" style={{ height: HEADER }}>
|
|
615
|
+
{showList ? (
|
|
616
|
+
<div className="sticky left-0 z-10 flex w-44 shrink-0 items-end border-e border-border bg-card px-3 pb-1.5 sm:w-72">
|
|
617
|
+
<span className="text-xs font-medium text-muted-foreground">
|
|
618
|
+
Task{" "}
|
|
619
|
+
<span data-nums className="tabular-nums">
|
|
620
|
+
· {shown.tasks.length}
|
|
621
|
+
</span>
|
|
622
|
+
</span>
|
|
623
|
+
</div>
|
|
624
|
+
) : null}
|
|
625
|
+
<div className="relative shrink-0" style={{ width }} aria-hidden="true">
|
|
626
|
+
{tiers.top.map((c) => (
|
|
627
|
+
<div key={c.key} className="absolute top-0 flex h-6 items-center border-s border-border px-2 first:border-s-0" style={{ left: c.x, width: c.w }}>
|
|
628
|
+
<span className="sticky left-[calc(var(--gantt-list)+8px)] truncate text-xs font-medium text-foreground">
|
|
629
|
+
{c.label}
|
|
630
|
+
</span>
|
|
631
|
+
</div>
|
|
632
|
+
))}
|
|
633
|
+
{tiers.bottom.map((c) => (
|
|
634
|
+
<div
|
|
635
|
+
key={c.key}
|
|
636
|
+
className={cx("absolute bottom-0 flex h-7 items-center justify-center border-s border-border/60 first:border-s-0", c.weekend ? "text-muted-foreground/60" : "text-muted-foreground")}
|
|
637
|
+
style={{ left: c.x, width: c.w }}
|
|
638
|
+
>
|
|
639
|
+
{c.today && z.unit === "day" ? (
|
|
640
|
+
<span data-nums className="rounded-full bg-primary px-1.5 text-2xs font-semibold tabular-nums text-primary-foreground">
|
|
641
|
+
{c.label}
|
|
642
|
+
</span>
|
|
643
|
+
) : c.w >= 22 ? (
|
|
644
|
+
<span data-nums className={cx("truncate text-2xs tabular-nums", c.today && "font-semibold text-foreground")}>
|
|
645
|
+
{c.label}
|
|
646
|
+
</span>
|
|
647
|
+
) : null}
|
|
648
|
+
</div>
|
|
649
|
+
))}
|
|
650
|
+
{todayIn && z.unit !== "day" ? <span className="absolute bottom-0 size-1.5 -translate-x-1/2 translate-y-1/2 rounded-full bg-primary" style={{ left: tx + dw / 2 }} /> : null}
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
653
|
+
|
|
654
|
+
<div className="relative flex">
|
|
655
|
+
{/* The list pane: sticky to the left while the timeline scrolls. */}
|
|
656
|
+
{showList ? (
|
|
657
|
+
<div ref={listRef} className="sticky left-0 z-30 w-44 shrink-0 border-e border-border bg-card sm:w-72">
|
|
658
|
+
{rows.list.map((r) =>
|
|
659
|
+
r.kind === "group" ? (
|
|
660
|
+
<GroupRow
|
|
661
|
+
key={r.key}
|
|
662
|
+
group={r.group}
|
|
663
|
+
by={by}
|
|
664
|
+
ctx={ctx}
|
|
665
|
+
count={r.group.tasks.length}
|
|
666
|
+
folded={folded.has(r.group.key)}
|
|
667
|
+
hot={hot === r.key}
|
|
668
|
+
onHot={() => setHot(r.key)}
|
|
669
|
+
onFold={() =>
|
|
670
|
+
setFolded((s) => {
|
|
671
|
+
const n = new Set(s);
|
|
672
|
+
if (n.has(r.group.key)) n.delete(r.group.key);
|
|
673
|
+
else n.add(r.group.key);
|
|
674
|
+
return n;
|
|
675
|
+
})
|
|
676
|
+
}
|
|
677
|
+
/>
|
|
678
|
+
) : r.kind === "task" ? (
|
|
679
|
+
<ListRow
|
|
680
|
+
key={r.key}
|
|
681
|
+
row={r}
|
|
682
|
+
ctx={ctx}
|
|
683
|
+
hot={hot === r.key}
|
|
684
|
+
onHot={() => setHot(r.key)}
|
|
685
|
+
onOpen={onOpen}
|
|
686
|
+
menu={menuFor(r)}
|
|
687
|
+
conflict={conflicted.has(r.task.id)}
|
|
688
|
+
blockers={(conflicted.get(r.task.id) ?? []).map((id) => name(byIdAll.get(id))).join(" and ")}
|
|
689
|
+
onFold={() =>
|
|
690
|
+
setCollapsed((s) => {
|
|
691
|
+
const n = new Set(s);
|
|
692
|
+
if (n.has(r.task.id)) n.delete(r.task.id);
|
|
693
|
+
else n.add(r.task.id);
|
|
694
|
+
return n;
|
|
695
|
+
})
|
|
696
|
+
}
|
|
697
|
+
/>
|
|
698
|
+
) : (
|
|
699
|
+
<QuickAdd
|
|
700
|
+
key={r.key}
|
|
701
|
+
uid={uid}
|
|
702
|
+
group={r.group}
|
|
703
|
+
addLabel={addLabel}
|
|
704
|
+
open={adding === r.group.key}
|
|
705
|
+
onOpen={() => setAdding(r.group.key)}
|
|
706
|
+
onClose={() => setAdding((a) => (a === r.group.key ? null : a))}
|
|
707
|
+
onCreate={create}
|
|
708
|
+
/>
|
|
709
|
+
),
|
|
710
|
+
)}
|
|
711
|
+
</div>
|
|
712
|
+
) : null}
|
|
713
|
+
|
|
714
|
+
{/* The timeline. */}
|
|
715
|
+
<div className="relative shrink-0" style={{ width, height: rows.height }}>
|
|
716
|
+
<div aria-hidden="true" className="pointer-events-none absolute inset-0">
|
|
717
|
+
{bands.map((b) => (
|
|
718
|
+
<span key={b.start} className="absolute inset-y-0 bg-muted/50" style={{ left: b.x, width: b.w }} />
|
|
719
|
+
))}
|
|
720
|
+
{tiers.bottom.slice(1).map((c) => (
|
|
721
|
+
<span key={c.key} className="absolute inset-y-0 w-px bg-border/50" style={{ left: c.x }} />
|
|
722
|
+
))}
|
|
723
|
+
{rows.list
|
|
724
|
+
.filter((r) => r.kind === "group")
|
|
725
|
+
.map((r) => (
|
|
726
|
+
<span key={r.key} className={cx("absolute inset-x-0 bg-muted/40", r.y > 0 && "border-t border-border")} style={{ top: r.y, height: ROW }} />
|
|
727
|
+
))}
|
|
728
|
+
</div>
|
|
729
|
+
|
|
730
|
+
{rows.list.map((r) => {
|
|
731
|
+
if (r.kind === "add") return <div key={r.key} style={{ height: ADD_ROW }} />;
|
|
732
|
+
if (r.kind === "group") {
|
|
733
|
+
const s = groupSpan(r.group.tasks, byId);
|
|
734
|
+
return (
|
|
735
|
+
<div key={r.key} className={cx("relative", hot === r.key && "bg-accent/40")} style={{ height: ROW }} onPointerEnter={() => setHot(r.key)}>
|
|
736
|
+
{s ? (
|
|
737
|
+
<span
|
|
738
|
+
aria-hidden="true"
|
|
739
|
+
className="absolute top-1/2 h-1.5 -translate-y-1/2 rounded-full bg-muted-foreground/25"
|
|
740
|
+
style={{ left: xOf(s.start, from, dw), width: Math.max(lengthOf(s) * dw, 6) }}
|
|
741
|
+
/>
|
|
742
|
+
) : null}
|
|
743
|
+
</div>
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
const box = boxes.get(r.task.id);
|
|
747
|
+
const first = box && Math.abs(box.y - (r.y + ROW / 2)) < 1;
|
|
748
|
+
return (
|
|
749
|
+
<TimelineRow key={r.key} row={r} hot={hot === r.key} onHot={() => setHot(r.key)} linking={linking && editable}>
|
|
750
|
+
{(over) =>
|
|
751
|
+
r.span ? (
|
|
752
|
+
<Bar
|
|
753
|
+
row={r}
|
|
754
|
+
x={r.milestone && !r.hasChildren ? xOf(r.span.end, from, dw) : xOf(r.span.start, from, dw)}
|
|
755
|
+
w={r.milestone && !r.hasChildren ? dw : Math.max(lengthOf(r.span) * dw, 6)}
|
|
756
|
+
dayWidth={dw}
|
|
757
|
+
tint={tintOf(r.task)}
|
|
758
|
+
shape={r.hasChildren ? "summary" : r.milestone ? "milestone" : "bar"}
|
|
759
|
+
editable={editable && first !== false}
|
|
760
|
+
onOpen={onOpen}
|
|
761
|
+
onPreview={onPreview}
|
|
762
|
+
onNudge={onNudge}
|
|
763
|
+
clickGuard={clickGuard}
|
|
764
|
+
dim={critical && path.size > 0 && !path.has(r.task.id)}
|
|
765
|
+
critical={critical && path.has(r.task.id)}
|
|
766
|
+
over={over}
|
|
767
|
+
moving={Boolean(drag && moved.has(r.task.id))}
|
|
768
|
+
hintId={`${uid}-hint`}
|
|
769
|
+
locale={locale}
|
|
770
|
+
/>
|
|
771
|
+
) : editable ? (
|
|
772
|
+
<Unscheduled row={r} dayWidth={dw} length={r.milestone ? 1 : newTaskLength} onSchedule={schedule} />
|
|
773
|
+
) : null
|
|
774
|
+
}
|
|
775
|
+
</TimelineRow>
|
|
776
|
+
);
|
|
777
|
+
})}
|
|
778
|
+
|
|
779
|
+
{/* raw: an SVG drawing — the dependency arrows over the
|
|
780
|
+
bars; no kit part draws them. */}
|
|
781
|
+
<svg aria-hidden="true" className="pointer-events-none absolute inset-0 z-[5] overflow-visible" width={width} height={rows.height}>
|
|
782
|
+
{edges.map((e) => {
|
|
783
|
+
const a = boxes.get(e.from);
|
|
784
|
+
const b = boxes.get(e.to);
|
|
785
|
+
const { d, head } = arrowPath(a, b, { rowHeight: ROW });
|
|
786
|
+
const bad = conflicts.has(`${e.from}>${e.to}`);
|
|
787
|
+
const hotPath = critical && path.has(e.from) && path.has(e.to);
|
|
788
|
+
const faded = critical && path.size > 0 && !hotPath;
|
|
789
|
+
const stroke = bad ? "stroke-destructive" : hotPath ? "stroke-primary" : "stroke-muted-foreground/70";
|
|
790
|
+
const fill = bad ? "fill-destructive" : hotPath ? "fill-primary" : "fill-muted-foreground/70";
|
|
791
|
+
return (
|
|
792
|
+
<g key={`${e.from}>${e.to}`} className={cx(faded && "opacity-30", "motion-safe:transition-opacity motion-safe:duration-150")}>
|
|
793
|
+
<path d={d} fill="none" className={stroke} strokeWidth={hotPath ? 2 : 1.25} strokeDasharray={bad ? "4 3" : undefined} strokeLinecap="round" />
|
|
794
|
+
<path d={head} className={fill} />
|
|
795
|
+
</g>
|
|
796
|
+
);
|
|
797
|
+
})}
|
|
798
|
+
</svg>
|
|
799
|
+
|
|
800
|
+
{todayIn ? (
|
|
801
|
+
<span aria-hidden="true" className="pointer-events-none absolute inset-y-0 z-[4] w-px bg-primary/70" style={{ left: tx + dw / 2 }} />
|
|
802
|
+
) : null}
|
|
803
|
+
</div>
|
|
804
|
+
</div>
|
|
805
|
+
</div>
|
|
806
|
+
</div>
|
|
807
|
+
</TaskDragProvider>
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return (
|
|
812
|
+
<section aria-label={label} className={cx("grid min-w-0 gap-3", className)}>
|
|
813
|
+
{toolbar && all.length ? (
|
|
814
|
+
<ViewToolbar
|
|
815
|
+
view={view}
|
|
816
|
+
onViewChange={setViewRaw}
|
|
817
|
+
ctx={ctx}
|
|
818
|
+
tasks={all}
|
|
819
|
+
controls={["search", "filter", "group", "sort"]}
|
|
820
|
+
groupFields={GROUP_FIELDS}
|
|
821
|
+
count={shown.tasks.length}
|
|
822
|
+
extra={
|
|
823
|
+
toolbarExtra || addButton ? (
|
|
824
|
+
<>
|
|
825
|
+
{toolbarExtra}
|
|
826
|
+
{addButton}
|
|
827
|
+
</>
|
|
828
|
+
) : null
|
|
829
|
+
}
|
|
830
|
+
/>
|
|
831
|
+
) : null}
|
|
832
|
+
{readError ? <Notice tone="danger">{readError}</Notice> : null}
|
|
833
|
+
{error ? (
|
|
834
|
+
<Notice tone="danger" onDismiss={clearError}>
|
|
835
|
+
{error}
|
|
836
|
+
</Notice>
|
|
837
|
+
) : null}
|
|
838
|
+
{notice ? (
|
|
839
|
+
<Notice tone="warning" onDismiss={() => setNotice(null)}>
|
|
840
|
+
{notice}
|
|
841
|
+
</Notice>
|
|
842
|
+
) : null}
|
|
843
|
+
<div className="min-w-0 overflow-hidden rounded-lg border border-border bg-card text-card-foreground">
|
|
844
|
+
{all.length && !loading ? bar : null}
|
|
845
|
+
{content}
|
|
846
|
+
{critical && all.length && !loading ? (
|
|
847
|
+
<p className="border-t border-border px-3 py-2 text-xs text-muted-foreground" aria-live="polite">
|
|
848
|
+
{path.size > 1 ? (
|
|
849
|
+
<>
|
|
850
|
+
<Icon name="zap" size={12} className="me-1 inline-block align-[-2px] text-primary" />
|
|
851
|
+
The critical path: {path.size} linked tasks, {pathDays} days end to end. Any day they slip, the finish slips.
|
|
852
|
+
</>
|
|
853
|
+
) : (
|
|
854
|
+
"No critical path yet: link tasks (drag the dot past a bar's end onto another bar) to see the chain that sets the finish."
|
|
855
|
+
)}
|
|
856
|
+
</p>
|
|
857
|
+
) : null}
|
|
858
|
+
</div>
|
|
859
|
+
<p id={`${uid}-hint`} className="sr-only">
|
|
860
|
+
Left and right arrows move it a day; with Shift they change its end, with Alt its start. Enter saves at once or opens it; Escape cancels a move.
|
|
861
|
+
</p>
|
|
862
|
+
<p className="sr-only" role="status" aria-live="polite">
|
|
863
|
+
{announce}
|
|
864
|
+
</p>
|
|
865
|
+
</section>
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function groupSpan(tasks, byId) {
|
|
870
|
+
let s = null;
|
|
871
|
+
for (const t of tasks) {
|
|
872
|
+
const x = spanOf(byId.get(t.id) ?? t);
|
|
873
|
+
if (!x) continue;
|
|
874
|
+
s = s ? { start: x.start < s.start ? x.start : s.start, end: x.end > s.end ? x.end : s.end } : x;
|
|
875
|
+
}
|
|
876
|
+
return s;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// The empty state's action: name the first task (it starts today), or
|
|
880
|
+
// start from a template.
|
|
881
|
+
function FirstTask({ onCreate, starters, onStarter, addLabel, uid }) {
|
|
882
|
+
const [draft, setDraft] = useState("");
|
|
883
|
+
const [busy, setBusy] = useState(false);
|
|
884
|
+
async function submit() {
|
|
885
|
+
const title = draft.trim();
|
|
886
|
+
if (!title || busy) return;
|
|
887
|
+
setBusy(true);
|
|
888
|
+
await onCreate(title);
|
|
889
|
+
setBusy(false);
|
|
890
|
+
setDraft("");
|
|
891
|
+
}
|
|
892
|
+
return (
|
|
893
|
+
<div className="grid w-full max-w-sm justify-items-center gap-3">
|
|
894
|
+
<form
|
|
895
|
+
className="flex w-full items-center gap-2"
|
|
896
|
+
onSubmit={(e) => {
|
|
897
|
+
e.preventDefault();
|
|
898
|
+
submit();
|
|
899
|
+
}}
|
|
900
|
+
>
|
|
901
|
+
<label htmlFor={`${uid}-first`} className="sr-only">
|
|
902
|
+
The first task
|
|
903
|
+
</label>
|
|
904
|
+
<Input id={`${uid}-first`} value={draft} placeholder="What needs doing first?" onChange={(e) => setDraft(e.target.value)} />
|
|
905
|
+
<Button type="submit" variant="primary" icon="plus" loading={busy} disabled={!draft.trim()}>
|
|
906
|
+
{addLabel}
|
|
907
|
+
</Button>
|
|
908
|
+
</form>
|
|
909
|
+
{starters?.length ? (
|
|
910
|
+
<div className="grid justify-items-center gap-1.5">
|
|
911
|
+
<span className="text-xs text-muted-foreground">Or start from a plan</span>
|
|
912
|
+
<div className="flex flex-wrap justify-center gap-2">
|
|
913
|
+
{starters.map((s) => (
|
|
914
|
+
<Button key={s.label} size="sm" variant="secondary" icon={s.icon ?? "sparkles"} onClick={() => onStarter(s)}>
|
|
915
|
+
{s.label}
|
|
916
|
+
</Button>
|
|
917
|
+
))}
|
|
918
|
+
</div>
|
|
919
|
+
</div>
|
|
920
|
+
) : null}
|
|
921
|
+
</div>
|
|
922
|
+
);
|
|
923
|
+
}
|