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,1125 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// MindMap — a mind map you can drive from the keyboard alone, on
|
|
4
|
+
// FlowCanvas. The centre topic, main branches balanced on both sides, each
|
|
5
|
+
// branch a tidy tree in its own chart hue. Tab adds a subtopic, Enter the
|
|
6
|
+
// next topic, typing renames, arrows walk the map, ⌘↑/⌘↓ fold; drag a
|
|
7
|
+
// topic onto another to move its branch there.
|
|
8
|
+
//
|
|
9
|
+
// The map is a tree of topics (tree.js), and this component owns it: every
|
|
10
|
+
// change makes a new tree, pushes the old one on the undo stack, lays the
|
|
11
|
+
// tree out again and hands the canvas the result as its `value`. The
|
|
12
|
+
// canvas runs read-only (its own tools, stickies and undo never see this
|
|
13
|
+
// map); pointer and keys come through this component instead, and the
|
|
14
|
+
// canvas pans, zooms, fits and draws.
|
|
15
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
16
|
+
import { Button, Icon, IconButton, Kbd, Menu, Popover, Separator, Sheet, ToggleGroup, Tooltip, cx, toast } from "../../ui";
|
|
17
|
+
import { ColorDot, FlowCanvas, Palette, useFlowCanvas } from "../flow-canvas";
|
|
18
|
+
import { MindContext, NODE_TYPES } from "./topic";
|
|
19
|
+
import {
|
|
20
|
+
BRANCH_COLORS,
|
|
21
|
+
countTopics,
|
|
22
|
+
createTopic,
|
|
23
|
+
duplicateTopic,
|
|
24
|
+
estimateText,
|
|
25
|
+
findTopic,
|
|
26
|
+
indentTopic,
|
|
27
|
+
insertSibling,
|
|
28
|
+
insertTopic,
|
|
29
|
+
isWithin,
|
|
30
|
+
mindMapTemplates,
|
|
31
|
+
mindMapToBoard,
|
|
32
|
+
mindMapToMarkdown,
|
|
33
|
+
mindMapToText,
|
|
34
|
+
moveTopic,
|
|
35
|
+
normalizeMap,
|
|
36
|
+
outdentTopic,
|
|
37
|
+
outlineToTopics,
|
|
38
|
+
parentOf,
|
|
39
|
+
pathTo,
|
|
40
|
+
removeTopic,
|
|
41
|
+
setAllCollapsed,
|
|
42
|
+
shiftTopic,
|
|
43
|
+
updateTopic,
|
|
44
|
+
} from "./tree";
|
|
45
|
+
|
|
46
|
+
const HISTORY = 100;
|
|
47
|
+
const EDITING_TARGET = 'input,textarea,select,[contenteditable="true"],[contenteditable="plaintext-only"],[role="dialog"],[role="menu"],[popover],button,a[href]';
|
|
48
|
+
const EXTRA_COLORS = ["warning", "success", "info", "destructive"];
|
|
49
|
+
const isMac = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent || "");
|
|
50
|
+
|
|
51
|
+
// Measures words in the map's own font (a canvas's measureText), so a
|
|
52
|
+
// topic's box fits its words exactly. Until the page is there (the
|
|
53
|
+
// server), a guess. Measured again once mounted (the font of the element
|
|
54
|
+
// the map sits in, not the body's) and when web fonts finish loading.
|
|
55
|
+
function useMeasure(hostRef) {
|
|
56
|
+
const [tick, setTick] = useState(0);
|
|
57
|
+
const r = useRef({ ctx: null, family: null, cache: new Map() });
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
let live = true;
|
|
60
|
+
const again = () => {
|
|
61
|
+
if (!live) return;
|
|
62
|
+
r.current.cache.clear();
|
|
63
|
+
r.current.family = null;
|
|
64
|
+
setTick((t) => t + 1);
|
|
65
|
+
};
|
|
66
|
+
again();
|
|
67
|
+
document.fonts?.ready?.then(again);
|
|
68
|
+
document.fonts?.addEventListener?.("loadingdone", again);
|
|
69
|
+
return () => {
|
|
70
|
+
live = false;
|
|
71
|
+
document.fonts?.removeEventListener?.("loadingdone", again);
|
|
72
|
+
};
|
|
73
|
+
}, []);
|
|
74
|
+
return useCallback(
|
|
75
|
+
(text, style) => {
|
|
76
|
+
if (typeof document === "undefined") return estimateText(text, style);
|
|
77
|
+
const c = r.current;
|
|
78
|
+
if (!c.ctx) c.ctx = document.createElement("canvas").getContext("2d");
|
|
79
|
+
if (!c.ctx) return estimateText(text, style);
|
|
80
|
+
if (!c.family) {
|
|
81
|
+
const cs = getComputedStyle(hostRef.current ?? document.body);
|
|
82
|
+
c.family = cs.fontFamily || "system-ui, sans-serif";
|
|
83
|
+
// A theme's tracking widens the words too (canvas text has it in
|
|
84
|
+
// current browsers; elsewhere the +2px slack covers a little).
|
|
85
|
+
if ("letterSpacing" in c.ctx) c.ctx.letterSpacing = cs.letterSpacing === "normal" ? "0px" : cs.letterSpacing;
|
|
86
|
+
}
|
|
87
|
+
const key = `${style.weight}|${style.font}|${text}`;
|
|
88
|
+
let w = c.cache.get(key);
|
|
89
|
+
if (w === undefined) {
|
|
90
|
+
c.ctx.font = `${style.weight} ${style.font}px ${c.family}`;
|
|
91
|
+
w = c.ctx.measureText(text).width;
|
|
92
|
+
if (c.cache.size > 4000) c.cache.clear();
|
|
93
|
+
c.cache.set(key, w);
|
|
94
|
+
}
|
|
95
|
+
return w;
|
|
96
|
+
},
|
|
97
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
98
|
+
[tick],
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function useCoarsePointer() {
|
|
103
|
+
const [coarse, setCoarse] = useState(false);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
const mq = window.matchMedia("(pointer: coarse)");
|
|
106
|
+
const on = () => setCoarse(mq.matches);
|
|
107
|
+
on();
|
|
108
|
+
mq.addEventListener("change", on);
|
|
109
|
+
return () => mq.removeEventListener("change", on);
|
|
110
|
+
}, []);
|
|
111
|
+
return coarse;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function settle(fn, ...args) {
|
|
115
|
+
try {
|
|
116
|
+
const res = await fn(...args);
|
|
117
|
+
return res && res.ok === false ? { state: "error", error: res.error || "Couldn't save" } : { state: "saved" };
|
|
118
|
+
} catch (e) {
|
|
119
|
+
return { state: "error", error: e?.message || "Couldn't save" };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function MindMap({
|
|
124
|
+
value,
|
|
125
|
+
defaultValue,
|
|
126
|
+
onChange,
|
|
127
|
+
onSave,
|
|
128
|
+
changeDelay = 400,
|
|
129
|
+
readOnly = false,
|
|
130
|
+
colors = BRANCH_COLORS,
|
|
131
|
+
templates,
|
|
132
|
+
placeholder = "New topic",
|
|
133
|
+
height = 600,
|
|
134
|
+
label = "Mind map",
|
|
135
|
+
onSelectionChange,
|
|
136
|
+
className,
|
|
137
|
+
}) {
|
|
138
|
+
const boxRef = useRef(null);
|
|
139
|
+
const measure = useMeasure(boxRef);
|
|
140
|
+
const coarse = useCoarsePointer();
|
|
141
|
+
const canvas = useFlowCanvas();
|
|
142
|
+
const cbs = useRef({});
|
|
143
|
+
cbs.current = { onChange, onSave, onSelectionChange, changeDelay, readOnly };
|
|
144
|
+
|
|
145
|
+
// ── the tree and its history ─────────────────────────────────────────
|
|
146
|
+
const [tree, setTreeState] = useState(() => normalizeMap(value ?? defaultValue));
|
|
147
|
+
const treeRef = useRef(tree);
|
|
148
|
+
const setTree = (next) => {
|
|
149
|
+
treeRef.current = next;
|
|
150
|
+
setTreeState(next);
|
|
151
|
+
};
|
|
152
|
+
const [hist, setHistState] = useState({ past: [], future: [] });
|
|
153
|
+
const histRef = useRef(hist);
|
|
154
|
+
const setHist = (h) => {
|
|
155
|
+
histRef.current = h;
|
|
156
|
+
setHistState(h);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const [selectedId, setSelectedState] = useState(null);
|
|
160
|
+
const selectedRef = useRef(null);
|
|
161
|
+
const [editing, setEditingState] = useState(null); // { id, isNew, before, prevText }
|
|
162
|
+
const editingRef = useRef(null);
|
|
163
|
+
const setEditing = (e) => {
|
|
164
|
+
editingRef.current = e;
|
|
165
|
+
setEditingState(e);
|
|
166
|
+
};
|
|
167
|
+
const select = useCallback((id) => {
|
|
168
|
+
selectedRef.current = id;
|
|
169
|
+
setSelectedState(id);
|
|
170
|
+
}, []);
|
|
171
|
+
|
|
172
|
+
// ── saving ───────────────────────────────────────────────────────────
|
|
173
|
+
const [save, setSave] = useState({ state: "idle" });
|
|
174
|
+
const timer = useRef(0);
|
|
175
|
+
const dirty = useRef(false);
|
|
176
|
+
const seq = useRef(0);
|
|
177
|
+
const lastEmitted = useRef(null);
|
|
178
|
+
const runSave = async (t) => {
|
|
179
|
+
const fn = cbs.current.onSave;
|
|
180
|
+
if (!fn) return;
|
|
181
|
+
const n = ++seq.current;
|
|
182
|
+
setSave({ state: "saving" });
|
|
183
|
+
const res = await settle(fn, t);
|
|
184
|
+
if (n === seq.current) setSave(res);
|
|
185
|
+
};
|
|
186
|
+
const emitNow = () => {
|
|
187
|
+
clearTimeout(timer.current);
|
|
188
|
+
if (!dirty.current) return;
|
|
189
|
+
dirty.current = false;
|
|
190
|
+
const t = treeRef.current;
|
|
191
|
+
lastEmitted.current = t;
|
|
192
|
+
cbs.current.onChange?.(t);
|
|
193
|
+
runSave(t);
|
|
194
|
+
};
|
|
195
|
+
const scheduleEmit = () => {
|
|
196
|
+
if (cbs.current.readOnly) return;
|
|
197
|
+
dirty.current = true;
|
|
198
|
+
clearTimeout(timer.current);
|
|
199
|
+
timer.current = setTimeout(emitNow, cbs.current.changeDelay ?? 400);
|
|
200
|
+
};
|
|
201
|
+
useEffect(
|
|
202
|
+
() => () => {
|
|
203
|
+
if (dirty.current) emitNow();
|
|
204
|
+
},
|
|
205
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
206
|
+
[],
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// A change the person made: the old tree goes on the undo stack.
|
|
210
|
+
const commit = (next, { push = true } = {}) => {
|
|
211
|
+
const cur = treeRef.current;
|
|
212
|
+
if (next === cur) return false;
|
|
213
|
+
if (push) setHist({ past: [...histRef.current.past, cur].slice(-HISTORY), future: [] });
|
|
214
|
+
setTree(next);
|
|
215
|
+
scheduleEmit();
|
|
216
|
+
return true;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// Controlled: a `value` that is not our own echo replaces the map (and
|
|
220
|
+
// can be undone).
|
|
221
|
+
const lastProp = useRef(value);
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
if (value === undefined || value === lastProp.current) return;
|
|
224
|
+
lastProp.current = value;
|
|
225
|
+
if (value === lastEmitted.current) return;
|
|
226
|
+
const next = normalizeMap(value);
|
|
227
|
+
setHist({ past: [...histRef.current.past, treeRef.current].slice(-HISTORY), future: [] });
|
|
228
|
+
setTree(next);
|
|
229
|
+
if (selectedRef.current && !findTopic(next, selectedRef.current)) select(null);
|
|
230
|
+
if (editingRef.current) setEditing(null);
|
|
231
|
+
}, [value, select]);
|
|
232
|
+
|
|
233
|
+
// ── what the canvas draws ────────────────────────────────────────────
|
|
234
|
+
const board = useMemo(() => mindMapToBoard(tree, { measure, colors }), [tree, measure, colors]);
|
|
235
|
+
const sizes = useMemo(() => new Map(board.nodes.map((n) => [n.id, n])), [board]);
|
|
236
|
+
|
|
237
|
+
// Fit the map in view once its words are measured. The canvas fits only
|
|
238
|
+
// on its own first mount, before the topics are sized in the map's font
|
|
239
|
+
// (and again when web fonts land), so without this the map opened off to
|
|
240
|
+
// the top-left of an empty-looking canvas (2026-09-24).
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
// Two frames after a short settle: the canvas syncs the new sizes from
|
|
243
|
+
// `value` in an effect of its own, and fits against what it holds.
|
|
244
|
+
let raf = 0;
|
|
245
|
+
const t = setTimeout(() => {
|
|
246
|
+
raf = requestAnimationFrame(() => (raf = requestAnimationFrame(() => canvas.current?.fitView({ duration: 0 }))));
|
|
247
|
+
}, 300);
|
|
248
|
+
return () => (clearTimeout(t), cancelAnimationFrame(raf));
|
|
249
|
+
}, [measure]);
|
|
250
|
+
|
|
251
|
+
// The canvas pans with a plain drag (its hand tool): a mind map has
|
|
252
|
+
// nothing to lasso.
|
|
253
|
+
const hand = useCallback(() => {
|
|
254
|
+
const api = canvas.current;
|
|
255
|
+
if (api && api.getTool?.() !== "hand") api.setTool("hand");
|
|
256
|
+
}, [canvas]);
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
let raf = 0;
|
|
259
|
+
let tries = 0;
|
|
260
|
+
const wait = () => {
|
|
261
|
+
if (canvas.current) return hand();
|
|
262
|
+
if (tries++ < 120) raf = requestAnimationFrame(wait);
|
|
263
|
+
};
|
|
264
|
+
wait();
|
|
265
|
+
return () => cancelAnimationFrame(raf);
|
|
266
|
+
}, [canvas, hand]);
|
|
267
|
+
|
|
268
|
+
// Keep a topic in view: centre it at the zoom there is, only when it is
|
|
269
|
+
// (partly) off screen.
|
|
270
|
+
const ensureVisible = useCallback(
|
|
271
|
+
(id, tries = 3) => {
|
|
272
|
+
const host = boxRef.current;
|
|
273
|
+
const el = host?.querySelector(`[data-topic="${CSS.escape(id)}"]`);
|
|
274
|
+
if (!el) {
|
|
275
|
+
if (tries > 0) setTimeout(() => ensureVisible(id, tries - 1), 60);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const r = el.getBoundingClientRect();
|
|
279
|
+
const c = host.getBoundingClientRect();
|
|
280
|
+
const pad = 32;
|
|
281
|
+
if (r.left >= c.left + pad && r.right <= c.right - pad && r.top >= c.top + pad + 8 && r.bottom <= c.bottom - pad - 40) return;
|
|
282
|
+
const w = sizes.get(id)?.width;
|
|
283
|
+
const zoom = w ? r.width / w : 1;
|
|
284
|
+
canvas.current?.fitView({ nodes: [{ id }], minZoom: zoom, maxZoom: zoom, padding: 0.5 });
|
|
285
|
+
},
|
|
286
|
+
[canvas, sizes],
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
// ── editing words ────────────────────────────────────────────────────
|
|
290
|
+
const setText = useCallback((id, text) => {
|
|
291
|
+
const next = updateTopic(treeRef.current, id, { text });
|
|
292
|
+
setTree(next);
|
|
293
|
+
scheduleEmit();
|
|
294
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
295
|
+
}, []);
|
|
296
|
+
|
|
297
|
+
const startEdit = useCallback(
|
|
298
|
+
(id, { replace, isNew = false, before } = {}) => {
|
|
299
|
+
if (readOnly) return;
|
|
300
|
+
const cur = treeRef.current;
|
|
301
|
+
const t = findTopic(cur, id);
|
|
302
|
+
if (!t) return;
|
|
303
|
+
select(id);
|
|
304
|
+
const ed = { id, isNew, before: before ?? cur, prevText: t.text };
|
|
305
|
+
if (replace !== undefined) setTree(updateTopic(cur, id, { text: replace }));
|
|
306
|
+
setEditing(ed);
|
|
307
|
+
},
|
|
308
|
+
[readOnly, select],
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
/** End the words' edit. Empty: a topic just made goes away (as if it
|
|
312
|
+
* never was), an old one gets its words back. */
|
|
313
|
+
const finishEdit = useCallback((id) => {
|
|
314
|
+
const ed = editingRef.current;
|
|
315
|
+
if (!ed || (id && ed.id !== id)) return;
|
|
316
|
+
setEditing(null);
|
|
317
|
+
const cur = treeRef.current;
|
|
318
|
+
const t = findTopic(cur, ed.id);
|
|
319
|
+
if (!t) return;
|
|
320
|
+
const text = String(t.text ?? "").replace(/\s+$/, "").replace(/^\s+/, "");
|
|
321
|
+
if (!text) {
|
|
322
|
+
if (ed.isNew && ed.id !== cur.id) {
|
|
323
|
+
const prev = neighbourAfterRemove(cur, ed.id);
|
|
324
|
+
setTree(removeTopic(cur, ed.id));
|
|
325
|
+
const past = histRef.current.past;
|
|
326
|
+
if (past[past.length - 1] === ed.before) setHist({ past: past.slice(0, -1), future: histRef.current.future });
|
|
327
|
+
select(prev);
|
|
328
|
+
} else {
|
|
329
|
+
setTree(updateTopic(cur, ed.id, { text: ed.prevText }));
|
|
330
|
+
}
|
|
331
|
+
scheduleEmit();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (text !== t.text) setTree(updateTopic(cur, ed.id, { text }));
|
|
335
|
+
if (!ed.isNew && text !== ed.prevText) setHist({ past: [...histRef.current.past, ed.before].slice(-HISTORY), future: [] });
|
|
336
|
+
scheduleEmit();
|
|
337
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
338
|
+
}, []);
|
|
339
|
+
|
|
340
|
+
// ── changing the map ─────────────────────────────────────────────────
|
|
341
|
+
const addChild = useCallback(
|
|
342
|
+
(parentId) => {
|
|
343
|
+
if (readOnly) return;
|
|
344
|
+
finishEdit();
|
|
345
|
+
const cur = treeRef.current;
|
|
346
|
+
if (!findTopic(cur, parentId)) return;
|
|
347
|
+
const t = createTopic("");
|
|
348
|
+
commit(insertTopic(cur, parentId, t));
|
|
349
|
+
startEdit(t.id, { isNew: true, before: cur });
|
|
350
|
+
},
|
|
351
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
352
|
+
[readOnly, finishEdit, startEdit],
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
const addSibling = useCallback(
|
|
356
|
+
(id, { before = false } = {}) => {
|
|
357
|
+
if (readOnly) return;
|
|
358
|
+
finishEdit();
|
|
359
|
+
const cur = treeRef.current;
|
|
360
|
+
if (id === cur.id) return addChild(id);
|
|
361
|
+
const t = createTopic("");
|
|
362
|
+
commit(insertSibling(cur, id, t, { before }));
|
|
363
|
+
startEdit(t.id, { isNew: true, before: cur });
|
|
364
|
+
},
|
|
365
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
366
|
+
[readOnly, finishEdit, startEdit, addChild],
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const remove = useCallback(
|
|
370
|
+
(id) => {
|
|
371
|
+
if (readOnly) return;
|
|
372
|
+
const cur = treeRef.current;
|
|
373
|
+
const t = findTopic(cur, id);
|
|
374
|
+
if (!t || id === cur.id) return;
|
|
375
|
+
setEditing(null);
|
|
376
|
+
const next = neighbourAfterRemove(cur, id);
|
|
377
|
+
commit(removeTopic(cur, id));
|
|
378
|
+
select(next);
|
|
379
|
+
const n = countTopics(t);
|
|
380
|
+
toast(`Deleted “${clip(t.text) || "Untitled"}”${n ? ` and ${n} under it` : ""}`, { action: { label: "Undo", onClick: () => undoRef.current?.() } });
|
|
381
|
+
},
|
|
382
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
383
|
+
[readOnly, select],
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
const toggleFold = useCallback(
|
|
387
|
+
(id, want) => {
|
|
388
|
+
const cur = treeRef.current;
|
|
389
|
+
const t = findTopic(cur, id);
|
|
390
|
+
if (!t || !t.children?.length || id === cur.id) return;
|
|
391
|
+
const on = want ?? !t.collapsed;
|
|
392
|
+
if (Boolean(t.collapsed) === on) return;
|
|
393
|
+
if (readOnly) setTree(updateTopic(cur, id, { collapsed: on }));
|
|
394
|
+
else commit(updateTopic(cur, id, { collapsed: on }));
|
|
395
|
+
// A fold under the selection hides it: the selection moves up.
|
|
396
|
+
const sel = selectedRef.current;
|
|
397
|
+
if (on && sel && sel !== id && isWithin(cur, id, sel)) select(id);
|
|
398
|
+
},
|
|
399
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
400
|
+
[readOnly, select],
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
const setColor = (id, color) => {
|
|
404
|
+
const cur = treeRef.current;
|
|
405
|
+
if (!findTopic(cur, id) || id === cur.id) return;
|
|
406
|
+
commit(updateTopic(cur, id, { color }));
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const restructure = (fn, id, { keepEditing = false } = {}) => {
|
|
410
|
+
const cur = treeRef.current;
|
|
411
|
+
const next = fn(cur, id);
|
|
412
|
+
if (next === cur) return;
|
|
413
|
+
if (keepEditing && editingRef.current?.isNew) {
|
|
414
|
+
// A topic still being made: its creation is the undo step.
|
|
415
|
+
setTree(next);
|
|
416
|
+
scheduleEmit();
|
|
417
|
+
} else commit(next);
|
|
418
|
+
select(id);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const undo = () => {
|
|
422
|
+
finishEdit();
|
|
423
|
+
const h = histRef.current;
|
|
424
|
+
if (!h.past.length) return;
|
|
425
|
+
const prev = h.past[h.past.length - 1];
|
|
426
|
+
setHist({ past: h.past.slice(0, -1), future: [treeRef.current, ...h.future].slice(0, HISTORY) });
|
|
427
|
+
setTree(prev);
|
|
428
|
+
if (selectedRef.current && !findTopic(prev, selectedRef.current)) select(null);
|
|
429
|
+
scheduleEmit();
|
|
430
|
+
say("Undone");
|
|
431
|
+
};
|
|
432
|
+
const redo = () => {
|
|
433
|
+
finishEdit();
|
|
434
|
+
const h = histRef.current;
|
|
435
|
+
if (!h.future.length) return;
|
|
436
|
+
const [next, ...future] = h.future;
|
|
437
|
+
setHist({ past: [...h.past, treeRef.current].slice(-HISTORY), future });
|
|
438
|
+
setTree(next);
|
|
439
|
+
if (selectedRef.current && !findTopic(next, selectedRef.current)) select(null);
|
|
440
|
+
scheduleEmit();
|
|
441
|
+
say("Redone");
|
|
442
|
+
};
|
|
443
|
+
const undoRef = useRef(undo);
|
|
444
|
+
undoRef.current = undo;
|
|
445
|
+
|
|
446
|
+
const loadMap = (map) => {
|
|
447
|
+
finishEdit();
|
|
448
|
+
commit(normalizeMap(map));
|
|
449
|
+
select(null);
|
|
450
|
+
setTimeout(() => canvas.current?.fitView(), 80);
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// ── announcements ────────────────────────────────────────────────────
|
|
454
|
+
const [announce, setAnnounce] = useState("");
|
|
455
|
+
const say = (msg) => setAnnounce((m) => (m === msg ? `${msg} ` : msg));
|
|
456
|
+
|
|
457
|
+
// Selection: tell the page, keep it in view, and keep the keyboard's
|
|
458
|
+
// focus on it (so a reader hears it).
|
|
459
|
+
useEffect(() => {
|
|
460
|
+
const t = selectedId ? findTopic(treeRef.current, selectedId) : null;
|
|
461
|
+
cbs.current.onSelectionChange?.(t ?? null);
|
|
462
|
+
if (!selectedId || editing) return;
|
|
463
|
+
if (t) {
|
|
464
|
+
const path = pathTo(treeRef.current, selectedId);
|
|
465
|
+
const kids = t.children?.length ?? 0;
|
|
466
|
+
say(`${t.text || "Untitled"}${path.length > 1 ? `, level ${path.length - 1}` : ", centre"}${kids ? `, ${kids} subtopic${kids === 1 ? "" : "s"}${t.collapsed ? ", folded" : ""}` : ""}`);
|
|
467
|
+
}
|
|
468
|
+
const raf = requestAnimationFrame(() => {
|
|
469
|
+
const host = boxRef.current;
|
|
470
|
+
if (!host) return;
|
|
471
|
+
const el = host.querySelector(`.react-flow__node[data-id="${CSS.escape(selectedId)}"]`);
|
|
472
|
+
if (el && host.contains(document.activeElement) && document.activeElement !== el && !document.activeElement?.closest?.("[role=textbox]")) el.focus({ preventScroll: true });
|
|
473
|
+
ensureVisible(selectedId);
|
|
474
|
+
});
|
|
475
|
+
return () => cancelAnimationFrame(raf);
|
|
476
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
477
|
+
}, [selectedId, editing]);
|
|
478
|
+
useEffect(() => {
|
|
479
|
+
if (editing) ensureVisible(editing.id);
|
|
480
|
+
}, [editing, ensureVisible]);
|
|
481
|
+
|
|
482
|
+
// ── moving with the keys ─────────────────────────────────────────────
|
|
483
|
+
const itemsById = useMemo(() => new Map(board.nodes.map((n) => [n.id, n])), [board]);
|
|
484
|
+
const navigate = (key) => {
|
|
485
|
+
const cur = treeRef.current;
|
|
486
|
+
const id = selectedRef.current;
|
|
487
|
+
const me = itemsById.get(id);
|
|
488
|
+
if (!me) return select(cur.id);
|
|
489
|
+
const side = me.data.side;
|
|
490
|
+
const cy = me.position.y + me.height / 2;
|
|
491
|
+
const nearest = (list) => list.sort((a, b) => Math.abs(a.position.y + a.height / 2 - cy) - Math.abs(b.position.y + b.height / 2 - cy))[0];
|
|
492
|
+
const t = findTopic(cur, id);
|
|
493
|
+
const kidsOn = (s) => (t.children ?? []).map((c) => itemsById.get(c.id)).filter((n) => n && (s === undefined || n.data.side === s));
|
|
494
|
+
const outward = side === "left" ? "ArrowLeft" : "ArrowRight";
|
|
495
|
+
if (side === "root") {
|
|
496
|
+
if (key === "ArrowRight" || key === "ArrowLeft") {
|
|
497
|
+
const hit = nearest(kidsOn(key === "ArrowRight" ? "right" : "left"));
|
|
498
|
+
if (hit) select(hit.id);
|
|
499
|
+
}
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (key === outward) {
|
|
503
|
+
if (t.collapsed && t.children?.length) {
|
|
504
|
+
toggleFold(id, false);
|
|
505
|
+
select(t.children[0].id);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const hit = nearest(kidsOn());
|
|
509
|
+
if (hit) select(hit.id);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (key === "ArrowLeft" || key === "ArrowRight") return select(parentOf(cur, id)?.id ?? cur.id);
|
|
513
|
+
// Up and down: the nearest topic above or below in the same column
|
|
514
|
+
// (same side, same depth) — a sibling, or a cousin across a gap.
|
|
515
|
+
const down = key === "ArrowDown";
|
|
516
|
+
const column = board.nodes.filter((n) => n.id !== id && n.data.side === side && n.data.depth === me.data.depth);
|
|
517
|
+
const cand = column
|
|
518
|
+
.map((n) => ({ n, d: n.position.y + n.height / 2 - cy }))
|
|
519
|
+
.filter(({ d }) => (down ? d > 1 : d < -1))
|
|
520
|
+
.sort((a, b) => Math.abs(a.d) - Math.abs(b.d));
|
|
521
|
+
if (cand[0]) select(cand[0].n.id);
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// ── the keyboard ─────────────────────────────────────────────────────
|
|
525
|
+
const [help, setHelp] = useState(false);
|
|
526
|
+
const onKeyDownCapture = (e) => {
|
|
527
|
+
if (e.nativeEvent.isComposing) return;
|
|
528
|
+
const target = e.target;
|
|
529
|
+
const k = e.key;
|
|
530
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
531
|
+
const done = () => {
|
|
532
|
+
e.preventDefault();
|
|
533
|
+
e.stopPropagation();
|
|
534
|
+
};
|
|
535
|
+
const cur = treeRef.current;
|
|
536
|
+
const ed = editingRef.current;
|
|
537
|
+
|
|
538
|
+
// Typing a topic's words.
|
|
539
|
+
if (ed && target?.closest?.("[role=textbox]")) {
|
|
540
|
+
const t = findTopic(cur, ed.id);
|
|
541
|
+
const empty = !String(t?.text ?? "").trim();
|
|
542
|
+
const isRoot = ed.id === cur.id;
|
|
543
|
+
if (k === "Enter" && !e.shiftKey) {
|
|
544
|
+
done();
|
|
545
|
+
if (mod || isRoot) return finishEdit();
|
|
546
|
+
if (empty && ed.isNew) {
|
|
547
|
+
// An empty new line: step out a level, or stop.
|
|
548
|
+
if (pathTo(cur, ed.id).length >= 3) return restructure(outdentTopic, ed.id, { keepEditing: true });
|
|
549
|
+
return finishEdit();
|
|
550
|
+
}
|
|
551
|
+
finishEdit();
|
|
552
|
+
return addSibling(ed.id);
|
|
553
|
+
}
|
|
554
|
+
if (k === "Tab") {
|
|
555
|
+
done();
|
|
556
|
+
if (isRoot) {
|
|
557
|
+
finishEdit();
|
|
558
|
+
return addChild(cur.id);
|
|
559
|
+
}
|
|
560
|
+
if (e.shiftKey) return restructure(outdentTopic, ed.id, { keepEditing: true });
|
|
561
|
+
if (empty && ed.isNew) return restructure(indentTopic, ed.id, { keepEditing: true });
|
|
562
|
+
finishEdit();
|
|
563
|
+
return addChild(ed.id);
|
|
564
|
+
}
|
|
565
|
+
if (k === "Escape") {
|
|
566
|
+
done();
|
|
567
|
+
return finishEdit();
|
|
568
|
+
}
|
|
569
|
+
if (k === "Backspace" && empty && !isRoot && !(t?.children?.length)) {
|
|
570
|
+
done();
|
|
571
|
+
const prev = neighbourAfterRemove(cur, ed.id, { preferBefore: true });
|
|
572
|
+
setEditing(null);
|
|
573
|
+
if (ed.isNew) {
|
|
574
|
+
setTree(removeTopic(cur, ed.id));
|
|
575
|
+
const past = histRef.current.past;
|
|
576
|
+
if (past[past.length - 1] === ed.before) setHist({ past: past.slice(0, -1), future: histRef.current.future });
|
|
577
|
+
scheduleEmit();
|
|
578
|
+
} else commit(removeTopic(cur, ed.id));
|
|
579
|
+
if (prev && prev !== cur.id) startEdit(prev);
|
|
580
|
+
else select(prev);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (target?.closest?.(EDITING_TARGET)) return;
|
|
586
|
+
|
|
587
|
+
const sel = selectedRef.current && findTopic(cur, selectedRef.current) ? selectedRef.current : null;
|
|
588
|
+
const low = k.length === 1 ? k.toLowerCase() : k;
|
|
589
|
+
|
|
590
|
+
if (k === "?" && !mod) return done(), setHelp(true);
|
|
591
|
+
if (mod && low === "z" && !readOnly) return done(), e.shiftKey ? redo() : undo();
|
|
592
|
+
if (mod && low === "y" && !readOnly) return done(), redo();
|
|
593
|
+
if (k === "Escape") {
|
|
594
|
+
if (sel) return done(), select(null);
|
|
595
|
+
setTimeout(hand, 0);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (!sel) {
|
|
599
|
+
if (k.startsWith("Arrow") || k === "Enter" || k === "Home") return done(), select(cur.id);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (k === "Home") return done(), select(cur.id);
|
|
603
|
+
if (k.startsWith("Arrow")) {
|
|
604
|
+
if (mod && (k === "ArrowUp" || k === "ArrowDown")) return done(), toggleFold(sel, k === "ArrowUp");
|
|
605
|
+
if (e.altKey && (k === "ArrowUp" || k === "ArrowDown")) return done(), !readOnly && restructure((r, id) => shiftTopic(r, id, k === "ArrowUp" ? -1 : 1), sel);
|
|
606
|
+
if (mod || e.altKey) return;
|
|
607
|
+
return done(), navigate(k);
|
|
608
|
+
}
|
|
609
|
+
if (readOnly) {
|
|
610
|
+
if (k === " " || k === "Enter") return done(), toggleFold(sel);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (k === "Tab") return done(), e.shiftKey ? restructure(outdentTopic, sel) : addChild(sel);
|
|
614
|
+
if (k === "Enter") return done(), addSibling(sel, { before: e.shiftKey });
|
|
615
|
+
if (k === " " || k === "F2") return done(), startEdit(sel);
|
|
616
|
+
if (k === "Delete" || k === "Backspace") return done(), remove(sel);
|
|
617
|
+
if (mod && low === "d") {
|
|
618
|
+
done();
|
|
619
|
+
const r = duplicateTopic(cur, sel);
|
|
620
|
+
if (r.id) {
|
|
621
|
+
commit(r.root);
|
|
622
|
+
select(r.id);
|
|
623
|
+
}
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
// ⇧1 and ⇧2 stay the canvas's (fit, zoom to selection).
|
|
627
|
+
if (e.shiftKey && (e.code === "Digit1" || e.code === "Digit2")) return;
|
|
628
|
+
if (!mod && !e.altKey && k.length === 1) return done(), startEdit(sel, { replace: k });
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
// Copy, cut, paste: a branch as an outline, and an outline (from notes,
|
|
632
|
+
// a doc, an agent) as subtopics of the selection.
|
|
633
|
+
useEffect(() => {
|
|
634
|
+
const here = () => {
|
|
635
|
+
const a = document.activeElement;
|
|
636
|
+
return boxRef.current?.contains(a) && !a?.closest?.(EDITING_TARGET) && !a?.closest?.("[role=textbox]");
|
|
637
|
+
};
|
|
638
|
+
const copy = (e) => {
|
|
639
|
+
const id = selectedRef.current;
|
|
640
|
+
const t = id && findTopic(treeRef.current, id);
|
|
641
|
+
if (!here() || !t) return false;
|
|
642
|
+
e.clipboardData?.setData("text/plain", mindMapToText(t));
|
|
643
|
+
e.preventDefault();
|
|
644
|
+
e.stopPropagation();
|
|
645
|
+
return true;
|
|
646
|
+
};
|
|
647
|
+
const onCopy = (e) => {
|
|
648
|
+
if (copy(e)) say("Copied as an outline");
|
|
649
|
+
};
|
|
650
|
+
const onCut = (e) => {
|
|
651
|
+
if (readOnly) return;
|
|
652
|
+
const id = selectedRef.current;
|
|
653
|
+
if (id && id !== treeRef.current.id && copy(e)) remove(id);
|
|
654
|
+
};
|
|
655
|
+
const onPaste = (e) => {
|
|
656
|
+
if (!here() || readOnly) return;
|
|
657
|
+
const text = e.clipboardData?.getData("text/plain") ?? "";
|
|
658
|
+
const topics = outlineToTopics(text).slice(0, 200);
|
|
659
|
+
e.preventDefault();
|
|
660
|
+
e.stopPropagation();
|
|
661
|
+
if (!topics.length) return;
|
|
662
|
+
const cur = treeRef.current;
|
|
663
|
+
const parent = selectedRef.current && findTopic(cur, selectedRef.current) ? selectedRef.current : cur.id;
|
|
664
|
+
let next = cur;
|
|
665
|
+
for (const t of topics) next = insertTopic(next, parent, t);
|
|
666
|
+
commit(next);
|
|
667
|
+
select(topics[0].id);
|
|
668
|
+
say(`${topics.length} topic${topics.length === 1 ? "" : "s"} pasted`);
|
|
669
|
+
};
|
|
670
|
+
window.addEventListener("copy", onCopy, true);
|
|
671
|
+
window.addEventListener("cut", onCut, true);
|
|
672
|
+
window.addEventListener("paste", onPaste, true);
|
|
673
|
+
return () => {
|
|
674
|
+
window.removeEventListener("copy", onCopy, true);
|
|
675
|
+
window.removeEventListener("cut", onCut, true);
|
|
676
|
+
window.removeEventListener("paste", onPaste, true);
|
|
677
|
+
};
|
|
678
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
679
|
+
}, [readOnly]);
|
|
680
|
+
|
|
681
|
+
// ── the pointer: select, drag a topic onto another ───────────────────
|
|
682
|
+
const [drag, setDrag] = useState(null); // { id, text, x, y }
|
|
683
|
+
const [dropId, setDropId] = useState(null);
|
|
684
|
+
const onTopicPointerDown = useCallback(
|
|
685
|
+
(e, id) => {
|
|
686
|
+
if (editingRef.current && editingRef.current.id !== id) finishEdit();
|
|
687
|
+
select(id);
|
|
688
|
+
const cur = treeRef.current;
|
|
689
|
+
if (readOnly || id === cur.id || e.pointerType !== "mouse") return;
|
|
690
|
+
const start = { x: e.clientX, y: e.clientY };
|
|
691
|
+
let active = false;
|
|
692
|
+
let over = null;
|
|
693
|
+
const t = findTopic(cur, id);
|
|
694
|
+
const move = (ev) => {
|
|
695
|
+
if (!active && Math.hypot(ev.clientX - start.x, ev.clientY - start.y) < 5) return;
|
|
696
|
+
active = true;
|
|
697
|
+
const host = boxRef.current?.getBoundingClientRect();
|
|
698
|
+
if (!host) return;
|
|
699
|
+
const hit = document
|
|
700
|
+
.elementsFromPoint(ev.clientX, ev.clientY)
|
|
701
|
+
.map((el) => el.closest?.("[data-topic]"))
|
|
702
|
+
.find((el) => el && el.dataset.topic !== id);
|
|
703
|
+
const tid = hit?.dataset.topic ?? null;
|
|
704
|
+
over = tid && !isWithin(treeRef.current, id, tid) && parentOf(treeRef.current, id)?.id !== tid ? tid : null;
|
|
705
|
+
setDropId(over);
|
|
706
|
+
setDrag({ id, text: t?.text || "Untitled", x: ev.clientX - host.left, y: ev.clientY - host.top });
|
|
707
|
+
};
|
|
708
|
+
const up = () => {
|
|
709
|
+
window.removeEventListener("pointermove", move);
|
|
710
|
+
window.removeEventListener("pointerup", up);
|
|
711
|
+
window.removeEventListener("pointercancel", up);
|
|
712
|
+
if (!active) return;
|
|
713
|
+
setDrag(null);
|
|
714
|
+
setDropId(null);
|
|
715
|
+
if (!over) return;
|
|
716
|
+
const cur2 = treeRef.current;
|
|
717
|
+
const next = moveTopic(cur2, id, over);
|
|
718
|
+
if (next !== cur2) {
|
|
719
|
+
commit(next);
|
|
720
|
+
select(id);
|
|
721
|
+
const to = findTopic(next, over);
|
|
722
|
+
say(`Moved under ${to?.text || "Untitled"}`);
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
window.addEventListener("pointermove", move);
|
|
726
|
+
window.addEventListener("pointerup", up);
|
|
727
|
+
window.addEventListener("pointercancel", up);
|
|
728
|
+
},
|
|
729
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
730
|
+
[readOnly, finishEdit, select],
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
// A click on the empty canvas (not a pan) clears the selection.
|
|
734
|
+
const paneDown = useRef(null);
|
|
735
|
+
const onPointerDownCapture = (e) => {
|
|
736
|
+
paneDown.current = e.target?.classList?.contains("react-flow__pane") ? { x: e.clientX, y: e.clientY } : null;
|
|
737
|
+
};
|
|
738
|
+
const onPointerUpCapture = (e) => {
|
|
739
|
+
const p = paneDown.current;
|
|
740
|
+
paneDown.current = null;
|
|
741
|
+
if (!p || Math.hypot(e.clientX - p.x, e.clientY - p.y) > 4) return;
|
|
742
|
+
finishEdit();
|
|
743
|
+
select(null);
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
// ── the context the topics read ──────────────────────────────────────
|
|
747
|
+
const ctx = useMemo(
|
|
748
|
+
() => ({
|
|
749
|
+
selectedId,
|
|
750
|
+
editingId: editing?.id ?? null,
|
|
751
|
+
dropId,
|
|
752
|
+
dragId: drag?.id ?? null,
|
|
753
|
+
readOnly,
|
|
754
|
+
coarse,
|
|
755
|
+
placeholder,
|
|
756
|
+
onTopicPointerDown,
|
|
757
|
+
startEdit,
|
|
758
|
+
setText,
|
|
759
|
+
finishEdit,
|
|
760
|
+
toggleFold,
|
|
761
|
+
addChild,
|
|
762
|
+
}),
|
|
763
|
+
[selectedId, editing, dropId, drag, readOnly, coarse, placeholder, onTopicPointerDown, startEdit, setText, finishEdit, toggleFold, addChild],
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
// ── the chrome ───────────────────────────────────────────────────────
|
|
767
|
+
const [view, setView] = useState("map");
|
|
768
|
+
const selTopic = selectedId ? findTopic(tree, selectedId) : null;
|
|
769
|
+
const selIsRoot = selTopic?.id === tree.id;
|
|
770
|
+
const selColor = selectedId ? itemsById.get(selectedId)?.data.color : null;
|
|
771
|
+
const empty = !(tree.children?.length ?? 0);
|
|
772
|
+
const palette = useMemo(() => [...new Set([...colors, ...EXTRA_COLORS])], [colors]);
|
|
773
|
+
const starters = templates ?? mindMapTemplates();
|
|
774
|
+
const hint = editing
|
|
775
|
+
? [["Enter", "next topic"], ["Tab", "subtopic"], ["Esc", "done"]]
|
|
776
|
+
: selTopic
|
|
777
|
+
? readOnly
|
|
778
|
+
? [["Arrows", "move"], ["Space", selTopic.collapsed ? "unfold" : "fold"]]
|
|
779
|
+
: [["Tab", "subtopic"], ["Enter", "topic"], ["Space", "rename"], ["⌫", "delete"]]
|
|
780
|
+
: [["Click", "a topic"], ["Drag", "to pan"], ["?", "shortcuts"]];
|
|
781
|
+
const copyMarkdown = async () => {
|
|
782
|
+
try {
|
|
783
|
+
await navigator.clipboard.writeText(mindMapToMarkdown(treeRef.current));
|
|
784
|
+
toast.success("Copied as Markdown");
|
|
785
|
+
} catch {
|
|
786
|
+
toast.error("Couldn't copy", { description: "Your browser kept the clipboard closed." });
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
const sized = /(^|\s)(h-|min-h-|size-|flex-1|grow)/.test(className ?? "");
|
|
790
|
+
|
|
791
|
+
return (
|
|
792
|
+
<section
|
|
793
|
+
aria-label={label}
|
|
794
|
+
className={cx("flex flex-col overflow-hidden rounded-lg border border-border bg-background text-foreground", className)}
|
|
795
|
+
style={sized ? undefined : { height }}
|
|
796
|
+
>
|
|
797
|
+
<header className="flex min-h-12 shrink-0 flex-wrap items-center gap-1 border-b border-border bg-card px-2 py-1.5 text-card-foreground">
|
|
798
|
+
{!readOnly ? (
|
|
799
|
+
<>
|
|
800
|
+
<Tooltip label={`Undo · ${isMac() ? "⌘" : "Ctrl+"}Z`}>
|
|
801
|
+
<IconButton icon="undo" label="Undo" size="sm" disabled={!hist.past.length} onClick={undo} />
|
|
802
|
+
</Tooltip>
|
|
803
|
+
<Tooltip label={`Redo · ${isMac() ? "⇧⌘Z" : "Ctrl+Shift+Z"}`}>
|
|
804
|
+
<IconButton icon="redo" label="Redo" size="sm" disabled={!hist.future.length} onClick={redo} />
|
|
805
|
+
</Tooltip>
|
|
806
|
+
<SaveState save={save} onRetry={() => runSave(treeRef.current)} />
|
|
807
|
+
</>
|
|
808
|
+
) : (
|
|
809
|
+
<p className="px-2 text-sm font-medium">{clip(tree.text, 48) || "Mind map"}</p>
|
|
810
|
+
)}
|
|
811
|
+
<div className="flex-1" />
|
|
812
|
+
{view === "map" && !readOnly ? (
|
|
813
|
+
<div role="toolbar" aria-label="Topic" className="flex items-center gap-1">
|
|
814
|
+
<Tooltip label={selTopic && !selIsRoot ? "Add a topic below · Enter" : "Add a main topic"}>
|
|
815
|
+
<Button size="sm" variant="quiet" icon="plus" onClick={() => (selTopic ? addSibling(selTopic.id) : addChild(tree.id))} aria-label="Add topic">
|
|
816
|
+
<span className="max-sm:hidden">Topic</span>
|
|
817
|
+
</Button>
|
|
818
|
+
</Tooltip>
|
|
819
|
+
<Tooltip label="Add a subtopic · Tab">
|
|
820
|
+
<Button size="sm" variant="quiet" icon="git-branch" onClick={() => addChild(selTopic?.id ?? tree.id)} aria-label="Add subtopic">
|
|
821
|
+
<span className="max-sm:hidden">Subtopic</span>
|
|
822
|
+
</Button>
|
|
823
|
+
</Tooltip>
|
|
824
|
+
<Separator orientation="vertical" className="mx-0.5 h-5 max-sm:hidden" />
|
|
825
|
+
<Popover
|
|
826
|
+
label="Branch colour"
|
|
827
|
+
width="auto"
|
|
828
|
+
className="p-1.5"
|
|
829
|
+
trigger={(p) => (
|
|
830
|
+
<Button {...p} variant="quiet" size="sm" iconOnly aria-label="Branch colour" title="Branch colour" disabled={!selTopic || selIsRoot}>
|
|
831
|
+
<ColorDot color={selColor ?? "muted"} />
|
|
832
|
+
</Button>
|
|
833
|
+
)}
|
|
834
|
+
>
|
|
835
|
+
<Palette colors={palette} value={selTopic?.color ?? selColor} swatch="dot" onPick={(c) => selTopic && setColor(selTopic.id, c)} label="Branch colour" />
|
|
836
|
+
{selTopic?.color ? (
|
|
837
|
+
<Button variant="quiet" size="sm" className="mt-1 w-full" onClick={() => setColor(selTopic.id, undefined)}>
|
|
838
|
+
Use the branch's colour
|
|
839
|
+
</Button>
|
|
840
|
+
) : null}
|
|
841
|
+
</Popover>
|
|
842
|
+
<Tooltip label={selTopic?.collapsed ? "Unfold · ⌘↓" : "Fold · ⌘↑"}>
|
|
843
|
+
<IconButton
|
|
844
|
+
icon={selTopic?.collapsed ? "eye" : "eye-off"}
|
|
845
|
+
label={selTopic?.collapsed ? "Unfold branch" : "Fold branch"}
|
|
846
|
+
size="sm"
|
|
847
|
+
disabled={!selTopic || selIsRoot || !selTopic.children?.length}
|
|
848
|
+
onClick={() => toggleFold(selTopic.id)}
|
|
849
|
+
/>
|
|
850
|
+
</Tooltip>
|
|
851
|
+
<Tooltip label="Delete · ⌫">
|
|
852
|
+
<IconButton icon="trash" label="Delete topic" size="sm" disabled={!selTopic || selIsRoot} onClick={() => remove(selTopic.id)} />
|
|
853
|
+
</Tooltip>
|
|
854
|
+
</div>
|
|
855
|
+
) : null}
|
|
856
|
+
<ToggleGroup
|
|
857
|
+
size="sm"
|
|
858
|
+
className="max-sm:hidden"
|
|
859
|
+
aria-label="View"
|
|
860
|
+
value={view}
|
|
861
|
+
onChange={setView}
|
|
862
|
+
items={[
|
|
863
|
+
{ value: "map", label: "Map", icon: "git-branch", iconOnly: true },
|
|
864
|
+
{ value: "outline", label: "Outline", icon: "list", iconOnly: true },
|
|
865
|
+
]}
|
|
866
|
+
/>
|
|
867
|
+
<Menu
|
|
868
|
+
label="Map actions"
|
|
869
|
+
items={[
|
|
870
|
+
{ label: "Unfold everything", icon: "eye", onSelect: () => (readOnly ? setTree(setAllCollapsed(treeRef.current, false)) : commit(setAllCollapsed(treeRef.current, false))) },
|
|
871
|
+
{ label: "Fold every branch", icon: "eye-off", onSelect: () => (readOnly ? setTree(setAllCollapsed(treeRef.current, true)) : commit(setAllCollapsed(treeRef.current, true))) },
|
|
872
|
+
{ label: "Fit to screen", icon: "maximize", shortcut: "⇧1", onSelect: () => canvas.current?.fitView() },
|
|
873
|
+
{ type: "checkbox", label: "Outline view", checked: view === "outline", onCheckedChange: (on) => setView(on ? "outline" : "map") },
|
|
874
|
+
{ type: "separator" },
|
|
875
|
+
{ label: "Copy as Markdown", icon: "copy", onSelect: copyMarkdown },
|
|
876
|
+
{ label: "Keyboard shortcuts", icon: "keyboard", shortcut: "?", onSelect: () => setHelp(true) },
|
|
877
|
+
]}
|
|
878
|
+
/>
|
|
879
|
+
</header>
|
|
880
|
+
|
|
881
|
+
<div
|
|
882
|
+
ref={boxRef}
|
|
883
|
+
className="relative min-h-0 flex-1"
|
|
884
|
+
onKeyDownCapture={onKeyDownCapture}
|
|
885
|
+
onPointerDownCapture={onPointerDownCapture}
|
|
886
|
+
onPointerUpCapture={onPointerUpCapture}
|
|
887
|
+
>
|
|
888
|
+
<p className="sr-only" aria-live="polite">
|
|
889
|
+
{announce}
|
|
890
|
+
</p>
|
|
891
|
+
<div className="absolute inset-0" inert={view !== "map" || undefined} aria-hidden={view !== "map" || undefined}>
|
|
892
|
+
<MindContext.Provider value={ctx}>
|
|
893
|
+
<FlowCanvas
|
|
894
|
+
value={board}
|
|
895
|
+
readOnly
|
|
896
|
+
nodeTypes={NODE_TYPES}
|
|
897
|
+
canvasRef={canvas}
|
|
898
|
+
label={label}
|
|
899
|
+
height="100%"
|
|
900
|
+
bare
|
|
901
|
+
emptyState={false}
|
|
902
|
+
templates={[]}
|
|
903
|
+
describeNode={describeTopic}
|
|
904
|
+
className="[&_.react-flow__node]:transition-transform [&_.react-flow__node]:duration-200 [&_.react-flow__node]:ease-out motion-reduce:[&_.react-flow__node]:transition-none"
|
|
905
|
+
/>
|
|
906
|
+
</MindContext.Provider>
|
|
907
|
+
</div>
|
|
908
|
+
|
|
909
|
+
{view === "map" && drag ? (
|
|
910
|
+
<div aria-hidden="true" className="pointer-events-none absolute z-20 grid -translate-x-1/2 -translate-y-[calc(100%+10px)] justify-items-center gap-1" style={{ left: drag.x, top: drag.y }}>
|
|
911
|
+
<span className="max-w-60 truncate rounded-lg border border-border bg-popover px-3 py-1.5 text-sm font-medium text-popover-foreground shadow-lg">{clip(drag.text, 40)}</span>
|
|
912
|
+
<span className="rounded-md bg-foreground px-2 py-0.5 text-xs text-background">{dropId ? `Move under “${clip(findTopic(tree, dropId)?.text, 24) || "Untitled"}”` : "Drop on a topic"}</span>
|
|
913
|
+
</div>
|
|
914
|
+
) : null}
|
|
915
|
+
|
|
916
|
+
{view === "map" && empty && !readOnly && !editing ? (
|
|
917
|
+
<Starter templates={starters} onPick={(t) => loadMap(typeof t.map === "function" ? t.map() : t.map)} onStart={() => addChild(tree.id)} />
|
|
918
|
+
) : null}
|
|
919
|
+
|
|
920
|
+
{view === "map" ? (
|
|
921
|
+
<div aria-hidden="true" className="pointer-events-none absolute bottom-3 left-3 z-10 hidden max-w-[calc(100%-12rem)] flex-wrap items-center gap-x-3 gap-y-1 rounded-lg border border-border bg-card/90 px-2.5 py-1.5 text-xs text-muted-foreground shadow-xs backdrop-blur-sm md:flex">
|
|
922
|
+
{hint.map(([key, what]) => (
|
|
923
|
+
<span key={key} className="flex items-center gap-1.5 whitespace-nowrap">
|
|
924
|
+
<Kbd>{key}</Kbd>
|
|
925
|
+
{what}
|
|
926
|
+
</span>
|
|
927
|
+
))}
|
|
928
|
+
</div>
|
|
929
|
+
) : null}
|
|
930
|
+
|
|
931
|
+
{view === "outline" ? (
|
|
932
|
+
<OutlineView
|
|
933
|
+
tree={tree}
|
|
934
|
+
items={itemsById}
|
|
935
|
+
onPick={(id) => {
|
|
936
|
+
setView("map");
|
|
937
|
+
// Open the folds on the way down, so the topic can be seen.
|
|
938
|
+
const path = pathTo(treeRef.current, id);
|
|
939
|
+
let next = treeRef.current;
|
|
940
|
+
for (const p of path.slice(0, -1)) if (p.collapsed) next = updateTopic(next, p.id, { collapsed: false });
|
|
941
|
+
if (next !== treeRef.current) (readOnly ? setTree(next) : commit(next));
|
|
942
|
+
select(id);
|
|
943
|
+
}}
|
|
944
|
+
onCopy={copyMarkdown}
|
|
945
|
+
/>
|
|
946
|
+
) : null}
|
|
947
|
+
</div>
|
|
948
|
+
|
|
949
|
+
<Shortcuts open={help} onClose={() => setHelp(false)} readOnly={readOnly} />
|
|
950
|
+
</section>
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// ── pieces ─────────────────────────────────────────────────────────────
|
|
955
|
+
|
|
956
|
+
const clip = (s, n = 60) => {
|
|
957
|
+
const t = String(s ?? "").replace(/\s+/g, " ").trim();
|
|
958
|
+
return t.length > n ? `${t.slice(0, n - 1)}…` : t;
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
function describeTopic(n) {
|
|
962
|
+
if (n.type !== "topic") return undefined;
|
|
963
|
+
const d = n.data ?? {};
|
|
964
|
+
return `${d.depth === 0 ? "Centre" : `Level ${d.depth}`}: "${clip(d.text) || "Untitled"}"${d.collapsed ? ` (folded, ${d.hidden} hidden)` : ""}`;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/** Where the selection goes when a topic leaves: the next sibling, else
|
|
968
|
+
* the one before, else the parent. */
|
|
969
|
+
function neighbourAfterRemove(root, id, { preferBefore = false } = {}) {
|
|
970
|
+
const parent = parentOf(root, id);
|
|
971
|
+
if (!parent) return root.id;
|
|
972
|
+
const kids = parent.children;
|
|
973
|
+
const i = kids.findIndex((c) => c.id === id);
|
|
974
|
+
const order = preferBefore ? [kids[i - 1], kids[i + 1]] : [kids[i + 1], kids[i - 1]];
|
|
975
|
+
return (order.find(Boolean) ?? parent).id;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function SaveState({ save, onRetry }) {
|
|
979
|
+
if (save.state === "error") {
|
|
980
|
+
return (
|
|
981
|
+
<div role="alert" className="flex min-w-0 items-center gap-1 pl-1 text-xs">
|
|
982
|
+
<Icon name="warning" size={14} className="shrink-0 text-destructive" />
|
|
983
|
+
<span className="truncate max-md:sr-only">{save.error || "Couldn't save"}</span>
|
|
984
|
+
<Button variant="quiet" size="sm" onClick={onRetry}>
|
|
985
|
+
Retry
|
|
986
|
+
</Button>
|
|
987
|
+
</div>
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
if (save.state === "idle") return null;
|
|
991
|
+
const saving = save.state === "saving";
|
|
992
|
+
return (
|
|
993
|
+
<p role="status" className="flex shrink-0 items-center gap-1 px-1 text-xs text-muted-foreground">
|
|
994
|
+
<Icon name={saving ? "loader" : "check"} size={14} className={cx(saving && "motion-safe:animate-spin")} />
|
|
995
|
+
<span className="max-sm:sr-only">{saving ? "Saving…" : "Saved"}</span>
|
|
996
|
+
</p>
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/** The first run: how the map works, and a few starting points. */
|
|
1001
|
+
function Starter({ templates, onPick, onStart }) {
|
|
1002
|
+
return (
|
|
1003
|
+
<div className="pointer-events-none absolute inset-x-3 bottom-3 z-10 flex justify-center md:bottom-14">
|
|
1004
|
+
<div className="pointer-events-auto w-full max-w-md rounded-xl border border-border bg-popover p-4 text-popover-foreground shadow-lg">
|
|
1005
|
+
<p className="text-sm font-semibold">Start with the big idea</p>
|
|
1006
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
1007
|
+
Double-click the centre to name it. <Kbd>Tab</Kbd> adds a branch, <Kbd>Enter</Kbd> the next one — no mouse needed.
|
|
1008
|
+
</p>
|
|
1009
|
+
<div className="mt-3 flex flex-wrap gap-2">
|
|
1010
|
+
<Button size="sm" variant="primary" icon="plus" onClick={onStart}>
|
|
1011
|
+
Add a branch
|
|
1012
|
+
</Button>
|
|
1013
|
+
{templates.map((t) => (
|
|
1014
|
+
<Tooltip key={t.id} label={t.description ?? t.label}>
|
|
1015
|
+
<Button size="sm" icon={t.icon} onClick={() => onPick(t)}>
|
|
1016
|
+
{t.label}
|
|
1017
|
+
</Button>
|
|
1018
|
+
</Tooltip>
|
|
1019
|
+
))}
|
|
1020
|
+
</div>
|
|
1021
|
+
</div>
|
|
1022
|
+
</div>
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/** The map as a nested list: the reading of it for a screen reader, and
|
|
1027
|
+
* a quick way to jump to a topic. */
|
|
1028
|
+
function OutlineView({ tree, items, onPick, onCopy }) {
|
|
1029
|
+
const rows = [];
|
|
1030
|
+
const walk = (t, depth, color) => {
|
|
1031
|
+
const c = items.get(t.id)?.data.color ?? color;
|
|
1032
|
+
rows.push({ t, depth, color: c });
|
|
1033
|
+
for (const k of t.children ?? []) walk(k, depth + 1, c);
|
|
1034
|
+
};
|
|
1035
|
+
walk(tree, 0, null);
|
|
1036
|
+
return (
|
|
1037
|
+
<div className="absolute inset-0 z-10 overflow-y-auto bg-background">
|
|
1038
|
+
<div className="mx-auto grid max-w-2xl gap-4 px-4 py-6 sm:px-6">
|
|
1039
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
1040
|
+
<div className="min-w-0">
|
|
1041
|
+
<h3 className="truncate text-base font-semibold">{tree.text || "Untitled"}</h3>
|
|
1042
|
+
<p className="text-sm text-muted-foreground">
|
|
1043
|
+
{countTopics(tree)} topic{countTopics(tree) === 1 ? "" : "s"}. Choose one to find it on the map.
|
|
1044
|
+
</p>
|
|
1045
|
+
</div>
|
|
1046
|
+
<Button size="sm" icon="copy" onClick={onCopy}>
|
|
1047
|
+
Copy as Markdown
|
|
1048
|
+
</Button>
|
|
1049
|
+
</div>
|
|
1050
|
+
{rows.length > 1 ? (
|
|
1051
|
+
<ul className="grid gap-0.5" aria-label="Outline">
|
|
1052
|
+
{rows.slice(1).map(({ t, depth, color }) => (
|
|
1053
|
+
<li key={t.id} style={{ paddingLeft: (depth - 1) * 20 }}>
|
|
1054
|
+
<Button variant="quiet" size="sm" align="start" className="w-full min-w-0" onClick={() => onPick(t.id)}>
|
|
1055
|
+
<ColorDot color={color ?? "muted"} className={depth > 1 ? "size-2" : "size-2.5"} />
|
|
1056
|
+
<span className={cx("truncate", depth === 1 ? "font-medium" : "font-normal")}>{t.text || "Untitled"}</span>
|
|
1057
|
+
{t.collapsed && t.children?.length ? <span className="ml-auto shrink-0 text-xs text-muted-foreground tabular-nums">{countTopics(t)} folded</span> : null}
|
|
1058
|
+
</Button>
|
|
1059
|
+
</li>
|
|
1060
|
+
))}
|
|
1061
|
+
</ul>
|
|
1062
|
+
) : (
|
|
1063
|
+
<div className="grid justify-items-center gap-2 rounded-lg border border-dashed border-border px-4 py-10 text-center">
|
|
1064
|
+
<p className="text-sm text-muted-foreground">Only the centre so far. Branches you add show up here as a list.</p>
|
|
1065
|
+
</div>
|
|
1066
|
+
)}
|
|
1067
|
+
</div>
|
|
1068
|
+
</div>
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const KEYS = [
|
|
1073
|
+
["Tab", "Add a subtopic"],
|
|
1074
|
+
["Enter", "Add the next topic (⇧ above)"],
|
|
1075
|
+
["Space", "Rename (or just start typing)"],
|
|
1076
|
+
["⌫", "Delete the topic and its branch"],
|
|
1077
|
+
["← → ↑ ↓", "Move around the map"],
|
|
1078
|
+
["⌥↑ ⌥↓", "Move the topic up or down"],
|
|
1079
|
+
["⇧Tab", "Move the topic up a level"],
|
|
1080
|
+
["⌘↑ ⌘↓", "Fold, unfold"],
|
|
1081
|
+
["⌘D", "Duplicate the branch"],
|
|
1082
|
+
["⌘C ⌘V", "Copy a branch, paste an outline"],
|
|
1083
|
+
["⌘Z", "Undo (⇧⌘Z redo)"],
|
|
1084
|
+
["⇧1", "Fit the map to the screen"],
|
|
1085
|
+
];
|
|
1086
|
+
const TYPING = [
|
|
1087
|
+
["Enter", "Next topic (an empty one steps out a level)"],
|
|
1088
|
+
["Tab", "Subtopic (an empty one moves in a level)"],
|
|
1089
|
+
["⇧Enter", "A new line in the words"],
|
|
1090
|
+
["Esc", "Done"],
|
|
1091
|
+
];
|
|
1092
|
+
|
|
1093
|
+
function Shortcuts({ open, onClose, readOnly }) {
|
|
1094
|
+
const mac = isMac();
|
|
1095
|
+
const key = (k) => (mac ? k : k.replace(/⌘/g, "Ctrl+").replace(/⌥/g, "Alt+").replace(/⇧/g, "Shift+"));
|
|
1096
|
+
const rows = readOnly ? [["← → ↑ ↓", "Move around the map"], ["Space", "Fold, unfold"], ["⇧1", "Fit the map to the screen"]] : KEYS;
|
|
1097
|
+
const list = (items) => (
|
|
1098
|
+
<dl className="grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2 text-sm">
|
|
1099
|
+
{items.map(([k, what]) => (
|
|
1100
|
+
<div key={k} className="contents">
|
|
1101
|
+
<dt className="justify-self-start">
|
|
1102
|
+
<Kbd>{key(k)}</Kbd>
|
|
1103
|
+
</dt>
|
|
1104
|
+
<dd className="text-muted-foreground">{what}</dd>
|
|
1105
|
+
</div>
|
|
1106
|
+
))}
|
|
1107
|
+
</dl>
|
|
1108
|
+
);
|
|
1109
|
+
return (
|
|
1110
|
+
<Sheet open={open} onClose={onClose} title="Keyboard shortcuts" description="A mind map you can build without the mouse.">
|
|
1111
|
+
<div className="grid gap-6">
|
|
1112
|
+
<section className="grid gap-3">
|
|
1113
|
+
<h3 className="text-sm font-medium">With a topic selected</h3>
|
|
1114
|
+
{list(rows)}
|
|
1115
|
+
</section>
|
|
1116
|
+
{!readOnly ? (
|
|
1117
|
+
<section className="grid gap-3">
|
|
1118
|
+
<h3 className="text-sm font-medium">While typing a topic</h3>
|
|
1119
|
+
{list(TYPING)}
|
|
1120
|
+
</section>
|
|
1121
|
+
) : null}
|
|
1122
|
+
</div>
|
|
1123
|
+
</Sheet>
|
|
1124
|
+
);
|
|
1125
|
+
}
|