makersclaw 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +41 -1
- package/THIRD-PARTY-NOTICES.md +180 -0
- package/package.json +15 -8
- 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 +55 -0
- 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 +341 -0
- 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 +56 -0
- 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 +3612 -0
- 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 +64 -13
- package/registry/blocks/board/block.json +2 -2
- package/registry/blocks/board/board.jsx +237 -94
- 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 +86 -2
- 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 +171 -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/parts/fixture.jsx +1 -1
- package/templates/next/files/components/parts/sample.js +15 -0
- 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/files/lib/sample.js +4 -12
- package/templates/next/template.json +264 -8
|
@@ -0,0 +1,1329 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// FlowCanvas — a Miro-like board on React Flow, in the workspace theme,
|
|
4
|
+
// behind one set of props (README.md beside this file).
|
|
5
|
+
//
|
|
6
|
+
// Client only, one static bundle: the modules load with the page (the
|
|
7
|
+
// app's check refuses import()), and the board itself mounts after the
|
|
8
|
+
// first effect (the mount guard). Until then — on the server and in the
|
|
9
|
+
// first client paint — a box of the same size holds the board as text, so
|
|
10
|
+
// the server HTML already says what is on it.
|
|
11
|
+
//
|
|
12
|
+
// State: nodes and edges live here (React Flow's shape). A change that
|
|
13
|
+
// should be undoable COMMITS: a drag ends, a card is placed, words stop
|
|
14
|
+
// being edited, a colour is picked. A commit runs the block's constraints
|
|
15
|
+
// (`constrain`, `beforeChange`), pushes a snapshot onto the history
|
|
16
|
+
// (board.js historyReducer) and schedules one onChange/onSave after
|
|
17
|
+
// `changeDelay` ms. Selection and the view never commit.
|
|
18
|
+
import "@xyflow/react/dist/style.css";
|
|
19
|
+
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
|
|
20
|
+
import {
|
|
21
|
+
Background,
|
|
22
|
+
BackgroundVariant,
|
|
23
|
+
ConnectionMode,
|
|
24
|
+
MiniMap,
|
|
25
|
+
ReactFlow,
|
|
26
|
+
ReactFlowProvider,
|
|
27
|
+
SelectionMode,
|
|
28
|
+
ViewportPortal,
|
|
29
|
+
applyEdgeChanges,
|
|
30
|
+
applyNodeChanges,
|
|
31
|
+
useConnection,
|
|
32
|
+
useReactFlow,
|
|
33
|
+
useStore,
|
|
34
|
+
} from "@xyflow/react";
|
|
35
|
+
import { ContextMenu, cx } from "../../ui";
|
|
36
|
+
import {
|
|
37
|
+
AUTO_HEIGHT,
|
|
38
|
+
EDITABLE,
|
|
39
|
+
GRID,
|
|
40
|
+
SHAPE_NAMES,
|
|
41
|
+
STICKY_COLORS,
|
|
42
|
+
alignNodes,
|
|
43
|
+
arrangeNodes,
|
|
44
|
+
autoLayout,
|
|
45
|
+
boardBounds,
|
|
46
|
+
boardKey,
|
|
47
|
+
boardToMarkdown,
|
|
48
|
+
boardToText,
|
|
49
|
+
cleanBoard,
|
|
50
|
+
createEdge,
|
|
51
|
+
createNode,
|
|
52
|
+
distributeNodes,
|
|
53
|
+
duplicateNodes,
|
|
54
|
+
emptyBoard,
|
|
55
|
+
facingSides,
|
|
56
|
+
frameContents,
|
|
57
|
+
historyInit,
|
|
58
|
+
historyReducer,
|
|
59
|
+
makeId,
|
|
60
|
+
nodeRect,
|
|
61
|
+
readBoard,
|
|
62
|
+
sidePoint,
|
|
63
|
+
starterTemplates,
|
|
64
|
+
} from "./board";
|
|
65
|
+
import { ConnectMenu, EdgeToolbar, EmptyState, FloatAt, HistoryBar, ListView, PresentBar, SelectionToolbar, ShortcutsSheet, ToolFlyout, ToolRail, ZoomBar, modKey } from "./chrome";
|
|
66
|
+
import { CanvasContext } from "./context";
|
|
67
|
+
import { BUILT_IN_EDGES, connectorGeometry } from "./edges";
|
|
68
|
+
import { BUILT_IN_NODES } from "./nodes";
|
|
69
|
+
import { THEME } from "./theme";
|
|
70
|
+
|
|
71
|
+
/** A ref for the canvas handle: `const board = useFlowCanvas()`, then
|
|
72
|
+
* `<FlowCanvas canvasRef={board} />` and `board.current?.fitView()`. */
|
|
73
|
+
export function useFlowCanvas() {
|
|
74
|
+
return useRef(null);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The built-in tools. `place` names the node type a click puts down;
|
|
78
|
+
// `sizable` ones can be dragged out to a size.
|
|
79
|
+
const TOOLS = {
|
|
80
|
+
select: { id: "select", label: "Select", icon: "mouse-pointer", shortcut: "V", kind: "mode" },
|
|
81
|
+
hand: { id: "hand", label: "Hand", icon: "hand", shortcut: "H", kind: "mode" },
|
|
82
|
+
sticky: { id: "sticky", label: "Sticky note", icon: "sticky-note", shortcut: "N", kind: "mode", place: "sticky" },
|
|
83
|
+
shape: { id: "shape", label: "Shape", icon: "square", shortcut: "S", kind: "mode", place: "shape", sizable: true },
|
|
84
|
+
text: { id: "text", label: "Text", icon: "type", shortcut: "T", kind: "mode", place: "text", sizable: true },
|
|
85
|
+
connector: { id: "connector", label: "Connector", icon: "connector", shortcut: "L", kind: "mode" },
|
|
86
|
+
frame: { id: "frame", label: "Frame", icon: "frame", shortcut: "F", kind: "mode", place: "frame", sizable: true },
|
|
87
|
+
image: { id: "image", label: "Image", icon: "image", shortcut: "I", kind: "action" },
|
|
88
|
+
};
|
|
89
|
+
const SHAPE_ICON = { rect: "square", ellipse: "circle", diamond: "diamond", pill: "minus" };
|
|
90
|
+
const DEFAULT_RAIL = ["select", "hand", "|", "sticky", "shape", "text", "connector", "frame"];
|
|
91
|
+
// Props React Flow compares by identity: made once, not every render.
|
|
92
|
+
const EDGE_DEFAULTS = { type: "flow" };
|
|
93
|
+
const MULTI_KEYS = ["Shift", "Meta", "Control"];
|
|
94
|
+
const MIDDLE_BUTTON = [1];
|
|
95
|
+
const FIT_OPTIONS = { padding: 0.2, maxZoom: 1 };
|
|
96
|
+
const EDITING_TARGET = '[contenteditable="true"],[contenteditable="plaintext-only"],input,textarea,select,[role="menu"],[role="dialog"],[popover]';
|
|
97
|
+
|
|
98
|
+
/** Anything board-shaped → React Flow's nodes and edges. */
|
|
99
|
+
function prepare(board) {
|
|
100
|
+
const { nodes, edges } = readBoard(board);
|
|
101
|
+
return {
|
|
102
|
+
nodes: nodes.map((n) => ({ ...n, position: { x: Number(n.position?.x) || 0, y: Number(n.position?.y) || 0 }, data: n.data ?? {}, ...(n.type === "frame" && n.zIndex === undefined ? { zIndex: -1 } : null) })),
|
|
103
|
+
edges: edges.map((e) => ({ ...e, type: e.type ?? "flow", data: e.data ?? {} })),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A floating toolbar over something on the board, placed in SCREEN space
|
|
109
|
+
* with left/top and no CSS transform (FloatAt): a kit Menu or Tooltip
|
|
110
|
+
* inside it anchors to its button, and CSS anchor positioning is not told
|
|
111
|
+
* about a transformed ancestor such as React Flow's viewport. Only this
|
|
112
|
+
* re-renders as the view pans and zooms.
|
|
113
|
+
*/
|
|
114
|
+
function Floating({ at, children }) {
|
|
115
|
+
const [tx, ty, z] = useStore((s) => s.transform);
|
|
116
|
+
return (
|
|
117
|
+
<FloatAt x={at.x * z + tx} top={at.top * z + ty - at.lift} bottom={at.bottom * z + ty + 14}>
|
|
118
|
+
{children}
|
|
119
|
+
</FloatAt>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The zoom as a percentage; re-renders alone as the view zooms. */
|
|
124
|
+
function ZoomLevel() {
|
|
125
|
+
const z = useStore((s) => s.transform[2]);
|
|
126
|
+
return <>{Math.round((z || 1) * 100)}%</>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function usePrefersReducedMotion() {
|
|
130
|
+
const [reduce, setReduce] = useState(false);
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
133
|
+
const on = () => setReduce(mq.matches);
|
|
134
|
+
on();
|
|
135
|
+
mq.addEventListener("change", on);
|
|
136
|
+
return () => mq.removeEventListener("change", on);
|
|
137
|
+
}, []);
|
|
138
|
+
return reduce;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function FlowCanvas(props) {
|
|
142
|
+
const [mounted, setMounted] = useState(false);
|
|
143
|
+
useEffect(() => setMounted(true), []);
|
|
144
|
+
if (!mounted) return <Placeholder {...props} />;
|
|
145
|
+
return (
|
|
146
|
+
<ReactFlowProvider>
|
|
147
|
+
<LiveCanvas {...props} />
|
|
148
|
+
</ReactFlowProvider>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The server's (and the first paint's) board: its box, and its words for
|
|
153
|
+
* a reader. Not interactive. */
|
|
154
|
+
function Placeholder({ value, defaultValue, height = 560, bare = false, className, label = "Board", describeNode }) {
|
|
155
|
+
const text = boardToText(value ?? defaultValue ?? emptyBoard(), { describe: describeNode });
|
|
156
|
+
return (
|
|
157
|
+
<div role="region" aria-label={label} aria-busy="true" className={cx("relative overflow-hidden bg-background", !bare && "rounded-lg border border-border", className)} style={{ height }}>
|
|
158
|
+
<p className="sr-only whitespace-pre-line">{text}</p>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function LiveCanvas(props) {
|
|
164
|
+
const {
|
|
165
|
+
value,
|
|
166
|
+
defaultValue,
|
|
167
|
+
readOnly: readOnlyProp = false,
|
|
168
|
+
presentation = false,
|
|
169
|
+
onExitPresentation,
|
|
170
|
+
nodeTypes: ownNodeTypes,
|
|
171
|
+
tools: ownTools,
|
|
172
|
+
rail,
|
|
173
|
+
topBar,
|
|
174
|
+
toolbarExtra,
|
|
175
|
+
contextMenuItems,
|
|
176
|
+
emptyState,
|
|
177
|
+
templates: templatesProp,
|
|
178
|
+
renderCard,
|
|
179
|
+
describeNode,
|
|
180
|
+
palette = STICKY_COLORS,
|
|
181
|
+
snap: snapProp = true,
|
|
182
|
+
grid = GRID,
|
|
183
|
+
minimap: minimapProp = false,
|
|
184
|
+
height = 560,
|
|
185
|
+
bare = false,
|
|
186
|
+
label = "Board",
|
|
187
|
+
hideAttribution = false,
|
|
188
|
+
className,
|
|
189
|
+
canvasRef,
|
|
190
|
+
} = props;
|
|
191
|
+
const readOnly = readOnlyProp || presentation;
|
|
192
|
+
const rf = useReactFlow();
|
|
193
|
+
const reduce = usePrefersReducedMotion();
|
|
194
|
+
const duration = reduce ? 0 : 220;
|
|
195
|
+
const propsRef = useRef(props);
|
|
196
|
+
propsRef.current = props;
|
|
197
|
+
|
|
198
|
+
// ── the board ──────────────────────────────────────────────────────────
|
|
199
|
+
const initial = useRef(null);
|
|
200
|
+
if (!initial.current) initial.current = prepare(value ?? defaultValue ?? emptyBoard());
|
|
201
|
+
const [nodes, setNodes] = useState(initial.current.nodes);
|
|
202
|
+
const [edges, setEdges] = useState(initial.current.edges);
|
|
203
|
+
const nodesRef = useRef(nodes);
|
|
204
|
+
const edgesRef = useRef(edges);
|
|
205
|
+
nodesRef.current = nodes;
|
|
206
|
+
edgesRef.current = edges;
|
|
207
|
+
const [history, dispatch] = useReducer(historyReducer, initial.current, historyInit);
|
|
208
|
+
const historyRef = useRef(history);
|
|
209
|
+
historyRef.current = history;
|
|
210
|
+
const lastKey = useRef(boardKey(initial.current));
|
|
211
|
+
const viewportRef = useRef(readBoard(value ?? defaultValue).viewport);
|
|
212
|
+
|
|
213
|
+
// ── ui state ───────────────────────────────────────────────────────────
|
|
214
|
+
const [tool, setTool] = useState("select");
|
|
215
|
+
const [stickyColor, setStickyColor] = useState(palette[0] ?? "warning");
|
|
216
|
+
const [shapeKind, setShapeKind] = useState("rect");
|
|
217
|
+
const [editing, setEditing] = useState({ node: null, edge: null });
|
|
218
|
+
const [connectMenu, setConnectMenu] = useState(null);
|
|
219
|
+
const [listView, setListView] = useState(false);
|
|
220
|
+
const [minimap, setMinimap] = useState(minimapProp);
|
|
221
|
+
const [snap, setSnap] = useState(snapProp);
|
|
222
|
+
const [sheet, setSheet] = useState(false);
|
|
223
|
+
const [save, setSave] = useState({ state: "idle" });
|
|
224
|
+
const [draft, setDraft] = useState(null);
|
|
225
|
+
const [narrow, setNarrow] = useState(false);
|
|
226
|
+
const [announce, setAnnounce] = useState("");
|
|
227
|
+
const [slide, setSlide] = useState(0);
|
|
228
|
+
const [ctxOn, setCtxOn] = useState("pane");
|
|
229
|
+
const wrapRef = useRef(null);
|
|
230
|
+
const frameMembers = useRef(new Map());
|
|
231
|
+
const clip = useRef(null);
|
|
232
|
+
const pasteCount = useRef(0);
|
|
233
|
+
const pointer = useRef(null);
|
|
234
|
+
const suppressClick = useRef(false);
|
|
235
|
+
const ctxPoint = useRef(null);
|
|
236
|
+
const connecting = useConnection((c) => c.inProgress);
|
|
237
|
+
const marquee = useStore((s) => s.userSelectionActive);
|
|
238
|
+
const say = (msg) => setAnnounce((m) => (m === msg ? `${msg} ` : msg));
|
|
239
|
+
|
|
240
|
+
useEffect(() => setSnap(snapProp), [snapProp]);
|
|
241
|
+
useEffect(() => setMinimap(minimapProp), [minimapProp]);
|
|
242
|
+
|
|
243
|
+
// The rail moves to the bottom when the CANVAS is narrow (not the window).
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
const el = wrapRef.current;
|
|
246
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
247
|
+
const ro = new ResizeObserver(([e]) => setNarrow(e.contentRect.width < 768));
|
|
248
|
+
ro.observe(el);
|
|
249
|
+
return () => ro.disconnect();
|
|
250
|
+
}, []);
|
|
251
|
+
|
|
252
|
+
// ── committing, history, onChange / onSave ─────────────────────────────
|
|
253
|
+
const emitTimer = useRef(0);
|
|
254
|
+
const dirty = useRef(false);
|
|
255
|
+
const saveSeq = useRef(0);
|
|
256
|
+
const currentBoard = () => cleanBoard({ nodes: nodesRef.current, edges: edgesRef.current, viewport: rf.getViewport?.() ?? viewportRef.current });
|
|
257
|
+
|
|
258
|
+
const runSave = async (board) => {
|
|
259
|
+
const onSave = propsRef.current.onSave;
|
|
260
|
+
if (!onSave) return;
|
|
261
|
+
const seq = ++saveSeq.current;
|
|
262
|
+
setSave({ state: "saving" });
|
|
263
|
+
let res;
|
|
264
|
+
try {
|
|
265
|
+
res = await onSave(board);
|
|
266
|
+
} catch (e) {
|
|
267
|
+
res = { ok: false, error: e?.message };
|
|
268
|
+
}
|
|
269
|
+
if (seq !== saveSeq.current) return;
|
|
270
|
+
setSave(res && res.ok === false ? { state: "error", error: res.error || "Not saved" } : { state: "saved" });
|
|
271
|
+
};
|
|
272
|
+
const emitNow = () => {
|
|
273
|
+
clearTimeout(emitTimer.current);
|
|
274
|
+
if (!dirty.current) return;
|
|
275
|
+
dirty.current = false;
|
|
276
|
+
const board = currentBoard();
|
|
277
|
+
lastKey.current = boardKey(board);
|
|
278
|
+
propsRef.current.onChange?.(board);
|
|
279
|
+
runSave(board);
|
|
280
|
+
};
|
|
281
|
+
const scheduleEmit = () => {
|
|
282
|
+
dirty.current = true;
|
|
283
|
+
clearTimeout(emitTimer.current);
|
|
284
|
+
emitTimer.current = setTimeout(emitNow, propsRef.current.changeDelay ?? 400);
|
|
285
|
+
};
|
|
286
|
+
// Leaving with a change not yet sent: send it now.
|
|
287
|
+
useEffect(
|
|
288
|
+
() => () => {
|
|
289
|
+
if (dirty.current) emitNow();
|
|
290
|
+
},
|
|
291
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
292
|
+
[],
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const [tick, setTick] = useState(0);
|
|
296
|
+
const pending = useRef(null);
|
|
297
|
+
const commit = useCallback((reason = "edit", ids) => {
|
|
298
|
+
const prev = pending.current;
|
|
299
|
+
pending.current = { reason, ids: [...(prev?.ids ?? []), ...(ids ?? [])] };
|
|
300
|
+
setTick((t) => t + 1);
|
|
301
|
+
}, []);
|
|
302
|
+
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
const p = pending.current;
|
|
305
|
+
if (!p) return;
|
|
306
|
+
pending.current = null;
|
|
307
|
+
const { constrain, beforeChange } = propsRef.current;
|
|
308
|
+
let ns = nodesRef.current;
|
|
309
|
+
const touched = new Set(p.ids);
|
|
310
|
+
if (touched.size) {
|
|
311
|
+
ns = ns.map((n) => {
|
|
312
|
+
if (!touched.has(n.id)) return n;
|
|
313
|
+
let next = n;
|
|
314
|
+
// A card whose height follows its words drops the height a resize gave it.
|
|
315
|
+
if (p.reason === "resize" && AUTO_HEIGHT.has(n.type) && next.height !== undefined) {
|
|
316
|
+
const { height: _h, ...rest } = next;
|
|
317
|
+
next = rest;
|
|
318
|
+
}
|
|
319
|
+
if (constrain) {
|
|
320
|
+
const out = constrain(next, { reason: p.reason, board: { nodes: ns, edges: edgesRef.current } });
|
|
321
|
+
if (out && typeof out === "object") next = { ...next, ...out };
|
|
322
|
+
}
|
|
323
|
+
return next;
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
let next = { nodes: ns, edges: edgesRef.current };
|
|
327
|
+
if (beforeChange) {
|
|
328
|
+
const out = beforeChange(cleanBoard(next, { viewport: false }), historyRef.current.present, p.reason);
|
|
329
|
+
if (out === false) {
|
|
330
|
+
const back = prepare(historyRef.current.present);
|
|
331
|
+
const sel = new Set(ns.filter((n) => n.selected).map((n) => n.id));
|
|
332
|
+
setNodes(back.nodes.map((n) => (sel.has(n.id) ? { ...n, selected: true } : n)));
|
|
333
|
+
setEdges(back.edges);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (out && typeof out === "object") {
|
|
337
|
+
const sel = new Set(ns.filter((n) => n.selected).map((n) => n.id));
|
|
338
|
+
const b = prepare(out);
|
|
339
|
+
next = { nodes: b.nodes.map((n) => (sel.has(n.id) ? { ...n, selected: true } : n)), edges: b.edges };
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (next.nodes !== nodesRef.current) setNodes(next.nodes);
|
|
343
|
+
if (next.edges !== edgesRef.current) setEdges(next.edges);
|
|
344
|
+
const before = historyRef.current.key;
|
|
345
|
+
dispatch({ type: "push", present: next });
|
|
346
|
+
if (boardKey(next) !== before) scheduleEmit();
|
|
347
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
348
|
+
}, [tick]);
|
|
349
|
+
|
|
350
|
+
const restore = (board) => {
|
|
351
|
+
const b = prepare(board);
|
|
352
|
+
setNodes(b.nodes);
|
|
353
|
+
setEdges(b.edges);
|
|
354
|
+
setEditing({ node: null, edge: null });
|
|
355
|
+
};
|
|
356
|
+
const undo = () => {
|
|
357
|
+
const h = historyRef.current;
|
|
358
|
+
if (!h.past.length) return;
|
|
359
|
+
restore(h.past[h.past.length - 1]);
|
|
360
|
+
dispatch({ type: "undo" });
|
|
361
|
+
scheduleEmit();
|
|
362
|
+
say("Undone");
|
|
363
|
+
};
|
|
364
|
+
const redo = () => {
|
|
365
|
+
const h = historyRef.current;
|
|
366
|
+
if (!h.future.length) return;
|
|
367
|
+
restore(h.future[0]);
|
|
368
|
+
dispatch({ type: "redo" });
|
|
369
|
+
scheduleEmit();
|
|
370
|
+
say("Redone");
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// Controlled: a `value` that is not what we last sent replaces the board
|
|
374
|
+
// (and can be undone).
|
|
375
|
+
useEffect(() => {
|
|
376
|
+
if (value === undefined) return;
|
|
377
|
+
const k = boardKey(value);
|
|
378
|
+
if (k === lastKey.current) return;
|
|
379
|
+
lastKey.current = k;
|
|
380
|
+
const b = prepare(value);
|
|
381
|
+
const sel = new Set(nodesRef.current.filter((n) => n.selected).map((n) => n.id));
|
|
382
|
+
setNodes(b.nodes.map((n) => (sel.has(n.id) ? { ...n, selected: true } : n)));
|
|
383
|
+
setEdges(b.edges);
|
|
384
|
+
dispatch({ type: "push", present: b });
|
|
385
|
+
}, [value]);
|
|
386
|
+
|
|
387
|
+
// ── changing the board ─────────────────────────────────────────────────
|
|
388
|
+
const selectOnly = (ids = [], edgeIds = []) => {
|
|
389
|
+
const want = new Set(ids);
|
|
390
|
+
const wantE = new Set(edgeIds);
|
|
391
|
+
setNodes((ns) => ns.map((n) => (Boolean(n.selected) === want.has(n.id) ? n : { ...n, selected: want.has(n.id) })));
|
|
392
|
+
setEdges((es) => es.map((e) => (Boolean(e.selected) === wantE.has(e.id) ? e : { ...e, selected: wantE.has(e.id) })));
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const addNodes = (list, { select = true, edit = false, reason = "create", edges: newEdges } = {}) => {
|
|
396
|
+
const arr = (Array.isArray(list) ? list : [list]).filter(Boolean).map((n) => ({
|
|
397
|
+
...n,
|
|
398
|
+
id: n.id ?? makeId(n.type ?? "n"),
|
|
399
|
+
position: n.position ?? { x: 0, y: 0 },
|
|
400
|
+
data: n.data ?? {},
|
|
401
|
+
...(n.type === "frame" && n.zIndex === undefined ? { zIndex: -1 } : null),
|
|
402
|
+
selected: select,
|
|
403
|
+
}));
|
|
404
|
+
if (!arr.length) return [];
|
|
405
|
+
setNodes((ns) => [...(select ? ns.map((n) => (n.selected ? { ...n, selected: false } : n)) : ns), ...arr]);
|
|
406
|
+
if (newEdges?.length) setEdges((es) => [...es.map((e) => (e.selected ? { ...e, selected: false } : e)), ...newEdges.map((e) => ({ ...e, type: e.type ?? "flow", data: e.data ?? {} }))]);
|
|
407
|
+
commit(reason, arr.map((n) => n.id));
|
|
408
|
+
if (edit && arr.length === 1) setEditing({ node: arr[0].id, edge: null });
|
|
409
|
+
return arr.map((n) => n.id);
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const updateNodes = (ids, fn, { commit: doCommit = true, reason = "edit" } = {}) => {
|
|
413
|
+
const set = new Set(ids);
|
|
414
|
+
setNodes((ns) => ns.map((n) => (set.has(n.id) ? fn(n) : n)));
|
|
415
|
+
if (doCommit) commit(reason);
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const removeNodes = (ids) => {
|
|
419
|
+
const gone = new Set(ids);
|
|
420
|
+
if (!gone.size) return;
|
|
421
|
+
setNodes((ns) => ns.filter((n) => !gone.has(n.id)));
|
|
422
|
+
setEdges((es) => es.filter((e) => !gone.has(e.source) && !gone.has(e.target)));
|
|
423
|
+
setEditing({ node: null, edge: null });
|
|
424
|
+
commit("delete");
|
|
425
|
+
say(`${gone.size} item${gone.size === 1 ? "" : "s"} deleted`);
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const removeEdges = (ids) => {
|
|
429
|
+
const gone = new Set(ids);
|
|
430
|
+
setEdges((es) => es.filter((e) => !gone.has(e.id)));
|
|
431
|
+
setEditing((ed) => (gone.has(ed.edge) ? { node: null, edge: null } : ed));
|
|
432
|
+
commit("delete");
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const deleteSelection = () => {
|
|
436
|
+
const ids = nodesRef.current.filter((n) => n.selected && n.deletable !== false && !n.data?.locked).map((n) => n.id);
|
|
437
|
+
const eids = edgesRef.current.filter((e) => e.selected && e.deletable !== false).map((e) => e.id);
|
|
438
|
+
if (!ids.length && !eids.length) return;
|
|
439
|
+
const gone = new Set(ids);
|
|
440
|
+
const goneE = new Set(eids);
|
|
441
|
+
setNodes((ns) => ns.filter((n) => !gone.has(n.id)));
|
|
442
|
+
setEdges((es) => es.filter((e) => !goneE.has(e.id) && !gone.has(e.source) && !gone.has(e.target)));
|
|
443
|
+
commit("delete");
|
|
444
|
+
say(`${ids.length + eids.length} deleted`);
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const duplicate = (ids) => {
|
|
448
|
+
const src = { nodes: nodesRef.current, edges: edgesRef.current };
|
|
449
|
+
const copy = duplicateNodes(src, ids.filter((id) => nodesRef.current.some((n) => n.id === id)), { offset: { x: grid * 2, y: grid * 2 } });
|
|
450
|
+
if (!copy.nodes.length) return;
|
|
451
|
+
addNodes(copy.nodes, { reason: "paste", edges: copy.edges });
|
|
452
|
+
say(`${copy.nodes.length} duplicated`);
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const pasteBoard = (data, at) => {
|
|
456
|
+
const b = readBoard(data);
|
|
457
|
+
if (!b.nodes.length) return;
|
|
458
|
+
const bounds = boardBounds(b.nodes);
|
|
459
|
+
pasteCount.current += 1;
|
|
460
|
+
const offset = at
|
|
461
|
+
? { x: Math.round((at.x - bounds.x - bounds.width / 2) / grid) * grid, y: Math.round((at.y - bounds.y - bounds.height / 2) / grid) * grid }
|
|
462
|
+
: { x: grid * 2 * pasteCount.current, y: grid * 2 * pasteCount.current };
|
|
463
|
+
const copy = duplicateNodes(b, b.nodes.map((n) => n.id), { offset });
|
|
464
|
+
addNodes(copy.nodes, { reason: "paste", edges: copy.edges });
|
|
465
|
+
say(`${copy.nodes.length} pasted`);
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const lock = (ids, on = true) => {
|
|
469
|
+
updateNodes(ids, (n) => {
|
|
470
|
+
const data = { ...n.data };
|
|
471
|
+
if (on) data.locked = true;
|
|
472
|
+
else delete data.locked;
|
|
473
|
+
const next = { ...n, data };
|
|
474
|
+
if (on) {
|
|
475
|
+
next.draggable = false;
|
|
476
|
+
next.deletable = false;
|
|
477
|
+
} else {
|
|
478
|
+
delete next.draggable;
|
|
479
|
+
delete next.deletable;
|
|
480
|
+
}
|
|
481
|
+
return next;
|
|
482
|
+
});
|
|
483
|
+
say(on ? "Locked" : "Unlocked");
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const frameSelection = (ids) => {
|
|
487
|
+
const pick = nodesRef.current.filter((n) => ids.includes(n.id));
|
|
488
|
+
const b = boardBounds(pick);
|
|
489
|
+
if (!b) return;
|
|
490
|
+
const pad = grid * 2;
|
|
491
|
+
const f = createNode("frame", null, { rect: { x: b.x - pad, y: b.y - pad, width: b.width + pad * 2, height: b.height + pad * 2 }, grid, data: { title: "Frame" } });
|
|
492
|
+
addNodes(f, { edit: true });
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const fitView = (opts) => rf.fitView({ padding: 0.2, maxZoom: 1.25, duration, ...opts });
|
|
496
|
+
const focusNode = (id) => {
|
|
497
|
+
selectOnly([id]);
|
|
498
|
+
const n = nodesRef.current.find((x) => x.id === id);
|
|
499
|
+
if (!n) return;
|
|
500
|
+
const r = nodeRect(n);
|
|
501
|
+
rf.setCenter(r.x + r.width / 2, r.y + r.height / 2, { zoom: Math.max(rf.getZoom(), 0.8), duration });
|
|
502
|
+
requestAnimationFrame(() => wrapRef.current?.querySelector(`.react-flow__node[data-id="${CSS.escape(id)}"]`)?.focus({ preventScroll: true }));
|
|
503
|
+
};
|
|
504
|
+
const centerPoint = () => {
|
|
505
|
+
const r = wrapRef.current?.getBoundingClientRect();
|
|
506
|
+
if (!r) return { x: 0, y: 0 };
|
|
507
|
+
return rf.screenToFlowPosition({ x: r.left + r.width / 2, y: r.top + r.height / 2 });
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// The handle. One object for the canvas's life: every method reads the
|
|
511
|
+
// latest state through refs, so a block may keep it.
|
|
512
|
+
const apiRef = useRef(null);
|
|
513
|
+
if (!apiRef.current) apiRef.current = {};
|
|
514
|
+
const api = apiRef.current;
|
|
515
|
+
Object.assign(api, {
|
|
516
|
+
getBoard: currentBoard,
|
|
517
|
+
setBoard: (board, { fit = true } = {}) => {
|
|
518
|
+
restore(board);
|
|
519
|
+
commit("load");
|
|
520
|
+
if (fit) setTimeout(() => fitView(), 60);
|
|
521
|
+
},
|
|
522
|
+
getSelection: () => ({ nodes: nodesRef.current.filter((n) => n.selected), edges: edgesRef.current.filter((e) => e.selected) }),
|
|
523
|
+
addNodes,
|
|
524
|
+
addEdge: (edge) => {
|
|
525
|
+
const e = edge.id ? { type: "flow", data: {}, ...edge } : createEdge(edge.source, edge.target, edge);
|
|
526
|
+
setEdges((es) => [...es, e]);
|
|
527
|
+
commit("connect");
|
|
528
|
+
return e.id;
|
|
529
|
+
},
|
|
530
|
+
updateNode: (id, patch, opts) => updateNodes([id], (n) => ({ ...n, ...(typeof patch === "function" ? patch(n) : patch) }), opts),
|
|
531
|
+
updateNodeData: (id, patch, opts) => updateNodes([id], (n) => ({ ...n, data: { ...n.data, ...(typeof patch === "function" ? patch(n.data) : patch) } }), opts),
|
|
532
|
+
updateNodesData: (ids, patch, opts) => updateNodes(ids, (n) => ({ ...n, data: { ...n.data, ...patch } }), opts),
|
|
533
|
+
removeNodes,
|
|
534
|
+
updateEdge: (id, patch, { commit: doCommit = true } = {}) => {
|
|
535
|
+
setEdges((es) => es.map((e) => (e.id === id ? { ...e, ...(typeof patch === "function" ? patch(e) : patch) } : e)));
|
|
536
|
+
if (doCommit) commit("edit");
|
|
537
|
+
},
|
|
538
|
+
removeEdges,
|
|
539
|
+
select: (ids, edgeIds) => selectOnly(ids, edgeIds),
|
|
540
|
+
duplicate,
|
|
541
|
+
lock,
|
|
542
|
+
arrange: (ids, where) => {
|
|
543
|
+
setNodes((ns) => arrangeNodes(ns, ids, where));
|
|
544
|
+
commit("arrange");
|
|
545
|
+
},
|
|
546
|
+
align: (ids, how) => {
|
|
547
|
+
setNodes((ns) => alignNodes(ns, ids, how));
|
|
548
|
+
commit("align", ids);
|
|
549
|
+
},
|
|
550
|
+
distribute: (ids, axis) => {
|
|
551
|
+
setNodes((ns) => distributeNodes(ns, ids, axis));
|
|
552
|
+
commit("align", ids);
|
|
553
|
+
},
|
|
554
|
+
autoLayout: (opts = {}) => {
|
|
555
|
+
setNodes((ns) => autoLayout(ns, edgesRef.current, { grid, ...opts }));
|
|
556
|
+
commit("layout", opts.ids ?? nodesRef.current.map((n) => n.id));
|
|
557
|
+
if (!opts.ids) setTimeout(() => fitView(), 60);
|
|
558
|
+
},
|
|
559
|
+
frameSelection,
|
|
560
|
+
edit: (id) => setEditing({ node: id, edge: null }),
|
|
561
|
+
editEdge: (id) => setEditing({ node: null, edge: id }),
|
|
562
|
+
stopEditing: () => {
|
|
563
|
+
setEditing({ node: null, edge: null });
|
|
564
|
+
// Words emptied on a connector label go away with it.
|
|
565
|
+
setEdges((es) => es.map((e) => (typeof e.label === "string" && !e.label.trim() ? { ...e, label: undefined } : e)));
|
|
566
|
+
commit("edit");
|
|
567
|
+
requestAnimationFrame(() => {
|
|
568
|
+
if (!wrapRef.current?.contains(document.activeElement)) wrapRef.current?.focus({ preventScroll: true });
|
|
569
|
+
});
|
|
570
|
+
},
|
|
571
|
+
commit,
|
|
572
|
+
undo,
|
|
573
|
+
redo,
|
|
574
|
+
canUndo: () => historyRef.current.past.length > 0,
|
|
575
|
+
canRedo: () => historyRef.current.future.length > 0,
|
|
576
|
+
setTool: (id) => setTool(id),
|
|
577
|
+
getTool: () => tool,
|
|
578
|
+
fitView,
|
|
579
|
+
focusNode,
|
|
580
|
+
zoomIn: () => rf.zoomIn({ duration }),
|
|
581
|
+
zoomOut: () => rf.zoomOut({ duration }),
|
|
582
|
+
zoomTo: (z) => rf.zoomTo(z, { duration }),
|
|
583
|
+
centerPoint,
|
|
584
|
+
screenToFlow: (p) => rf.screenToFlowPosition(p),
|
|
585
|
+
toText: () => boardToText(currentBoard(), { describe: propsRef.current.describeNode }),
|
|
586
|
+
toMarkdown: (opts) => boardToMarkdown(currentBoard(), { describe: propsRef.current.describeNode, ...opts }),
|
|
587
|
+
save: () => {
|
|
588
|
+
dirty.current = true;
|
|
589
|
+
emitNow();
|
|
590
|
+
},
|
|
591
|
+
readOnly,
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
useEffect(() => {
|
|
595
|
+
if (!canvasRef) return;
|
|
596
|
+
canvasRef.current = api;
|
|
597
|
+
return () => {
|
|
598
|
+
if (canvasRef.current === api) canvasRef.current = null;
|
|
599
|
+
};
|
|
600
|
+
}, [canvasRef, api]);
|
|
601
|
+
|
|
602
|
+
// ── tools ──────────────────────────────────────────────────────────────
|
|
603
|
+
const customs = useMemo(
|
|
604
|
+
() =>
|
|
605
|
+
(ownTools ?? []).map((t) => ({ ...t, kind: t.onSelect && !t.create ? "action" : "mode", custom: true })),
|
|
606
|
+
[ownTools],
|
|
607
|
+
);
|
|
608
|
+
const toolById = useMemo(() => {
|
|
609
|
+
const m = new Map(Object.values(TOOLS).map((t) => [t.id, t]));
|
|
610
|
+
for (const t of customs) m.set(t.id, t);
|
|
611
|
+
return m;
|
|
612
|
+
}, [customs]);
|
|
613
|
+
const railItems = useMemo(() => {
|
|
614
|
+
const ids = rail ?? [...DEFAULT_RAIL, ...customs.map((t) => t.id), ...(props.pickImage ? ["image"] : [])];
|
|
615
|
+
return ids.map((id) => (id === "|" ? "|" : toolById.get(id))).filter(Boolean);
|
|
616
|
+
}, [rail, customs, toolById, props.pickImage]);
|
|
617
|
+
const active = toolById.get(tool) ?? TOOLS.select;
|
|
618
|
+
const placing = Boolean(active.place || active.create);
|
|
619
|
+
const cursorTool = tool === "hand" ? "hand" : tool === "connector" ? "connector" : tool === "text" ? "text" : placing ? "place" : "select";
|
|
620
|
+
|
|
621
|
+
const pickTool = (t) => {
|
|
622
|
+
if (!t || readOnly) return;
|
|
623
|
+
setConnectMenu(null);
|
|
624
|
+
if (t.id === "image") {
|
|
625
|
+
Promise.resolve(propsRef.current.pickImage?.())
|
|
626
|
+
.then((img) => {
|
|
627
|
+
if (!img?.src) return;
|
|
628
|
+
addNodes(createNode("image", centerPoint(), { grid, data: { src: img.src, alt: img.alt ?? "" }, width: img.width, height: img.height }));
|
|
629
|
+
})
|
|
630
|
+
.catch(() => {});
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (t.kind === "action") {
|
|
634
|
+
t.onSelect?.(api);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
setTool((cur) => (cur === t.id && t.id !== "select" ? "select" : t.id));
|
|
638
|
+
if (t.id !== "select") selectOnly([]);
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
/** Put down what a tool makes, at a board point (or filling a rect). */
|
|
642
|
+
const place = (t, point, rect) => {
|
|
643
|
+
let made;
|
|
644
|
+
if (t.create) {
|
|
645
|
+
made = t.create(point, { rect, api, grid });
|
|
646
|
+
} else if (t.place) {
|
|
647
|
+
const data = t.place === "sticky" ? { color: stickyColor } : t.place === "shape" ? { shape: shapeKind } : undefined;
|
|
648
|
+
made = createNode(t.place, point, { grid: snap ? grid : 0, data, rect: t.place === "text" && rect ? { ...rect, height: undefined } : rect });
|
|
649
|
+
if (t.place === "text" && rect) delete made.height;
|
|
650
|
+
}
|
|
651
|
+
const list = (Array.isArray(made) ? made : [made]).filter(Boolean);
|
|
652
|
+
if (!list.length) return;
|
|
653
|
+
const one = list.length === 1 ? list[0] : null;
|
|
654
|
+
addNodes(list, { edit: Boolean(one && EDITABLE.has(one.type)) });
|
|
655
|
+
setTool("select");
|
|
656
|
+
say(`${t.label} added`);
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
// ── keyboard ───────────────────────────────────────────────────────────
|
|
660
|
+
const nudge = useRef(0);
|
|
661
|
+
const selectedNodes = nodes.filter((n) => n.selected);
|
|
662
|
+
const selectedEdges = edges.filter((e) => e.selected);
|
|
663
|
+
|
|
664
|
+
const frames = useMemo(
|
|
665
|
+
() =>
|
|
666
|
+
nodes
|
|
667
|
+
.filter((n) => n.type === "frame")
|
|
668
|
+
.sort((a, b) => {
|
|
669
|
+
const ra = nodeRect(a);
|
|
670
|
+
const rb = nodeRect(b);
|
|
671
|
+
return Math.abs(ra.y - rb.y) > 40 ? ra.y - rb.y : ra.x - rb.x;
|
|
672
|
+
}),
|
|
673
|
+
[nodes],
|
|
674
|
+
);
|
|
675
|
+
const goSlide = (i) => {
|
|
676
|
+
const f = frames[Math.max(0, Math.min(frames.length - 1, i))];
|
|
677
|
+
if (!f) return;
|
|
678
|
+
setSlide(frames.indexOf(f));
|
|
679
|
+
rf.fitView({ nodes: [{ id: f.id }], padding: 0.08, duration: reduce ? 0 : 400 });
|
|
680
|
+
};
|
|
681
|
+
useEffect(() => {
|
|
682
|
+
if (!presentation) return;
|
|
683
|
+
const t = setTimeout(() => (frames.length ? goSlide(0) : fitView()), 80);
|
|
684
|
+
return () => clearTimeout(t);
|
|
685
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
686
|
+
}, [presentation]);
|
|
687
|
+
|
|
688
|
+
const onKeyDownCapture = (e) => {
|
|
689
|
+
const t = e.target;
|
|
690
|
+
if (t?.closest?.(EDITING_TARGET)) return;
|
|
691
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
692
|
+
const k = e.key;
|
|
693
|
+
const low = k.length === 1 ? k.toLowerCase() : k;
|
|
694
|
+
const onControl = Boolean(t?.closest?.("button, a[href]"));
|
|
695
|
+
const done = () => {
|
|
696
|
+
e.preventDefault();
|
|
697
|
+
e.stopPropagation();
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
// View keys work everywhere, read-only too.
|
|
701
|
+
if (e.shiftKey && !mod && (e.code === "Digit1" || k === "!")) return done(), fitView();
|
|
702
|
+
if (e.shiftKey && !mod && (e.code === "Digit2" || k === "@")) {
|
|
703
|
+
done();
|
|
704
|
+
const sel = nodesRef.current.filter((n) => n.selected);
|
|
705
|
+
return sel.length ? fitView({ nodes: sel.map((n) => ({ id: n.id })), maxZoom: 2 }) : fitView();
|
|
706
|
+
}
|
|
707
|
+
if ((mod && (k === "=" || k === "+")) || (!mod && !onControl && (k === "+" || k === "="))) return done(), rf.zoomIn({ duration });
|
|
708
|
+
if ((mod && k === "-") || (!mod && !onControl && k === "-")) return done(), rf.zoomOut({ duration });
|
|
709
|
+
if (mod && k === "0") return done(), rf.zoomTo(1, { duration });
|
|
710
|
+
if (k === "?" && !mod) return done(), setSheet(true);
|
|
711
|
+
if (presentation) {
|
|
712
|
+
if (k === "ArrowRight" || k === "PageDown" || (k === " " && !onControl)) return done(), goSlide(slide + 1);
|
|
713
|
+
if (k === "ArrowLeft" || k === "PageUp") return done(), goSlide(slide - 1);
|
|
714
|
+
if (k === "Escape") return done(), propsRef.current.onExitPresentation?.();
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (k === "Escape") {
|
|
718
|
+
if (editing.node || editing.edge) return done(), setEditing({ node: null, edge: null });
|
|
719
|
+
if (connectMenu) return done(), setConnectMenu(null);
|
|
720
|
+
if (listView) return done(), setListView(false);
|
|
721
|
+
if (tool !== "select") return done(), setTool("select");
|
|
722
|
+
if (selectedNodes.length || selectedEdges.length) return done(), selectOnly([]);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (readOnly) return;
|
|
726
|
+
|
|
727
|
+
if (mod && low === "z") return done(), e.shiftKey ? redo() : undo();
|
|
728
|
+
if (mod && low === "y") return done(), redo();
|
|
729
|
+
if (mod && low === "s") return done(), api.save();
|
|
730
|
+
if (mod && low === "a") return done(), selectOnly(nodesRef.current.filter((n) => n.selectable !== false).map((n) => n.id));
|
|
731
|
+
const ids = nodesRef.current.filter((n) => n.selected).map((n) => n.id);
|
|
732
|
+
if (mod && low === "d") return done(), ids.length && duplicate(ids);
|
|
733
|
+
if (mod && e.shiftKey && low === "l") return done(), ids.length && lock(ids, !nodesRef.current.some((n) => n.selected && n.data?.locked));
|
|
734
|
+
if (mod && k === "]") return done(), ids.length && api.arrange(ids, "front");
|
|
735
|
+
if (mod && k === "[") return done(), ids.length && api.arrange(ids, "back");
|
|
736
|
+
if (mod) return;
|
|
737
|
+
|
|
738
|
+
if (!onControl && (k === "Delete" || k === "Backspace")) return done(), deleteSelection();
|
|
739
|
+
if (!onControl && k === "Enter") {
|
|
740
|
+
const focused = t?.classList?.contains("react-flow__node") ? t.dataset.id : null;
|
|
741
|
+
if (focused && !nodesRef.current.find((n) => n.id === focused)?.selected) return done(), selectOnly([focused]);
|
|
742
|
+
if (ids.length === 1) {
|
|
743
|
+
const n = nodesRef.current.find((x) => x.id === ids[0]);
|
|
744
|
+
if (n && !n.data?.locked) return done(), openNode(n);
|
|
745
|
+
}
|
|
746
|
+
const eid = edgesRef.current.filter((x) => x.selected).map((x) => x.id);
|
|
747
|
+
if (eid.length === 1) return done(), setEditing({ node: null, edge: eid[0] });
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (!onControl && k.startsWith("Arrow") && ids.length) {
|
|
751
|
+
done();
|
|
752
|
+
const step = e.shiftKey ? 10 : 1;
|
|
753
|
+
const dx = k === "ArrowLeft" ? -step : k === "ArrowRight" ? step : 0;
|
|
754
|
+
const dy = k === "ArrowUp" ? -step : k === "ArrowDown" ? step : 0;
|
|
755
|
+
const movable = nodesRef.current.filter((n) => n.selected && n.draggable !== false && !n.data?.locked).map((n) => n.id);
|
|
756
|
+
updateNodes(movable, (n) => ({ ...n, position: { x: n.position.x + dx, y: n.position.y + dy } }), { commit: false });
|
|
757
|
+
clearTimeout(nudge.current);
|
|
758
|
+
nudge.current = setTimeout(() => commit("move", movable), 400);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (!e.altKey && k.length === 1) {
|
|
762
|
+
const hit = railItems.find((x) => x !== "|" && x.shortcut && x.shortcut.toLowerCase() === low);
|
|
763
|
+
if (hit) return done(), pickTool(hit);
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
// Tab onto a card selects it, so what the keys do is what is shown.
|
|
768
|
+
const onFocusCapture = (e) => {
|
|
769
|
+
const el = e.target;
|
|
770
|
+
if (!el?.classList?.contains("react-flow__node") || !el.matches(":focus-visible")) return;
|
|
771
|
+
const id = el.dataset.id;
|
|
772
|
+
const n = nodesRef.current.find((x) => x.id === id);
|
|
773
|
+
if (n && !n.selected && n.selectable !== false) selectOnly([id]);
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
// Copy, cut, paste: the board's own clipboard, with the text on the
|
|
777
|
+
// system one too (paste a list of lines from elsewhere = one sticky each).
|
|
778
|
+
useEffect(() => {
|
|
779
|
+
const MIME = "application/x-flow-canvas+json";
|
|
780
|
+
const activeHere = () => {
|
|
781
|
+
const a = document.activeElement;
|
|
782
|
+
return wrapRef.current?.contains(a) && !a?.closest?.(EDITING_TARGET);
|
|
783
|
+
};
|
|
784
|
+
const copy = (e) => {
|
|
785
|
+
if (!activeHere()) return false;
|
|
786
|
+
const ids = nodesRef.current.filter((n) => n.selected).map((n) => n.id);
|
|
787
|
+
if (!ids.length) return false;
|
|
788
|
+
const all = new Set(ids);
|
|
789
|
+
for (const id of ids) if (nodesRef.current.find((n) => n.id === id)?.type === "frame") for (const c of frameContents({ nodes: nodesRef.current }, id)) all.add(c);
|
|
790
|
+
const data = cleanBoard({ nodes: nodesRef.current.filter((n) => all.has(n.id)), edges: edgesRef.current.filter((x) => all.has(x.source) && all.has(x.target)) }, { viewport: false });
|
|
791
|
+
clip.current = data;
|
|
792
|
+
pasteCount.current = 0;
|
|
793
|
+
const text = boardToText(data, { describe: propsRef.current.describeNode });
|
|
794
|
+
clip.current.text = text;
|
|
795
|
+
e.clipboardData?.setData("text/plain", text);
|
|
796
|
+
try {
|
|
797
|
+
e.clipboardData?.setData(MIME, JSON.stringify(data));
|
|
798
|
+
} catch {}
|
|
799
|
+
e.preventDefault();
|
|
800
|
+
return true;
|
|
801
|
+
};
|
|
802
|
+
const onCopy = (e) => copy(e);
|
|
803
|
+
const onCut = (e) => {
|
|
804
|
+
if (propsRef.current.readOnly || propsRef.current.presentation) return;
|
|
805
|
+
if (copy(e)) deleteSelection();
|
|
806
|
+
};
|
|
807
|
+
const onPaste = (e) => {
|
|
808
|
+
if (!activeHere() || propsRef.current.readOnly || propsRef.current.presentation) return;
|
|
809
|
+
let data = null;
|
|
810
|
+
try {
|
|
811
|
+
const raw = e.clipboardData?.getData(MIME);
|
|
812
|
+
if (raw) data = JSON.parse(raw);
|
|
813
|
+
} catch {}
|
|
814
|
+
const text = e.clipboardData?.getData("text/plain") ?? "";
|
|
815
|
+
if (!data && clip.current && text === clip.current.text) data = clip.current;
|
|
816
|
+
e.preventDefault();
|
|
817
|
+
if (data) return pasteBoard(data, pointer.current);
|
|
818
|
+
const lines = text
|
|
819
|
+
.split(/\r?\n/)
|
|
820
|
+
.map((s) => s.replace(/^\s*(?:[-*•]|\d+[.)])\s+/, "").trim())
|
|
821
|
+
.filter(Boolean)
|
|
822
|
+
.slice(0, 24);
|
|
823
|
+
if (!lines.length) return;
|
|
824
|
+
const at = pointer.current ?? centerPoint();
|
|
825
|
+
const cols = Math.min(4, lines.length);
|
|
826
|
+
const made = lines.map((line, i) =>
|
|
827
|
+
createNode("sticky", { x: at.x + (i % cols) * 220 - ((cols - 1) * 220) / 2, y: at.y + Math.floor(i / cols) * 220 }, { grid, data: { text: line, color: stickyColor } }),
|
|
828
|
+
);
|
|
829
|
+
addNodes(made, { reason: "paste" });
|
|
830
|
+
};
|
|
831
|
+
document.addEventListener("copy", onCopy);
|
|
832
|
+
document.addEventListener("cut", onCut);
|
|
833
|
+
document.addEventListener("paste", onPaste);
|
|
834
|
+
return () => {
|
|
835
|
+
document.removeEventListener("copy", onCopy);
|
|
836
|
+
document.removeEventListener("cut", onCut);
|
|
837
|
+
document.removeEventListener("paste", onPaste);
|
|
838
|
+
};
|
|
839
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
840
|
+
}, [stickyColor]);
|
|
841
|
+
|
|
842
|
+
// ── pointer: focus, double-click, drag-to-size ─────────────────────────
|
|
843
|
+
const isPane = (el) => el?.classList?.contains("react-flow__pane");
|
|
844
|
+
const onPointerDownCapture = (e) => {
|
|
845
|
+
const a = document.activeElement;
|
|
846
|
+
if (!e.target.closest?.(EDITING_TARGET) && !wrapRef.current?.contains(a)) wrapRef.current?.focus({ preventScroll: true });
|
|
847
|
+
if (!isPane(e.target) || e.button !== 0 || readOnly) return;
|
|
848
|
+
if (connectMenu) setConnectMenu(null);
|
|
849
|
+
if (!active.sizable) return;
|
|
850
|
+
const start = rf.screenToFlowPosition({ x: e.clientX, y: e.clientY });
|
|
851
|
+
const origin = { x: e.clientX, y: e.clientY };
|
|
852
|
+
let moved = false;
|
|
853
|
+
const rectFrom = (ev) => {
|
|
854
|
+
const p = rf.screenToFlowPosition({ x: ev.clientX, y: ev.clientY });
|
|
855
|
+
const s = snap ? grid : 1;
|
|
856
|
+
const x1 = Math.round(Math.min(start.x, p.x) / s) * s;
|
|
857
|
+
const y1 = Math.round(Math.min(start.y, p.y) / s) * s;
|
|
858
|
+
const x2 = Math.round(Math.max(start.x, p.x) / s) * s;
|
|
859
|
+
const y2 = Math.round(Math.max(start.y, p.y) / s) * s;
|
|
860
|
+
return { x: x1, y: y1, width: Math.max(s, x2 - x1), height: Math.max(s, y2 - y1) };
|
|
861
|
+
};
|
|
862
|
+
const move = (ev) => {
|
|
863
|
+
if (!moved && Math.hypot(ev.clientX - origin.x, ev.clientY - origin.y) < 5) return;
|
|
864
|
+
moved = true;
|
|
865
|
+
setDraft(rectFrom(ev));
|
|
866
|
+
};
|
|
867
|
+
const up = (ev) => {
|
|
868
|
+
window.removeEventListener("pointermove", move);
|
|
869
|
+
window.removeEventListener("pointerup", up);
|
|
870
|
+
setDraft(null);
|
|
871
|
+
if (!moved) return;
|
|
872
|
+
suppressClick.current = true;
|
|
873
|
+
const r = rectFrom(ev);
|
|
874
|
+
if (r.width < grid * 2 && r.height < grid * 2) return;
|
|
875
|
+
place(active, { x: r.x + r.width / 2, y: r.y + r.height / 2 }, r);
|
|
876
|
+
};
|
|
877
|
+
window.addEventListener("pointermove", move);
|
|
878
|
+
window.addEventListener("pointerup", up);
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
const onPaneClick = (e) => {
|
|
882
|
+
setConnectMenu(null);
|
|
883
|
+
if (editing.node || editing.edge) setEditing({ node: null, edge: null });
|
|
884
|
+
if (suppressClick.current) {
|
|
885
|
+
suppressClick.current = false;
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
if (readOnly || !placing) return;
|
|
889
|
+
place(active, rf.screenToFlowPosition({ x: e.clientX, y: e.clientY }));
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
const onDoubleClick = (e) => {
|
|
893
|
+
if (readOnly || tool !== "select" || !isPane(e.target)) return;
|
|
894
|
+
const p = rf.screenToFlowPosition({ x: e.clientX, y: e.clientY });
|
|
895
|
+
const n = createNode("sticky", p, { grid: snap ? grid : 0, data: { color: stickyColor } });
|
|
896
|
+
addNodes(n, { edit: true });
|
|
897
|
+
say("Sticky added");
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
const openNode = (n) => {
|
|
901
|
+
const { onOpenNode } = propsRef.current;
|
|
902
|
+
const custom = !BUILT_IN_NODES[n.type];
|
|
903
|
+
if (onOpenNode && (custom || (n.type === "card" && renderCard) || readOnly)) return onOpenNode(n);
|
|
904
|
+
if (readOnly || n.data?.locked) return;
|
|
905
|
+
if (EDITABLE.has(n.type) || custom) setEditing({ node: n.id, edge: null });
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
// ── React Flow wiring ──────────────────────────────────────────────────
|
|
909
|
+
const onNodesChange = useCallback((changes) => {
|
|
910
|
+
const real = changes.filter((c) => c.type !== "remove");
|
|
911
|
+
setNodes((nds) => {
|
|
912
|
+
const members = frameMembers.current;
|
|
913
|
+
if (!members.size) return applyNodeChanges(real, nds);
|
|
914
|
+
const byId = new Map(nds.map((n) => [n.id, n]));
|
|
915
|
+
const moving = new Set(real.filter((c) => c.type === "position").map((c) => c.id));
|
|
916
|
+
const extra = [];
|
|
917
|
+
for (const c of real) {
|
|
918
|
+
if (c.type !== "position" || !c.position || !members.has(c.id)) continue;
|
|
919
|
+
const prev = byId.get(c.id);
|
|
920
|
+
if (!prev) continue;
|
|
921
|
+
const dx = c.position.x - prev.position.x;
|
|
922
|
+
const dy = c.position.y - prev.position.y;
|
|
923
|
+
if (!dx && !dy) continue;
|
|
924
|
+
for (const m of members.get(c.id)) {
|
|
925
|
+
const mn = byId.get(m);
|
|
926
|
+
if (!mn || moving.has(m)) continue;
|
|
927
|
+
moving.add(m);
|
|
928
|
+
extra.push({ type: "position", id: m, position: { x: mn.position.x + dx, y: mn.position.y + dy } });
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return applyNodeChanges(extra.length ? [...real, ...extra] : real, nds);
|
|
932
|
+
});
|
|
933
|
+
}, []);
|
|
934
|
+
const onEdgesChange = useCallback((changes) => setEdges((es) => applyEdgeChanges(changes.filter((c) => c.type !== "remove"), es)), []);
|
|
935
|
+
|
|
936
|
+
const dragStart = (dragged) => {
|
|
937
|
+
setConnectMenu(null);
|
|
938
|
+
const ids = new Set((dragged ?? []).map((n) => n.id));
|
|
939
|
+
const m = new Map();
|
|
940
|
+
for (const n of dragged ?? []) {
|
|
941
|
+
if (n.type !== "frame") continue;
|
|
942
|
+
const inside = frameContents({ nodes: nodesRef.current }, n.id).filter((id) => !ids.has(id));
|
|
943
|
+
if (inside.length) m.set(n.id, inside);
|
|
944
|
+
}
|
|
945
|
+
frameMembers.current = m;
|
|
946
|
+
};
|
|
947
|
+
const dragStop = (dragged) => {
|
|
948
|
+
const ids = (dragged ?? []).map((n) => n.id);
|
|
949
|
+
for (const list of frameMembers.current.values()) ids.push(...list);
|
|
950
|
+
frameMembers.current = new Map();
|
|
951
|
+
commit("move", ids);
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
const isValidConnection = (c) => {
|
|
955
|
+
if (c.source === c.target) return false;
|
|
956
|
+
const can = propsRef.current.canConnect;
|
|
957
|
+
return can ? can(c, { nodes: nodesRef.current, edges: edgesRef.current }) !== false : true;
|
|
958
|
+
};
|
|
959
|
+
const onConnect = (c) => {
|
|
960
|
+
if (!isValidConnection(c)) return;
|
|
961
|
+
if (edgesRef.current.some((e) => e.source === c.source && e.target === c.target)) return;
|
|
962
|
+
const e = createEdge(c.source, c.target);
|
|
963
|
+
setEdges((es) => [...es, e]);
|
|
964
|
+
commit("connect");
|
|
965
|
+
say("Connected");
|
|
966
|
+
};
|
|
967
|
+
const onConnectEnd = (event, state) => {
|
|
968
|
+
if (readOnly || state?.isValid || state?.toNode || !state?.fromNode) return;
|
|
969
|
+
const pt = "changedTouches" in event && event.changedTouches?.length ? event.changedTouches[0] : event;
|
|
970
|
+
const box = wrapRef.current?.getBoundingClientRect();
|
|
971
|
+
if (!box) return;
|
|
972
|
+
const flow = rf.screenToFlowPosition({ x: pt.clientX, y: pt.clientY });
|
|
973
|
+
setConnectMenu({ at: { x: Math.min(pt.clientX - box.left, box.width - 200), y: Math.min(pt.clientY - box.top, box.height - 240) }, flow, from: state.fromNode.id });
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
const connectItems = useMemo(() => {
|
|
977
|
+
if (!connectMenu) return [];
|
|
978
|
+
const { flow, from } = connectMenu;
|
|
979
|
+
const make = (node) => {
|
|
980
|
+
if (!node) return;
|
|
981
|
+
const list = Array.isArray(node) ? node : [node];
|
|
982
|
+
const first = list[0];
|
|
983
|
+
addNodes(list, {
|
|
984
|
+
edges: first ? [createEdge(from, first.id)] : [],
|
|
985
|
+
edit: list.length === 1 && EDITABLE.has(first?.type),
|
|
986
|
+
});
|
|
987
|
+
setConnectMenu(null);
|
|
988
|
+
};
|
|
989
|
+
const built = [
|
|
990
|
+
{ label: "Sticky note", icon: "sticky-note", onSelect: () => make(createNode("sticky", flow, { grid, data: { color: stickyColor } })) },
|
|
991
|
+
...Object.entries(SHAPE_ICON).map(([k, icon]) => ({ label: SHAPE_NAMES[k], icon, onSelect: () => make(createNode("shape", flow, { grid, data: { shape: k } })) })),
|
|
992
|
+
{ label: "Text", icon: "type", onSelect: () => make(createNode("text", flow, { grid })) },
|
|
993
|
+
];
|
|
994
|
+
const own = customs.filter((t) => t.create && t.connectable !== false).map((t) => ({ label: t.label, icon: t.icon, onSelect: () => make(t.create(flow, { api, grid })) }));
|
|
995
|
+
return [...built, own.length ? { type: "separator" } : null, ...own];
|
|
996
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
997
|
+
}, [connectMenu, customs, stickyColor]);
|
|
998
|
+
|
|
999
|
+
// ── the context menu (right-click, long press, Shift+F10) ───────────────
|
|
1000
|
+
const ctxItems = (() => {
|
|
1001
|
+
const m = modKey();
|
|
1002
|
+
if (readOnly) {
|
|
1003
|
+
return [
|
|
1004
|
+
{ label: "Fit to screen", icon: "maximize", shortcut: "⇧1", onSelect: () => fitView() },
|
|
1005
|
+
{ label: "List view", icon: "list", onSelect: () => setListView(true) },
|
|
1006
|
+
];
|
|
1007
|
+
}
|
|
1008
|
+
const sel = selectedNodes;
|
|
1009
|
+
if (ctxOn === "edge" && selectedEdges.length) {
|
|
1010
|
+
const ids = selectedEdges.map((x) => x.id);
|
|
1011
|
+
return [
|
|
1012
|
+
ids.length === 1 && { label: selectedEdges[0].label ? "Edit label" : "Add label", icon: "pencil", shortcut: "↵", onSelect: () => api.editEdge(ids[0]) },
|
|
1013
|
+
{ label: "Delete", icon: "trash", tone: "danger", shortcut: "⌫", onSelect: () => removeEdges(ids) },
|
|
1014
|
+
];
|
|
1015
|
+
}
|
|
1016
|
+
if (ctxOn === "selection" && sel.length) {
|
|
1017
|
+
const ids = sel.map((n) => n.id);
|
|
1018
|
+
const anyLocked = sel.some((n) => n.data?.locked);
|
|
1019
|
+
const single = sel.length === 1 ? sel[0] : null;
|
|
1020
|
+
const extra = typeof contextMenuItems === "function" ? contextMenuItems({ nodes: sel, edges: selectedEdges }, api) : null;
|
|
1021
|
+
return [
|
|
1022
|
+
single && !anyLocked && { label: "Edit", icon: "pencil", shortcut: "↵", onSelect: () => openNode(single) },
|
|
1023
|
+
{ label: "Duplicate", icon: "copy", shortcut: `${m}D`, onSelect: () => duplicate(ids) },
|
|
1024
|
+
!sel.every((n) => n.type === "frame") && { label: "Frame selection", icon: "frame", onSelect: () => frameSelection(ids) },
|
|
1025
|
+
{ type: "separator" },
|
|
1026
|
+
{ label: "Bring to front", icon: "bring-front", shortcut: `${m}]`, onSelect: () => api.arrange(ids, "front") },
|
|
1027
|
+
{ label: "Send to back", icon: "send-back", shortcut: `${m}[`, onSelect: () => api.arrange(ids, "back") },
|
|
1028
|
+
sel.length > 1 && {
|
|
1029
|
+
label: "Align",
|
|
1030
|
+
icon: "align-center",
|
|
1031
|
+
items: [
|
|
1032
|
+
{ label: "Left", onSelect: () => api.align(ids, "left") },
|
|
1033
|
+
{ label: "Centre", onSelect: () => api.align(ids, "center") },
|
|
1034
|
+
{ label: "Right", onSelect: () => api.align(ids, "right") },
|
|
1035
|
+
{ type: "separator" },
|
|
1036
|
+
{ label: "Top", onSelect: () => api.align(ids, "top") },
|
|
1037
|
+
{ label: "Middle", onSelect: () => api.align(ids, "middle") },
|
|
1038
|
+
{ label: "Bottom", onSelect: () => api.align(ids, "bottom") },
|
|
1039
|
+
sel.length > 2 && { type: "separator" },
|
|
1040
|
+
sel.length > 2 && { label: "Distribute horizontally", onSelect: () => api.distribute(ids, "horizontal") },
|
|
1041
|
+
sel.length > 2 && { label: "Distribute vertically", onSelect: () => api.distribute(ids, "vertical") },
|
|
1042
|
+
{ type: "separator" },
|
|
1043
|
+
{ label: "Tidy up", onSelect: () => api.autoLayout({ ids }) },
|
|
1044
|
+
],
|
|
1045
|
+
},
|
|
1046
|
+
{ label: anyLocked ? "Unlock" : "Lock", icon: anyLocked ? "lock-open" : "lock", shortcut: `⇧${m}L`, onSelect: () => lock(ids, !anyLocked) },
|
|
1047
|
+
...(extra?.length ? [{ type: "separator" }, ...extra] : []),
|
|
1048
|
+
{ type: "separator" },
|
|
1049
|
+
{ label: "Delete", icon: "trash", tone: "danger", shortcut: "⌫", disabled: sel.every((n) => n.data?.locked || n.deletable === false), onSelect: deleteSelection },
|
|
1050
|
+
];
|
|
1051
|
+
}
|
|
1052
|
+
const at = () => ctxPoint.current ?? centerPoint();
|
|
1053
|
+
const extra = typeof contextMenuItems === "function" ? contextMenuItems({ nodes: [], edges: [] }, api) : null;
|
|
1054
|
+
return [
|
|
1055
|
+
{ label: "Add sticky here", icon: "sticky-note", onSelect: () => addNodes(createNode("sticky", at(), { grid, data: { color: stickyColor } }), { edit: true }) },
|
|
1056
|
+
{ label: "Add shape here", icon: "square", onSelect: () => addNodes(createNode("shape", at(), { grid, data: { shape: shapeKind } }), { edit: true }) },
|
|
1057
|
+
{ label: "Add text here", icon: "type", onSelect: () => addNodes(createNode("text", at(), { grid }), { edit: true }) },
|
|
1058
|
+
{ label: "Add frame here", icon: "frame", onSelect: () => addNodes(createNode("frame", at(), { grid }), { edit: true }) },
|
|
1059
|
+
{ type: "separator" },
|
|
1060
|
+
clip.current && { label: "Paste here", icon: "copy", shortcut: `${m}V`, onSelect: () => pasteBoard(clip.current, at()) },
|
|
1061
|
+
nodes.length > 0 && { label: "Select all", shortcut: `${m}A`, onSelect: () => selectOnly(nodesRef.current.map((n) => n.id)) },
|
|
1062
|
+
nodes.length > 1 && { label: "Tidy up everything", icon: "sparkles", onSelect: () => api.autoLayout() },
|
|
1063
|
+
...(extra?.length ? [{ type: "separator" }, ...extra] : []),
|
|
1064
|
+
{ type: "separator" },
|
|
1065
|
+
{ label: "Fit to screen", icon: "maximize", shortcut: "⇧1", onSelect: () => fitView() },
|
|
1066
|
+
{ type: "checkbox", label: "Snap to grid", checked: snap, onCheckedChange: setSnap },
|
|
1067
|
+
{ label: "Keyboard shortcuts", icon: "keyboard", shortcut: "?", onSelect: () => setSheet(true) },
|
|
1068
|
+
];
|
|
1069
|
+
})();
|
|
1070
|
+
|
|
1071
|
+
// ── node and edge types, context ───────────────────────────────────────
|
|
1072
|
+
const nodeTypes = useMemo(() => ({ ...BUILT_IN_NODES, ...(ownNodeTypes ?? {}) }), [ownNodeTypes]);
|
|
1073
|
+
const soleEdge = selectedEdges.length === 1 && !selectedNodes.length ? selectedEdges[0].id : null;
|
|
1074
|
+
const ctx = useMemo(
|
|
1075
|
+
() => ({ api, tool, editingNode: editing.node, editingEdge: editing.edge, soleEdge, readOnly, palette, renderCard, connecting, grid }),
|
|
1076
|
+
[api, tool, editing.node, editing.edge, soleEdge, readOnly, palette, renderCard, connecting, grid],
|
|
1077
|
+
);
|
|
1078
|
+
|
|
1079
|
+
// React Flow's event props, one stable function each: a new function per
|
|
1080
|
+
// render would redraw every node on every render (a pan, a keystroke).
|
|
1081
|
+
const live = useRef({});
|
|
1082
|
+
live.current = {
|
|
1083
|
+
onConnect,
|
|
1084
|
+
onConnectEnd,
|
|
1085
|
+
isValidConnection,
|
|
1086
|
+
onNodeDragStart: (_, __, dragged) => dragStart(dragged),
|
|
1087
|
+
onNodeDragStop: (_, __, dragged) => dragStop(dragged),
|
|
1088
|
+
onSelectionDragStart: (_, dragged) => dragStart(dragged),
|
|
1089
|
+
onSelectionDragStop: (_, dragged) => dragStop(dragged),
|
|
1090
|
+
onNodeDoubleClick: (e, n) => {
|
|
1091
|
+
e.stopPropagation();
|
|
1092
|
+
openNode(n);
|
|
1093
|
+
},
|
|
1094
|
+
onEdgeDoubleClick: (e, ed) => {
|
|
1095
|
+
e.stopPropagation();
|
|
1096
|
+
if (!readOnly) setEditing({ node: null, edge: ed.id });
|
|
1097
|
+
},
|
|
1098
|
+
onPaneClick,
|
|
1099
|
+
onPaneContextMenu: (e) => {
|
|
1100
|
+
ctxPoint.current = rf.screenToFlowPosition({ x: e.clientX, y: e.clientY });
|
|
1101
|
+
setCtxOn("pane");
|
|
1102
|
+
},
|
|
1103
|
+
onNodeContextMenu: (_, n) => {
|
|
1104
|
+
if (!n.selected) selectOnly([n.id]);
|
|
1105
|
+
setCtxOn("selection");
|
|
1106
|
+
},
|
|
1107
|
+
onSelectionContextMenu: () => setCtxOn("selection"),
|
|
1108
|
+
onEdgeContextMenu: (_, ed) => {
|
|
1109
|
+
if (!ed.selected) selectOnly([], [ed.id]);
|
|
1110
|
+
setCtxOn("edge");
|
|
1111
|
+
},
|
|
1112
|
+
onSelectionChange: ({ nodes: sn, edges: se }) => propsRef.current.onSelectionChange?.(sn, se),
|
|
1113
|
+
onMoveEnd: (_, vp) => {
|
|
1114
|
+
viewportRef.current = vp;
|
|
1115
|
+
},
|
|
1116
|
+
};
|
|
1117
|
+
const handlers = useMemo(() => Object.fromEntries(Object.keys(live.current).map((k) => [k, (...a) => live.current[k](...a)])), []);
|
|
1118
|
+
|
|
1119
|
+
const dragging = nodes.some((n) => n.dragging) || marquee;
|
|
1120
|
+
const snapGrid = useMemo(() => [grid, grid], [grid]);
|
|
1121
|
+
const proOptions = useMemo(() => ({ hideAttribution }), [hideAttribution]);
|
|
1122
|
+
const showToolbar = !readOnly && tool === "select" && selectedNodes.length > 0 && !dragging && !editing.node && !connectMenu && !listView;
|
|
1123
|
+
const templates = templatesProp ?? starterTemplates();
|
|
1124
|
+
|
|
1125
|
+
// Where the floating toolbars hang, in board coordinates (Floating turns
|
|
1126
|
+
// them into screen ones as the view moves).
|
|
1127
|
+
const selBounds = showToolbar ? boardBounds(selectedNodes) : null;
|
|
1128
|
+
const selBox = selBounds
|
|
1129
|
+
? {
|
|
1130
|
+
x: selBounds.x + selBounds.width / 2,
|
|
1131
|
+
top: selBounds.y,
|
|
1132
|
+
bottom: selBounds.y + selBounds.height,
|
|
1133
|
+
// A frame's title sits above its box: clear it.
|
|
1134
|
+
lift: selectedNodes.some((n) => n.type === "frame") ? 34 : 14,
|
|
1135
|
+
}
|
|
1136
|
+
: null;
|
|
1137
|
+
const soleEdgeObj = !readOnly && tool === "select" && soleEdge && !editing.edge && !connectMenu && !listView ? selectedEdges[0] : null;
|
|
1138
|
+
const edgeBar = (() => {
|
|
1139
|
+
if (!soleEdgeObj) return null;
|
|
1140
|
+
const s = nodes.find((n) => n.id === soleEdgeObj.source);
|
|
1141
|
+
const t = nodes.find((n) => n.id === soleEdgeObj.target);
|
|
1142
|
+
if (!s || !t) return null;
|
|
1143
|
+
const ra = nodeRect(s);
|
|
1144
|
+
const rb = nodeRect(t);
|
|
1145
|
+
const sides = facingSides(ra, rb);
|
|
1146
|
+
const g = connectorGeometry(sidePoint(ra, sides.source), sides.source, sidePoint(rb, sides.target), sides.target, soleEdgeObj.data?.line ?? "curved");
|
|
1147
|
+
return { at: { x: g.mid.x, top: g.mid.y, bottom: g.mid.y, lift: 26 }, edge: soleEdgeObj };
|
|
1148
|
+
})();
|
|
1149
|
+
const isEmpty = nodes.length === 0;
|
|
1150
|
+
const board = listView ? cleanBoard({ nodes, edges }, { viewport: false }) : null;
|
|
1151
|
+
|
|
1152
|
+
return (
|
|
1153
|
+
<CanvasContext.Provider value={ctx}>
|
|
1154
|
+
<div
|
|
1155
|
+
ref={wrapRef}
|
|
1156
|
+
role="region"
|
|
1157
|
+
aria-label={label}
|
|
1158
|
+
aria-roledescription="board"
|
|
1159
|
+
tabIndex={-1}
|
|
1160
|
+
data-tool={cursorTool}
|
|
1161
|
+
data-connecting={connecting ? "true" : "false"}
|
|
1162
|
+
className={cx("relative isolate overflow-hidden bg-background text-foreground outline-none", !bare && "rounded-lg border border-border", THEME, className)}
|
|
1163
|
+
style={{ height }}
|
|
1164
|
+
onKeyDownCapture={onKeyDownCapture}
|
|
1165
|
+
onFocusCapture={onFocusCapture}
|
|
1166
|
+
onPointerDownCapture={onPointerDownCapture}
|
|
1167
|
+
onPointerMove={(e) => {
|
|
1168
|
+
if (e.pointerType === "mouse") pointer.current = rf.screenToFlowPosition({ x: e.clientX, y: e.clientY });
|
|
1169
|
+
}}
|
|
1170
|
+
onPointerLeave={() => {
|
|
1171
|
+
pointer.current = null;
|
|
1172
|
+
}}
|
|
1173
|
+
onDoubleClick={onDoubleClick}
|
|
1174
|
+
>
|
|
1175
|
+
<p className="sr-only" aria-live="polite">
|
|
1176
|
+
{announce}
|
|
1177
|
+
</p>
|
|
1178
|
+
<ContextMenu items={ctxItems} label={`${label} actions`} className="absolute inset-0">
|
|
1179
|
+
<ReactFlow
|
|
1180
|
+
nodes={nodes}
|
|
1181
|
+
edges={edges}
|
|
1182
|
+
nodeTypes={nodeTypes}
|
|
1183
|
+
edgeTypes={BUILT_IN_EDGES}
|
|
1184
|
+
defaultEdgeOptions={EDGE_DEFAULTS}
|
|
1185
|
+
onNodesChange={onNodesChange}
|
|
1186
|
+
onEdgesChange={onEdgesChange}
|
|
1187
|
+
{...handlers}
|
|
1188
|
+
connectionMode={ConnectionMode.Loose}
|
|
1189
|
+
connectionRadius={28}
|
|
1190
|
+
snapToGrid={snap}
|
|
1191
|
+
snapGrid={snapGrid}
|
|
1192
|
+
panOnScroll
|
|
1193
|
+
zoomOnScroll={false}
|
|
1194
|
+
zoomOnPinch
|
|
1195
|
+
zoomOnDoubleClick={false}
|
|
1196
|
+
panOnDrag={tool === "hand" ? true : MIDDLE_BUTTON}
|
|
1197
|
+
selectionOnDrag={tool === "select" && !readOnly}
|
|
1198
|
+
selectionMode={SelectionMode.Full}
|
|
1199
|
+
multiSelectionKeyCode={MULTI_KEYS}
|
|
1200
|
+
deleteKeyCode={null}
|
|
1201
|
+
nodesDraggable={!readOnly && (tool === "select" || placing)}
|
|
1202
|
+
nodesConnectable={!readOnly}
|
|
1203
|
+
elementsSelectable={!readOnly && tool !== "hand"}
|
|
1204
|
+
elevateNodesOnSelect={false}
|
|
1205
|
+
elevateEdgesOnSelect
|
|
1206
|
+
minZoom={0.1}
|
|
1207
|
+
maxZoom={4}
|
|
1208
|
+
fitView={!viewportRef.current}
|
|
1209
|
+
fitViewOptions={FIT_OPTIONS}
|
|
1210
|
+
defaultViewport={viewportRef.current ?? undefined}
|
|
1211
|
+
proOptions={proOptions}
|
|
1212
|
+
attributionPosition={narrow ? "top-left" : "bottom-center"}
|
|
1213
|
+
colorMode="light"
|
|
1214
|
+
aria-label={label}
|
|
1215
|
+
>
|
|
1216
|
+
<Background variant={BackgroundVariant.Dots} gap={grid} size={1.6} />
|
|
1217
|
+
{minimap && !narrow ? <MiniMap pannable zoomable ariaLabel="Minimap" position="bottom-right" nodeBorderRadius={6} className="bottom-14!" /> : null}
|
|
1218
|
+
{draft ? (
|
|
1219
|
+
<ViewportPortal>
|
|
1220
|
+
<div
|
|
1221
|
+
aria-hidden="true"
|
|
1222
|
+
className="absolute top-0 left-0 rounded-md border-2 border-dashed border-primary bg-primary/5"
|
|
1223
|
+
style={{ transform: `translate(${draft.x}px, ${draft.y}px)`, width: draft.width, height: draft.height }}
|
|
1224
|
+
/>
|
|
1225
|
+
</ViewportPortal>
|
|
1226
|
+
) : null}
|
|
1227
|
+
</ReactFlow>
|
|
1228
|
+
</ContextMenu>
|
|
1229
|
+
|
|
1230
|
+
{isEmpty && !readOnly && emptyState !== false && !listView && tool === "select" ? (
|
|
1231
|
+
emptyState ? (
|
|
1232
|
+
<div className="pointer-events-none absolute inset-0 grid place-items-center p-6">
|
|
1233
|
+
<div className="fc-chrome pointer-events-auto">{emptyState}</div>
|
|
1234
|
+
</div>
|
|
1235
|
+
) : (
|
|
1236
|
+
<EmptyState
|
|
1237
|
+
templates={templates}
|
|
1238
|
+
narrow={narrow}
|
|
1239
|
+
onTemplate={(t) => {
|
|
1240
|
+
const b = typeof t.board === "function" ? t.board() : t.board;
|
|
1241
|
+
api.setBoard(b);
|
|
1242
|
+
say(`${t.label} template added`);
|
|
1243
|
+
}}
|
|
1244
|
+
/>
|
|
1245
|
+
)
|
|
1246
|
+
) : null}
|
|
1247
|
+
|
|
1248
|
+
{showToolbar && selBox ? (
|
|
1249
|
+
<Floating at={selBox}>
|
|
1250
|
+
<SelectionToolbar nodes={selectedNodes} api={api} palette={palette} extra={toolbarExtra} />
|
|
1251
|
+
</Floating>
|
|
1252
|
+
) : null}
|
|
1253
|
+
{edgeBar ? (
|
|
1254
|
+
<Floating at={edgeBar.at}>
|
|
1255
|
+
<EdgeToolbar id={edgeBar.edge.id} data={edgeBar.edge.data} label={edgeBar.edge.label} />
|
|
1256
|
+
</Floating>
|
|
1257
|
+
) : null}
|
|
1258
|
+
|
|
1259
|
+
{topBar ? <div className="pointer-events-none absolute inset-x-3 top-3 z-10 flex justify-center [&>*]:pointer-events-auto">{topBar}</div> : null}
|
|
1260
|
+
|
|
1261
|
+
{!readOnly ? (
|
|
1262
|
+
<div className={cx("pointer-events-none absolute z-10 flex gap-2", narrow ? "inset-x-3 bottom-3 flex-col-reverse items-center" : "inset-y-3 left-3 flex-row items-center")}>
|
|
1263
|
+
<ToolRail items={railItems} tool={tool} onPick={pickTool} narrow={narrow} />
|
|
1264
|
+
<ToolFlyout tool={tool} palette={palette} stickyColor={stickyColor} onStickyColor={setStickyColor} shapeKind={shapeKind} onShapeKind={setShapeKind} />
|
|
1265
|
+
</div>
|
|
1266
|
+
) : null}
|
|
1267
|
+
|
|
1268
|
+
{!readOnly ? (
|
|
1269
|
+
<div className={cx("absolute left-3 z-10", narrow ? (tool === "sticky" || tool === "shape" ? "bottom-32" : "bottom-20") : "bottom-3")}>
|
|
1270
|
+
<HistoryBar canUndo={history.past.length > 0} canRedo={history.future.length > 0} onUndo={undo} onRedo={redo} save={save} onRetry={() => runSave(currentBoard())} />
|
|
1271
|
+
</div>
|
|
1272
|
+
) : null}
|
|
1273
|
+
|
|
1274
|
+
<div className={cx("absolute right-3 z-10", narrow ? (topBar ? "top-16" : "top-3") : "bottom-3")}>
|
|
1275
|
+
<ZoomBar
|
|
1276
|
+
zoom={<ZoomLevel />}
|
|
1277
|
+
narrow={narrow}
|
|
1278
|
+
presentation={presentation}
|
|
1279
|
+
onZoomIn={() => rf.zoomIn({ duration })}
|
|
1280
|
+
onZoomOut={() => rf.zoomOut({ duration })}
|
|
1281
|
+
onZoomTo={(z) => rf.zoomTo(z, { duration })}
|
|
1282
|
+
onFit={() => fitView()}
|
|
1283
|
+
onFitSelection={() => fitView({ nodes: selectedNodes.map((n) => ({ id: n.id })), maxZoom: 2 })}
|
|
1284
|
+
hasSelection={selectedNodes.length > 0}
|
|
1285
|
+
minimap={minimap}
|
|
1286
|
+
onMinimap={setMinimap}
|
|
1287
|
+
list={listView}
|
|
1288
|
+
onList={setListView}
|
|
1289
|
+
snap={snap}
|
|
1290
|
+
onSnap={setSnap}
|
|
1291
|
+
onShortcuts={() => setSheet(true)}
|
|
1292
|
+
/>
|
|
1293
|
+
</div>
|
|
1294
|
+
|
|
1295
|
+
{presentation ? (
|
|
1296
|
+
<div className="pointer-events-none absolute inset-x-3 bottom-3 z-10 flex justify-center">
|
|
1297
|
+
<PresentBar
|
|
1298
|
+
index={slide}
|
|
1299
|
+
count={frames.length}
|
|
1300
|
+
title={frames[slide]?.data?.title || "Untitled frame"}
|
|
1301
|
+
onPrev={() => goSlide(slide - 1)}
|
|
1302
|
+
onNext={() => goSlide(slide + 1)}
|
|
1303
|
+
onExit={onExitPresentation}
|
|
1304
|
+
/>
|
|
1305
|
+
</div>
|
|
1306
|
+
) : null}
|
|
1307
|
+
|
|
1308
|
+
{connectMenu ? <ConnectMenu at={connectMenu.at} items={connectItems} onClose={() => setConnectMenu(null)} /> : null}
|
|
1309
|
+
|
|
1310
|
+
{listView ? (
|
|
1311
|
+
<ListView
|
|
1312
|
+
board={board}
|
|
1313
|
+
label={label}
|
|
1314
|
+
describe={describeNode}
|
|
1315
|
+
iconOf={(n) => toolById.get(n.type)?.icon}
|
|
1316
|
+
onClose={() => setListView(false)}
|
|
1317
|
+
onPick={(id) => {
|
|
1318
|
+
setListView(false);
|
|
1319
|
+
requestAnimationFrame(() => focusNode(id));
|
|
1320
|
+
}}
|
|
1321
|
+
/>
|
|
1322
|
+
) : null}
|
|
1323
|
+
|
|
1324
|
+
<ShortcutsSheet open={sheet} onClose={() => setSheet(false)} tools={railItems} />
|
|
1325
|
+
</div>
|
|
1326
|
+
</CanvasContext.Provider>
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
|