makersclaw 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +180 -0
- package/package.json +3 -2
- package/r/annotate.json +62 -0
- package/r/approval-request.json +52 -0
- package/r/audio-player.json +74 -0
- package/r/auth-form.json +55 -0
- package/r/board.json +10 -6
- package/r/calendar-view.json +105 -0
- package/r/cart.json +62 -0
- package/r/chart.json +83 -0
- package/r/chat.json +95 -0
- package/r/code-editor.json +94 -0
- package/r/comment-box.json +64 -0
- package/r/contact-section.json +53 -0
- package/r/core.json +205 -8
- package/r/csv-import.json +63 -0
- package/r/data-table.json +69 -0
- package/r/diagram.json +74 -0
- package/r/doc-editor.json +82 -0
- package/r/doc-page.json +61 -0
- package/r/email-render.json +73 -0
- package/r/faq.json +50 -0
- package/r/file-upload.json +54 -0
- package/r/flow-canvas.json +93 -0
- package/r/footer.json +52 -0
- package/r/gantt.json +70 -0
- package/r/job-panel.json +12 -7
- package/r/json-editor.json +66 -0
- package/r/map.json +89 -0
- package/r/menubar.json +52 -0
- package/r/mind-map.json +67 -0
- package/r/navigation-menu.json +52 -0
- package/r/order-detail.json +54 -0
- package/r/org-chart.json +66 -0
- package/r/page-builder.json +105 -0
- package/r/payment-methods.json +51 -0
- package/r/pdf-document.json +79 -0
- package/r/pdf-viewer.json +88 -0
- package/r/product-detail.json +76 -0
- package/r/product-grid.json +65 -0
- package/r/record-form.json +67 -0
- package/r/record-notes.json +55 -0
- package/r/registry.json +3411 -29
- package/r/search-dialog.json +51 -0
- package/r/settings-list.json +66 -0
- package/r/setup-checklist.json +51 -0
- package/r/share-dialog.json +52 -0
- package/r/sidebar.json +69 -0
- package/r/signature-pad.json +72 -0
- package/r/sketch-canvas.json +99 -0
- package/r/spreadsheet.json +90 -0
- package/r/stat-cards.json +66 -0
- package/r/task-list.json +63 -0
- package/r/task-model.json +98 -0
- package/r/testimonials.json +52 -0
- package/r/transaction-list.json +53 -0
- package/r/whiteboard.json +66 -0
- package/registry/blocks/annotate/README.md +179 -0
- package/registry/blocks/annotate/annotate-data.js +250 -0
- package/registry/blocks/annotate/annotate.jsx +646 -0
- package/registry/blocks/annotate/block.json +10 -0
- package/registry/blocks/annotate/demo.jsx +135 -0
- package/registry/blocks/annotate/index.js +15 -0
- package/registry/blocks/approval-request/README.md +116 -0
- package/registry/blocks/approval-request/approval-request.jsx +386 -0
- package/registry/blocks/approval-request/block.json +10 -0
- package/registry/blocks/approval-request/demo.jsx +127 -0
- package/registry/blocks/approval-request/index.js +4 -0
- package/registry/blocks/audio-player/README.md +167 -0
- package/registry/blocks/audio-player/audio-player.jsx +623 -0
- package/registry/blocks/audio-player/block.json +14 -0
- package/registry/blocks/audio-player/colors.js +109 -0
- package/registry/blocks/audio-player/demo.jsx +209 -0
- package/registry/blocks/audio-player/index.js +6 -0
- package/registry/blocks/audio-player/time.js +67 -0
- package/registry/blocks/audio-player/wavesurfer.js +9 -0
- package/registry/blocks/auth-form/README.md +126 -0
- package/registry/blocks/auth-form/auth-form.jsx +568 -0
- package/registry/blocks/auth-form/block.json +10 -0
- package/registry/blocks/auth-form/demo.jsx +131 -0
- package/registry/blocks/auth-form/index.js +3 -0
- package/registry/blocks/board/README.md +55 -5
- package/registry/blocks/board/block.json +2 -2
- package/registry/blocks/board/board.jsx +230 -86
- package/registry/blocks/board/demo.jsx +103 -24
- package/registry/blocks/calendar-view/README.md +180 -0
- package/registry/blocks/calendar-view/agenda.jsx +147 -0
- package/registry/blocks/calendar-view/block.json +76 -0
- package/registry/blocks/calendar-view/calendar-model.js +442 -0
- package/registry/blocks/calendar-view/calendar.jsx +627 -0
- package/registry/blocks/calendar-view/demo.jsx +195 -0
- package/registry/blocks/calendar-view/draft.jsx +49 -0
- package/registry/blocks/calendar-view/index.js +39 -0
- package/registry/blocks/calendar-view/month.jsx +346 -0
- package/registry/blocks/calendar-view/paint.js +69 -0
- package/registry/blocks/calendar-view/time-grid.jsx +527 -0
- package/registry/blocks/calendar-view/tray.jsx +87 -0
- package/registry/blocks/cart/README.md +146 -0
- package/registry/blocks/cart/block.json +10 -0
- package/registry/blocks/cart/cart.jsx +597 -0
- package/registry/blocks/cart/demo.jsx +98 -0
- package/registry/blocks/cart/index.js +4 -0
- package/registry/blocks/cart/money.js +28 -0
- package/registry/blocks/chart/README.md +316 -0
- package/registry/blocks/chart/block.json +10 -0
- package/registry/blocks/chart/cartesian.jsx +642 -0
- package/registry/blocks/chart/core.jsx +262 -0
- package/registry/blocks/chart/demo.jsx +357 -0
- package/registry/blocks/chart/heatmap.jsx +336 -0
- package/registry/blocks/chart/index.js +8 -0
- package/registry/blocks/chart/pie.jsx +245 -0
- package/registry/blocks/chart/sparkline.jsx +88 -0
- package/registry/blocks/chat/README.md +273 -0
- package/registry/blocks/chat/attachment.jsx +95 -0
- package/registry/blocks/chat/block.json +10 -0
- package/registry/blocks/chat/chat.jsx +132 -0
- package/registry/blocks/chat/composer.jsx +364 -0
- package/registry/blocks/chat/demo.jsx +578 -0
- package/registry/blocks/chat/header.jsx +34 -0
- package/registry/blocks/chat/index.js +9 -0
- package/registry/blocks/chat/message.jsx +450 -0
- package/registry/blocks/chat/scroller.jsx +247 -0
- package/registry/blocks/chat/text.jsx +104 -0
- package/registry/blocks/code-editor/README.md +210 -0
- package/registry/blocks/code-editor/block.json +22 -0
- package/registry/blocks/code-editor/code-editor.jsx +534 -0
- package/registry/blocks/code-editor/codemirror.js +38 -0
- package/registry/blocks/code-editor/demo.jsx +205 -0
- package/registry/blocks/code-editor/index.js +8 -0
- package/registry/blocks/code-editor/json.js +317 -0
- package/registry/blocks/code-editor/sql.js +136 -0
- package/registry/blocks/code-editor/text.js +101 -0
- package/registry/blocks/code-editor/theme.js +186 -0
- package/registry/blocks/comment-box/README.md +123 -0
- package/registry/blocks/comment-box/block.json +10 -0
- package/registry/blocks/comment-box/comment-body.jsx +217 -0
- package/registry/blocks/comment-box/comment-box.jsx +198 -0
- package/registry/blocks/comment-box/comment-time.jsx +77 -0
- package/registry/blocks/comment-box/demo.jsx +141 -0
- package/registry/blocks/comment-box/index.js +8 -0
- package/registry/blocks/contact-section/README.md +152 -0
- package/registry/blocks/contact-section/block.json +11 -0
- package/registry/blocks/contact-section/contact-section.jsx +509 -0
- package/registry/blocks/contact-section/demo.jsx +93 -0
- package/registry/blocks/contact-section/index.js +4 -0
- package/registry/blocks/csv-import/README.md +133 -0
- package/registry/blocks/csv-import/block.json +13 -0
- package/registry/blocks/csv-import/csv-import.jsx +479 -0
- package/registry/blocks/csv-import/demo.jsx +106 -0
- package/registry/blocks/csv-import/import.js +200 -0
- package/registry/blocks/csv-import/index.js +7 -0
- package/registry/blocks/data-table/README.md +386 -0
- package/registry/blocks/data-table/block.json +10 -0
- package/registry/blocks/data-table/data-table.jsx +1164 -0
- package/registry/blocks/data-table/demo.jsx +588 -0
- package/registry/blocks/data-table/index.js +5 -0
- package/registry/blocks/data-table/model.js +169 -0
- package/registry/blocks/diagram/README.md +108 -0
- package/registry/blocks/diagram/block.json +13 -0
- package/registry/blocks/diagram/demo.jsx +113 -0
- package/registry/blocks/diagram/diagram.jsx +481 -0
- package/registry/blocks/diagram/index.js +7 -0
- package/registry/blocks/diagram/mermaid.js +52 -0
- package/registry/blocks/diagram/palette.js +313 -0
- package/registry/blocks/diagram/source.js +98 -0
- package/registry/blocks/doc-editor/README.md +215 -0
- package/registry/blocks/doc-editor/block.json +15 -0
- package/registry/blocks/doc-editor/demo.jsx +225 -0
- package/registry/blocks/doc-editor/doc-editor.jsx +291 -0
- package/registry/blocks/doc-editor/doc.js +309 -0
- package/registry/blocks/doc-editor/index.js +7 -0
- package/registry/blocks/doc-editor/schema.jsx +56 -0
- package/registry/blocks/doc-editor/shadcn-classes.js +110 -0
- package/registry/blocks/doc-editor/theme.js +82 -0
- package/registry/blocks/doc-page/README.md +119 -0
- package/registry/blocks/doc-page/autosave.js +104 -0
- package/registry/blocks/doc-page/block.json +10 -0
- package/registry/blocks/doc-page/demo.jsx +137 -0
- package/registry/blocks/doc-page/doc-page.jsx +276 -0
- package/registry/blocks/doc-page/index.js +4 -0
- package/registry/blocks/email-render/README.md +151 -0
- package/registry/blocks/email-render/block.json +14 -0
- package/registry/blocks/email-render/demo.jsx +153 -0
- package/registry/blocks/email-render/doc-email.jsx +210 -0
- package/registry/blocks/email-render/email.jsx +221 -0
- package/registry/blocks/email-render/index.js +10 -0
- package/registry/blocks/email-render/render.js +50 -0
- package/registry/blocks/email-render/theme.js +215 -0
- package/registry/blocks/faq/README.md +101 -0
- package/registry/blocks/faq/block.json +11 -0
- package/registry/blocks/faq/demo.jsx +89 -0
- package/registry/blocks/faq/faq.jsx +220 -0
- package/registry/blocks/faq/index.js +4 -0
- package/registry/blocks/file-upload/README.md +138 -0
- package/registry/blocks/file-upload/block.json +10 -0
- package/registry/blocks/file-upload/demo.jsx +204 -0
- package/registry/blocks/file-upload/file-upload.jsx +635 -0
- package/registry/blocks/file-upload/index.js +4 -0
- package/registry/blocks/flow-canvas/README.md +420 -0
- package/registry/blocks/flow-canvas/block.json +13 -0
- package/registry/blocks/flow-canvas/board.js +819 -0
- package/registry/blocks/flow-canvas/chrome.jsx +649 -0
- package/registry/blocks/flow-canvas/context.jsx +35 -0
- package/registry/blocks/flow-canvas/demo.jsx +249 -0
- package/registry/blocks/flow-canvas/edges.jsx +107 -0
- package/registry/blocks/flow-canvas/flow-canvas.jsx +1329 -0
- package/registry/blocks/flow-canvas/index.js +41 -0
- package/registry/blocks/flow-canvas/nodes.jsx +392 -0
- package/registry/blocks/flow-canvas/theme.js +174 -0
- package/registry/blocks/footer/README.md +133 -0
- package/registry/blocks/footer/block.json +11 -0
- package/registry/blocks/footer/demo.jsx +119 -0
- package/registry/blocks/footer/footer.jsx +406 -0
- package/registry/blocks/footer/index.js +4 -0
- package/registry/blocks/gantt/README.md +220 -0
- package/registry/blocks/gantt/block.json +10 -0
- package/registry/blocks/gantt/demo.jsx +148 -0
- package/registry/blocks/gantt/gantt.jsx +923 -0
- package/registry/blocks/gantt/index.js +7 -0
- package/registry/blocks/gantt/rows.jsx +468 -0
- package/registry/blocks/gantt/schedule.js +278 -0
- package/registry/blocks/job-panel/README.md +80 -0
- package/registry/blocks/job-panel/block.json +3 -3
- package/registry/blocks/job-panel/demo.jsx +230 -3
- package/registry/blocks/job-panel/job-panel.jsx +169 -3
- package/registry/blocks/json-editor/README.md +133 -0
- package/registry/blocks/json-editor/block.json +10 -0
- package/registry/blocks/json-editor/demo.jsx +92 -0
- package/registry/blocks/json-editor/index.js +7 -0
- package/registry/blocks/json-editor/json-editor.jsx +305 -0
- package/registry/blocks/json-editor/json-tree.jsx +393 -0
- package/registry/blocks/json-editor/tree.js +225 -0
- package/registry/blocks/map/README.md +221 -0
- package/registry/blocks/map/block.json +14 -0
- package/registry/blocks/map/demo.jsx +201 -0
- package/registry/blocks/map/geo.js +198 -0
- package/registry/blocks/map/index.js +8 -0
- package/registry/blocks/map/list.jsx +128 -0
- package/registry/blocks/map/map-view.jsx +715 -0
- package/registry/blocks/map/maplibre.js +30 -0
- package/registry/blocks/map/marks.jsx +146 -0
- package/registry/blocks/map/theme.js +142 -0
- package/registry/blocks/menubar/README.md +109 -0
- package/registry/blocks/menubar/block.json +10 -0
- package/registry/blocks/menubar/demo.jsx +128 -0
- package/registry/blocks/menubar/index.js +4 -0
- package/registry/blocks/menubar/menubar.jsx +199 -0
- package/registry/blocks/mind-map/README.md +206 -0
- package/registry/blocks/mind-map/block.json +10 -0
- package/registry/blocks/mind-map/demo.jsx +103 -0
- package/registry/blocks/mind-map/index.js +31 -0
- package/registry/blocks/mind-map/mind-map.jsx +1125 -0
- package/registry/blocks/mind-map/topic.jsx +143 -0
- package/registry/blocks/mind-map/tree.js +645 -0
- package/registry/blocks/navigation-menu/README.md +124 -0
- package/registry/blocks/navigation-menu/block.json +10 -0
- package/registry/blocks/navigation-menu/demo.jsx +84 -0
- package/registry/blocks/navigation-menu/index.js +4 -0
- package/registry/blocks/navigation-menu/navigation-menu.jsx +448 -0
- package/registry/blocks/order-detail/README.md +109 -0
- package/registry/blocks/order-detail/block.json +10 -0
- package/registry/blocks/order-detail/demo.jsx +88 -0
- package/registry/blocks/order-detail/index.js +4 -0
- package/registry/blocks/order-detail/order-detail.jsx +354 -0
- package/registry/blocks/org-chart/README.md +145 -0
- package/registry/blocks/org-chart/block.json +10 -0
- package/registry/blocks/org-chart/demo.jsx +94 -0
- package/registry/blocks/org-chart/index.js +7 -0
- package/registry/blocks/org-chart/org-chart.jsx +631 -0
- package/registry/blocks/org-chart/org.js +245 -0
- package/registry/blocks/org-chart/person.jsx +96 -0
- package/registry/blocks/page-builder/README.md +257 -0
- package/registry/blocks/page-builder/block.json +14 -0
- package/registry/blocks/page-builder/demo.jsx +145 -0
- package/registry/blocks/page-builder/grapes.js +18 -0
- package/registry/blocks/page-builder/index.js +11 -0
- package/registry/blocks/page-builder/page-builder.jsx +704 -0
- package/registry/blocks/page-builder/page-view.jsx +33 -0
- package/registry/blocks/page-builder/page.js +118 -0
- package/registry/blocks/page-builder/panels.jsx +588 -0
- package/registry/blocks/page-builder/sections.js +209 -0
- package/registry/blocks/page-builder/theme.js +87 -0
- package/registry/blocks/page-builder/thumbs.jsx +189 -0
- package/registry/blocks/page-builder/tokens.js +329 -0
- package/registry/blocks/payment-methods/README.md +104 -0
- package/registry/blocks/payment-methods/block.json +10 -0
- package/registry/blocks/payment-methods/demo.jsx +71 -0
- package/registry/blocks/payment-methods/index.js +4 -0
- package/registry/blocks/payment-methods/payment-methods.jsx +302 -0
- package/registry/blocks/pdf-document/README.md +213 -0
- package/registry/blocks/pdf-document/block.json +13 -0
- package/registry/blocks/pdf-document/demo.jsx +136 -0
- package/registry/blocks/pdf-document/index.js +33 -0
- package/registry/blocks/pdf-document/money.js +83 -0
- package/registry/blocks/pdf-document/pdf-document.jsx +144 -0
- package/registry/blocks/pdf-document/primitives.jsx +271 -0
- package/registry/blocks/pdf-document/quote.jsx +179 -0
- package/registry/blocks/pdf-document/theme.js +193 -0
- package/registry/blocks/pdf-viewer/README.md +191 -0
- package/registry/blocks/pdf-viewer/block.json +14 -0
- package/registry/blocks/pdf-viewer/demo.jsx +252 -0
- package/registry/blocks/pdf-viewer/helpers.js +270 -0
- package/registry/blocks/pdf-viewer/index.js +8 -0
- package/registry/blocks/pdf-viewer/notes.jsx +195 -0
- package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1368 -0
- package/registry/blocks/pdf-viewer/pdfjs.js +37 -0
- package/registry/blocks/pdf-viewer/ssr-shim-end.js +6 -0
- package/registry/blocks/pdf-viewer/ssr-shim.js +44 -0
- package/registry/blocks/product-detail/README.md +217 -0
- package/registry/blocks/product-detail/block.json +10 -0
- package/registry/blocks/product-detail/demo.jsx +233 -0
- package/registry/blocks/product-detail/index.js +7 -0
- package/registry/blocks/product-detail/price.jsx +115 -0
- package/registry/blocks/product-detail/product-detail.jsx +348 -0
- package/registry/blocks/product-detail/product-media.jsx +102 -0
- package/registry/blocks/product-detail/product-options.jsx +144 -0
- package/registry/blocks/product-grid/README.md +162 -0
- package/registry/blocks/product-grid/block.json +10 -0
- package/registry/blocks/product-grid/demo.jsx +255 -0
- package/registry/blocks/product-grid/index.js +5 -0
- package/registry/blocks/product-grid/product-card.jsx +232 -0
- package/registry/blocks/product-grid/product-grid.jsx +235 -0
- package/registry/blocks/record-form/README.md +196 -0
- package/registry/blocks/record-form/block.json +10 -0
- package/registry/blocks/record-form/demo.jsx +525 -0
- package/registry/blocks/record-form/index.js +4 -0
- package/registry/blocks/record-form/record-fields.jsx +457 -0
- package/registry/blocks/record-form/record-form.jsx +415 -0
- package/registry/blocks/record-form/rules.js +81 -0
- package/registry/blocks/record-notes/README.md +139 -0
- package/registry/blocks/record-notes/block.json +10 -0
- package/registry/blocks/record-notes/demo.jsx +179 -0
- package/registry/blocks/record-notes/index.js +4 -0
- package/registry/blocks/record-notes/record-notes.jsx +266 -0
- package/registry/blocks/search-dialog/README.md +108 -0
- package/registry/blocks/search-dialog/block.json +10 -0
- package/registry/blocks/search-dialog/demo.jsx +116 -0
- package/registry/blocks/search-dialog/index.js +3 -0
- package/registry/blocks/search-dialog/search-dialog.jsx +235 -0
- package/registry/blocks/settings-list/README.md +171 -0
- package/registry/blocks/settings-list/block.json +10 -0
- package/registry/blocks/settings-list/demo.jsx +333 -0
- package/registry/blocks/settings-list/index.js +4 -0
- package/registry/blocks/settings-list/row-state.jsx +93 -0
- package/registry/blocks/settings-list/settings-list.jsx +349 -0
- package/registry/blocks/settings-list/settings-matrix.jsx +118 -0
- package/registry/blocks/setup-checklist/README.md +142 -0
- package/registry/blocks/setup-checklist/block.json +10 -0
- package/registry/blocks/setup-checklist/demo.jsx +159 -0
- package/registry/blocks/setup-checklist/index.js +4 -0
- package/registry/blocks/setup-checklist/setup-checklist.jsx +446 -0
- package/registry/blocks/share-dialog/README.md +113 -0
- package/registry/blocks/share-dialog/block.json +10 -0
- package/registry/blocks/share-dialog/demo.jsx +140 -0
- package/registry/blocks/share-dialog/index.js +3 -0
- package/registry/blocks/share-dialog/share-dialog.jsx +384 -0
- package/registry/blocks/sidebar/README.md +190 -0
- package/registry/blocks/sidebar/block.json +43 -0
- package/registry/blocks/sidebar/demo.jsx +577 -0
- package/registry/blocks/sidebar/index.js +4 -0
- package/registry/blocks/sidebar/sidebar-panel.jsx +149 -0
- package/registry/blocks/sidebar/sidebar.jsx +864 -0
- package/registry/blocks/signature-pad/README.md +137 -0
- package/registry/blocks/signature-pad/block.json +13 -0
- package/registry/blocks/signature-pad/colors.js +115 -0
- package/registry/blocks/signature-pad/demo.jsx +94 -0
- package/registry/blocks/signature-pad/index.js +6 -0
- package/registry/blocks/signature-pad/pad.js +7 -0
- package/registry/blocks/signature-pad/signature-pad.jsx +434 -0
- package/registry/blocks/signature-pad/signature.js +68 -0
- package/registry/blocks/sketch-canvas/README.md +280 -0
- package/registry/blocks/sketch-canvas/block.json +13 -0
- package/registry/blocks/sketch-canvas/demo.jsx +159 -0
- package/registry/blocks/sketch-canvas/excalidraw.js +40 -0
- package/registry/blocks/sketch-canvas/index.js +9 -0
- package/registry/blocks/sketch-canvas/palette.js +226 -0
- package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1188 -0
- package/registry/blocks/sketch-canvas/sketch.js +179 -0
- package/registry/blocks/sketch-canvas/ssr-shim-end.js +6 -0
- package/registry/blocks/sketch-canvas/ssr-shim.js +45 -0
- package/registry/blocks/sketch-canvas/stamps.js +104 -0
- package/registry/blocks/sketch-canvas/theme.js +131 -0
- package/registry/blocks/spreadsheet/README.md +307 -0
- package/registry/blocks/spreadsheet/block.json +13 -0
- package/registry/blocks/spreadsheet/cells.jsx +455 -0
- package/registry/blocks/spreadsheet/chrome.jsx +325 -0
- package/registry/blocks/spreadsheet/demo.jsx +177 -0
- package/registry/blocks/spreadsheet/grid.js +387 -0
- package/registry/blocks/spreadsheet/index.js +34 -0
- package/registry/blocks/spreadsheet/spreadsheet.jsx +1732 -0
- package/registry/blocks/spreadsheet/types.js +473 -0
- package/registry/blocks/stat-cards/README.md +173 -0
- package/registry/blocks/stat-cards/block.json +10 -0
- package/registry/blocks/stat-cards/demo.jsx +257 -0
- package/registry/blocks/stat-cards/index.js +4 -0
- package/registry/blocks/stat-cards/stat-cards.jsx +307 -0
- package/registry/blocks/stat-cards/usage.jsx +243 -0
- package/registry/blocks/task-list/README.md +176 -0
- package/registry/blocks/task-list/block.json +10 -0
- package/registry/blocks/task-list/demo.jsx +134 -0
- package/registry/blocks/task-list/index.js +5 -0
- package/registry/blocks/task-list/task-list.jsx +907 -0
- package/registry/blocks/task-model/README.md +455 -0
- package/registry/blocks/task-model/block.json +167 -0
- package/registry/blocks/task-model/changes.js +89 -0
- package/registry/blocks/task-model/dates.js +371 -0
- package/registry/blocks/task-model/demo.jsx +215 -0
- package/registry/blocks/task-model/dnd.jsx +240 -0
- package/registry/blocks/task-model/index.js +97 -0
- package/registry/blocks/task-model/model.js +919 -0
- package/registry/blocks/task-model/parts.jsx +449 -0
- package/registry/blocks/task-model/toolbar.jsx +271 -0
- package/registry/blocks/testimonials/README.md +139 -0
- package/registry/blocks/testimonials/block.json +11 -0
- package/registry/blocks/testimonials/demo.jsx +137 -0
- package/registry/blocks/testimonials/index.js +4 -0
- package/registry/blocks/testimonials/testimonials.jsx +430 -0
- package/registry/blocks/transaction-list/README.md +126 -0
- package/registry/blocks/transaction-list/block.json +10 -0
- package/registry/blocks/transaction-list/demo.jsx +79 -0
- package/registry/blocks/transaction-list/index.js +4 -0
- package/registry/blocks/transaction-list/transaction-list.jsx +383 -0
- package/registry/blocks/whiteboard/README.md +179 -0
- package/registry/blocks/whiteboard/block.json +10 -0
- package/registry/blocks/whiteboard/demo.jsx +169 -0
- package/registry/blocks/whiteboard/index.js +6 -0
- package/registry/blocks/whiteboard/templates.js +150 -0
- package/registry/blocks/whiteboard/whiteboard.jsx +721 -0
- package/src/cli.mjs +19 -6
- package/src/install.mjs +18 -0
- package/src/registry.mjs +38 -4
- package/src/shadcn.mjs +14 -1
- package/templates/next/files/README.md +145 -11
- package/templates/next/files/app/globals.css +55 -4
- package/templates/next/files/check/run.mjs +3 -3
- package/templates/next/files/components/ui/avatar.jsx +141 -0
- package/templates/next/files/components/ui/breadcrumb.jsx +149 -0
- package/templates/next/files/components/ui/button-group.jsx +70 -0
- package/templates/next/files/components/ui/button.jsx +132 -12
- package/templates/next/files/components/ui/calendar.jsx +609 -0
- package/templates/next/files/components/ui/carousel.jsx +281 -0
- package/templates/next/files/components/ui/collapsible.jsx +208 -0
- package/templates/next/files/components/ui/combobox.jsx +608 -0
- package/templates/next/files/components/ui/command.jsx +577 -0
- package/templates/next/files/components/ui/context-menu.jsx +191 -0
- package/templates/next/files/components/ui/date-picker.jsx +360 -0
- package/templates/next/files/components/ui/distribution.jsx +117 -0
- package/templates/next/files/components/ui/feedback.jsx +143 -0
- package/templates/next/files/components/ui/field.jsx +115 -3
- package/templates/next/files/components/ui/file-input.jsx +234 -0
- package/templates/next/files/components/ui/icon.jsx +130 -8
- package/templates/next/files/components/ui/image.jsx +143 -0
- package/templates/next/files/components/ui/index.js +47 -2
- package/templates/next/files/components/ui/input-group.jsx +213 -0
- package/templates/next/files/components/ui/input-otp.jsx +170 -0
- package/templates/next/files/components/ui/item.jsx +206 -0
- package/templates/next/files/components/ui/menu.jsx +454 -82
- package/templates/next/files/components/ui/notice.jsx +126 -14
- package/templates/next/files/components/ui/pagination.jsx +156 -0
- package/templates/next/files/components/ui/popover.jsx +284 -0
- package/templates/next/files/components/ui/radio.jsx +193 -0
- package/templates/next/files/components/ui/rating.jsx +131 -0
- package/templates/next/files/components/ui/resizable.jsx +338 -0
- package/templates/next/files/components/ui/scroll-area.jsx +123 -0
- package/templates/next/files/components/ui/separator.jsx +119 -0
- package/templates/next/files/components/ui/sheet.jsx +96 -11
- package/templates/next/files/components/ui/slider.jsx +174 -0
- package/templates/next/files/components/ui/stat.jsx +574 -0
- package/templates/next/files/components/ui/steps.jsx +338 -0
- package/templates/next/files/components/ui/swatch.jsx +33 -0
- package/templates/next/files/components/ui/table.jsx +130 -0
- package/templates/next/files/components/ui/toast.jsx +440 -0
- package/templates/next/files/components/ui/toggle.jsx +195 -0
- package/templates/next/files/components/ui/tooltip.jsx +270 -0
- package/templates/next/template.json +264 -8
package/r/mind-map.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "mind-map",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "MindMap",
|
|
6
|
+
"description": "A mind map you can build without the mouse, on flow-canvas: a centre topic with main branches balanced on both sides, each branch a tidy auto-laid-out tree in its own chart hue. Tab adds a subtopic, Enter the next topic, typing renames, arrows walk the map, ⌘↑/⌘↓ fold a branch (a folded one shows how many it hides), ⌥↑/⌥↓ reorder, Shift+Tab moves up a level; drag a topic onto another to move its branch. Undo/redo, a colour per branch, an Outline view, copy a branch as an outline and paste an outline as topics, starter templates. The map is plain JSON (a tree of topics); saves are async callbacks answering { ok }.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.3.1/r/core.json",
|
|
9
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.3.1/r/flow-canvas.json"
|
|
10
|
+
],
|
|
11
|
+
"dependencies": [],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "registry/blocks/mind-map/index.js",
|
|
15
|
+
"type": "registry:component",
|
|
16
|
+
"target": "@components/makersclaw/blocks/mind-map/index.js",
|
|
17
|
+
"content": "// The MindMap block — `makersclaw add mind-map`. Fixed: compose it through\n// its props (README.md beside this file). Built on flow-canvas (it draws,\n// pans and zooms; this block owns the tree, the layout and the keys). The\n// tree helpers are plain functions and run anywhere, the server included.\nexport { MindMap } from \"./mind-map\";\nexport {\n BRANCH_COLORS,\n normalizeMap,\n createTopic,\n findTopic,\n pathTo,\n parentOf,\n countTopics,\n updateTopic,\n insertTopic,\n insertSibling,\n removeTopic,\n moveTopic,\n shiftTopic,\n indentTopic,\n outdentTopic,\n duplicateTopic,\n setAllCollapsed,\n layoutMindMap,\n mindMapToBoard,\n mindMapToText,\n mindMapToMarkdown,\n outlineToMindMap,\n outlineToTopics,\n mindMapTemplates,\n} from \"./tree\";\n"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"path": "registry/blocks/mind-map/mind-map.jsx",
|
|
21
|
+
"type": "registry:component",
|
|
22
|
+
"target": "@components/makersclaw/blocks/mind-map/mind-map.jsx",
|
|
23
|
+
"content": "\"use client\";\n\n// MindMap — a mind map you can drive from the keyboard alone, on\n// FlowCanvas. The centre topic, main branches balanced on both sides, each\n// branch a tidy tree in its own chart hue. Tab adds a subtopic, Enter the\n// next topic, typing renames, arrows walk the map, ⌘↑/⌘↓ fold; drag a\n// topic onto another to move its branch there.\n//\n// The map is a tree of topics (tree.js), and this component owns it: every\n// change makes a new tree, pushes the old one on the undo stack, lays the\n// tree out again and hands the canvas the result as its `value`. The\n// canvas runs read-only (its own tools, stickies and undo never see this\n// map); pointer and keys come through this component instead, and the\n// canvas pans, zooms, fits and draws.\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { Button, Icon, IconButton, Kbd, Menu, Popover, Separator, Sheet, ToggleGroup, Tooltip, cx, toast } from \"../../ui\";\nimport { ColorDot, FlowCanvas, Palette, useFlowCanvas } from \"../flow-canvas\";\nimport { MindContext, NODE_TYPES } from \"./topic\";\nimport {\n BRANCH_COLORS,\n countTopics,\n createTopic,\n duplicateTopic,\n estimateText,\n findTopic,\n indentTopic,\n insertSibling,\n insertTopic,\n isWithin,\n mindMapTemplates,\n mindMapToBoard,\n mindMapToMarkdown,\n mindMapToText,\n moveTopic,\n normalizeMap,\n outdentTopic,\n outlineToTopics,\n parentOf,\n pathTo,\n removeTopic,\n setAllCollapsed,\n shiftTopic,\n updateTopic,\n} from \"./tree\";\n\nconst HISTORY = 100;\nconst EDITING_TARGET = 'input,textarea,select,[contenteditable=\"true\"],[contenteditable=\"plaintext-only\"],[role=\"dialog\"],[role=\"menu\"],[popover],button,a[href]';\nconst EXTRA_COLORS = [\"warning\", \"success\", \"info\", \"destructive\"];\nconst isMac = () => typeof navigator !== \"undefined\" && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent || \"\");\n\n// Measures words in the map's own font (a canvas's measureText), so a\n// topic's box fits its words exactly. Until the page is there (the\n// server), a guess. Measured again once mounted (the font of the element\n// the map sits in, not the body's) and when web fonts finish loading.\nfunction useMeasure(hostRef) {\n const [tick, setTick] = useState(0);\n const r = useRef({ ctx: null, family: null, cache: new Map() });\n useEffect(() => {\n let live = true;\n const again = () => {\n if (!live) return;\n r.current.cache.clear();\n r.current.family = null;\n setTick((t) => t + 1);\n };\n again();\n document.fonts?.ready?.then(again);\n document.fonts?.addEventListener?.(\"loadingdone\", again);\n return () => {\n live = false;\n document.fonts?.removeEventListener?.(\"loadingdone\", again);\n };\n }, []);\n return useCallback(\n (text, style) => {\n if (typeof document === \"undefined\") return estimateText(text, style);\n const c = r.current;\n if (!c.ctx) c.ctx = document.createElement(\"canvas\").getContext(\"2d\");\n if (!c.ctx) return estimateText(text, style);\n if (!c.family) {\n const cs = getComputedStyle(hostRef.current ?? document.body);\n c.family = cs.fontFamily || \"system-ui, sans-serif\";\n // A theme's tracking widens the words too (canvas text has it in\n // current browsers; elsewhere the +2px slack covers a little).\n if (\"letterSpacing\" in c.ctx) c.ctx.letterSpacing = cs.letterSpacing === \"normal\" ? \"0px\" : cs.letterSpacing;\n }\n const key = `${style.weight}|${style.font}|${text}`;\n let w = c.cache.get(key);\n if (w === undefined) {\n c.ctx.font = `${style.weight} ${style.font}px ${c.family}`;\n w = c.ctx.measureText(text).width;\n if (c.cache.size > 4000) c.cache.clear();\n c.cache.set(key, w);\n }\n return w;\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [tick],\n );\n}\n\nfunction useCoarsePointer() {\n const [coarse, setCoarse] = useState(false);\n useEffect(() => {\n const mq = window.matchMedia(\"(pointer: coarse)\");\n const on = () => setCoarse(mq.matches);\n on();\n mq.addEventListener(\"change\", on);\n return () => mq.removeEventListener(\"change\", on);\n }, []);\n return coarse;\n}\n\nasync function settle(fn, ...args) {\n try {\n const res = await fn(...args);\n return res && res.ok === false ? { state: \"error\", error: res.error || \"Couldn't save\" } : { state: \"saved\" };\n } catch (e) {\n return { state: \"error\", error: e?.message || \"Couldn't save\" };\n }\n}\n\nexport function MindMap({\n value,\n defaultValue,\n onChange,\n onSave,\n changeDelay = 400,\n readOnly = false,\n colors = BRANCH_COLORS,\n templates,\n placeholder = \"New topic\",\n height = 600,\n label = \"Mind map\",\n onSelectionChange,\n className,\n}) {\n const boxRef = useRef(null);\n const measure = useMeasure(boxRef);\n const coarse = useCoarsePointer();\n const canvas = useFlowCanvas();\n const cbs = useRef({});\n cbs.current = { onChange, onSave, onSelectionChange, changeDelay, readOnly };\n\n // ── the tree and its history ─────────────────────────────────────────\n const [tree, setTreeState] = useState(() => normalizeMap(value ?? defaultValue));\n const treeRef = useRef(tree);\n const setTree = (next) => {\n treeRef.current = next;\n setTreeState(next);\n };\n const [hist, setHistState] = useState({ past: [], future: [] });\n const histRef = useRef(hist);\n const setHist = (h) => {\n histRef.current = h;\n setHistState(h);\n };\n\n const [selectedId, setSelectedState] = useState(null);\n const selectedRef = useRef(null);\n const [editing, setEditingState] = useState(null); // { id, isNew, before, prevText }\n const editingRef = useRef(null);\n const setEditing = (e) => {\n editingRef.current = e;\n setEditingState(e);\n };\n const select = useCallback((id) => {\n selectedRef.current = id;\n setSelectedState(id);\n }, []);\n\n // ── saving ───────────────────────────────────────────────────────────\n const [save, setSave] = useState({ state: \"idle\" });\n const timer = useRef(0);\n const dirty = useRef(false);\n const seq = useRef(0);\n const lastEmitted = useRef(null);\n const runSave = async (t) => {\n const fn = cbs.current.onSave;\n if (!fn) return;\n const n = ++seq.current;\n setSave({ state: \"saving\" });\n const res = await settle(fn, t);\n if (n === seq.current) setSave(res);\n };\n const emitNow = () => {\n clearTimeout(timer.current);\n if (!dirty.current) return;\n dirty.current = false;\n const t = treeRef.current;\n lastEmitted.current = t;\n cbs.current.onChange?.(t);\n runSave(t);\n };\n const scheduleEmit = () => {\n if (cbs.current.readOnly) return;\n dirty.current = true;\n clearTimeout(timer.current);\n timer.current = setTimeout(emitNow, cbs.current.changeDelay ?? 400);\n };\n useEffect(\n () => () => {\n if (dirty.current) emitNow();\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [],\n );\n\n // A change the person made: the old tree goes on the undo stack.\n const commit = (next, { push = true } = {}) => {\n const cur = treeRef.current;\n if (next === cur) return false;\n if (push) setHist({ past: [...histRef.current.past, cur].slice(-HISTORY), future: [] });\n setTree(next);\n scheduleEmit();\n return true;\n };\n\n // Controlled: a `value` that is not our own echo replaces the map (and\n // can be undone).\n const lastProp = useRef(value);\n useEffect(() => {\n if (value === undefined || value === lastProp.current) return;\n lastProp.current = value;\n if (value === lastEmitted.current) return;\n const next = normalizeMap(value);\n setHist({ past: [...histRef.current.past, treeRef.current].slice(-HISTORY), future: [] });\n setTree(next);\n if (selectedRef.current && !findTopic(next, selectedRef.current)) select(null);\n if (editingRef.current) setEditing(null);\n }, [value, select]);\n\n // ── what the canvas draws ────────────────────────────────────────────\n const board = useMemo(() => mindMapToBoard(tree, { measure, colors }), [tree, measure, colors]);\n const sizes = useMemo(() => new Map(board.nodes.map((n) => [n.id, n])), [board]);\n\n // Fit the map in view once its words are measured. The canvas fits only\n // on its own first mount, before the topics are sized in the map's font\n // (and again when web fonts land), so without this the map opened off to\n // the top-left of an empty-looking canvas (2026-09-24).\n useEffect(() => {\n // Two frames after a short settle: the canvas syncs the new sizes from\n // `value` in an effect of its own, and fits against what it holds.\n let raf = 0;\n const t = setTimeout(() => {\n raf = requestAnimationFrame(() => (raf = requestAnimationFrame(() => canvas.current?.fitView({ duration: 0 }))));\n }, 300);\n return () => (clearTimeout(t), cancelAnimationFrame(raf));\n }, [measure]);\n\n // The canvas pans with a plain drag (its hand tool): a mind map has\n // nothing to lasso.\n const hand = useCallback(() => {\n const api = canvas.current;\n if (api && api.getTool?.() !== \"hand\") api.setTool(\"hand\");\n }, [canvas]);\n useEffect(() => {\n let raf = 0;\n let tries = 0;\n const wait = () => {\n if (canvas.current) return hand();\n if (tries++ < 120) raf = requestAnimationFrame(wait);\n };\n wait();\n return () => cancelAnimationFrame(raf);\n }, [canvas, hand]);\n\n // Keep a topic in view: centre it at the zoom there is, only when it is\n // (partly) off screen.\n const ensureVisible = useCallback(\n (id, tries = 3) => {\n const host = boxRef.current;\n const el = host?.querySelector(`[data-topic=\"${CSS.escape(id)}\"]`);\n if (!el) {\n if (tries > 0) setTimeout(() => ensureVisible(id, tries - 1), 60);\n return;\n }\n const r = el.getBoundingClientRect();\n const c = host.getBoundingClientRect();\n const pad = 32;\n if (r.left >= c.left + pad && r.right <= c.right - pad && r.top >= c.top + pad + 8 && r.bottom <= c.bottom - pad - 40) return;\n const w = sizes.get(id)?.width;\n const zoom = w ? r.width / w : 1;\n canvas.current?.fitView({ nodes: [{ id }], minZoom: zoom, maxZoom: zoom, padding: 0.5 });\n },\n [canvas, sizes],\n );\n\n // ── editing words ────────────────────────────────────────────────────\n const setText = useCallback((id, text) => {\n const next = updateTopic(treeRef.current, id, { text });\n setTree(next);\n scheduleEmit();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const startEdit = useCallback(\n (id, { replace, isNew = false, before } = {}) => {\n if (readOnly) return;\n const cur = treeRef.current;\n const t = findTopic(cur, id);\n if (!t) return;\n select(id);\n const ed = { id, isNew, before: before ?? cur, prevText: t.text };\n if (replace !== undefined) setTree(updateTopic(cur, id, { text: replace }));\n setEditing(ed);\n },\n [readOnly, select],\n );\n\n /** End the words' edit. Empty: a topic just made goes away (as if it\n * never was), an old one gets its words back. */\n const finishEdit = useCallback((id) => {\n const ed = editingRef.current;\n if (!ed || (id && ed.id !== id)) return;\n setEditing(null);\n const cur = treeRef.current;\n const t = findTopic(cur, ed.id);\n if (!t) return;\n const text = String(t.text ?? \"\").replace(/\\s+$/, \"\").replace(/^\\s+/, \"\");\n if (!text) {\n if (ed.isNew && ed.id !== cur.id) {\n const prev = neighbourAfterRemove(cur, ed.id);\n setTree(removeTopic(cur, ed.id));\n const past = histRef.current.past;\n if (past[past.length - 1] === ed.before) setHist({ past: past.slice(0, -1), future: histRef.current.future });\n select(prev);\n } else {\n setTree(updateTopic(cur, ed.id, { text: ed.prevText }));\n }\n scheduleEmit();\n return;\n }\n if (text !== t.text) setTree(updateTopic(cur, ed.id, { text }));\n if (!ed.isNew && text !== ed.prevText) setHist({ past: [...histRef.current.past, ed.before].slice(-HISTORY), future: [] });\n scheduleEmit();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // ── changing the map ─────────────────────────────────────────────────\n const addChild = useCallback(\n (parentId) => {\n if (readOnly) return;\n finishEdit();\n const cur = treeRef.current;\n if (!findTopic(cur, parentId)) return;\n const t = createTopic(\"\");\n commit(insertTopic(cur, parentId, t));\n startEdit(t.id, { isNew: true, before: cur });\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [readOnly, finishEdit, startEdit],\n );\n\n const addSibling = useCallback(\n (id, { before = false } = {}) => {\n if (readOnly) return;\n finishEdit();\n const cur = treeRef.current;\n if (id === cur.id) return addChild(id);\n const t = createTopic(\"\");\n commit(insertSibling(cur, id, t, { before }));\n startEdit(t.id, { isNew: true, before: cur });\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [readOnly, finishEdit, startEdit, addChild],\n );\n\n const remove = useCallback(\n (id) => {\n if (readOnly) return;\n const cur = treeRef.current;\n const t = findTopic(cur, id);\n if (!t || id === cur.id) return;\n setEditing(null);\n const next = neighbourAfterRemove(cur, id);\n commit(removeTopic(cur, id));\n select(next);\n const n = countTopics(t);\n toast(`Deleted “${clip(t.text) || \"Untitled\"}”${n ? ` and ${n} under it` : \"\"}`, { action: { label: \"Undo\", onClick: () => undoRef.current?.() } });\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [readOnly, select],\n );\n\n const toggleFold = useCallback(\n (id, want) => {\n const cur = treeRef.current;\n const t = findTopic(cur, id);\n if (!t || !t.children?.length || id === cur.id) return;\n const on = want ?? !t.collapsed;\n if (Boolean(t.collapsed) === on) return;\n if (readOnly) setTree(updateTopic(cur, id, { collapsed: on }));\n else commit(updateTopic(cur, id, { collapsed: on }));\n // A fold under the selection hides it: the selection moves up.\n const sel = selectedRef.current;\n if (on && sel && sel !== id && isWithin(cur, id, sel)) select(id);\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [readOnly, select],\n );\n\n const setColor = (id, color) => {\n const cur = treeRef.current;\n if (!findTopic(cur, id) || id === cur.id) return;\n commit(updateTopic(cur, id, { color }));\n };\n\n const restructure = (fn, id, { keepEditing = false } = {}) => {\n const cur = treeRef.current;\n const next = fn(cur, id);\n if (next === cur) return;\n if (keepEditing && editingRef.current?.isNew) {\n // A topic still being made: its creation is the undo step.\n setTree(next);\n scheduleEmit();\n } else commit(next);\n select(id);\n };\n\n const undo = () => {\n finishEdit();\n const h = histRef.current;\n if (!h.past.length) return;\n const prev = h.past[h.past.length - 1];\n setHist({ past: h.past.slice(0, -1), future: [treeRef.current, ...h.future].slice(0, HISTORY) });\n setTree(prev);\n if (selectedRef.current && !findTopic(prev, selectedRef.current)) select(null);\n scheduleEmit();\n say(\"Undone\");\n };\n const redo = () => {\n finishEdit();\n const h = histRef.current;\n if (!h.future.length) return;\n const [next, ...future] = h.future;\n setHist({ past: [...h.past, treeRef.current].slice(-HISTORY), future });\n setTree(next);\n if (selectedRef.current && !findTopic(next, selectedRef.current)) select(null);\n scheduleEmit();\n say(\"Redone\");\n };\n const undoRef = useRef(undo);\n undoRef.current = undo;\n\n const loadMap = (map) => {\n finishEdit();\n commit(normalizeMap(map));\n select(null);\n setTimeout(() => canvas.current?.fitView(), 80);\n };\n\n // ── announcements ────────────────────────────────────────────────────\n const [announce, setAnnounce] = useState(\"\");\n const say = (msg) => setAnnounce((m) => (m === msg ? `${msg} ` : msg));\n\n // Selection: tell the page, keep it in view, and keep the keyboard's\n // focus on it (so a reader hears it).\n useEffect(() => {\n const t = selectedId ? findTopic(treeRef.current, selectedId) : null;\n cbs.current.onSelectionChange?.(t ?? null);\n if (!selectedId || editing) return;\n if (t) {\n const path = pathTo(treeRef.current, selectedId);\n const kids = t.children?.length ?? 0;\n say(`${t.text || \"Untitled\"}${path.length > 1 ? `, level ${path.length - 1}` : \", centre\"}${kids ? `, ${kids} subtopic${kids === 1 ? \"\" : \"s\"}${t.collapsed ? \", folded\" : \"\"}` : \"\"}`);\n }\n const raf = requestAnimationFrame(() => {\n const host = boxRef.current;\n if (!host) return;\n const el = host.querySelector(`.react-flow__node[data-id=\"${CSS.escape(selectedId)}\"]`);\n if (el && host.contains(document.activeElement) && document.activeElement !== el && !document.activeElement?.closest?.(\"[role=textbox]\")) el.focus({ preventScroll: true });\n ensureVisible(selectedId);\n });\n return () => cancelAnimationFrame(raf);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [selectedId, editing]);\n useEffect(() => {\n if (editing) ensureVisible(editing.id);\n }, [editing, ensureVisible]);\n\n // ── moving with the keys ─────────────────────────────────────────────\n const itemsById = useMemo(() => new Map(board.nodes.map((n) => [n.id, n])), [board]);\n const navigate = (key) => {\n const cur = treeRef.current;\n const id = selectedRef.current;\n const me = itemsById.get(id);\n if (!me) return select(cur.id);\n const side = me.data.side;\n const cy = me.position.y + me.height / 2;\n 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];\n const t = findTopic(cur, id);\n const kidsOn = (s) => (t.children ?? []).map((c) => itemsById.get(c.id)).filter((n) => n && (s === undefined || n.data.side === s));\n const outward = side === \"left\" ? \"ArrowLeft\" : \"ArrowRight\";\n if (side === \"root\") {\n if (key === \"ArrowRight\" || key === \"ArrowLeft\") {\n const hit = nearest(kidsOn(key === \"ArrowRight\" ? \"right\" : \"left\"));\n if (hit) select(hit.id);\n }\n return;\n }\n if (key === outward) {\n if (t.collapsed && t.children?.length) {\n toggleFold(id, false);\n select(t.children[0].id);\n return;\n }\n const hit = nearest(kidsOn());\n if (hit) select(hit.id);\n return;\n }\n if (key === \"ArrowLeft\" || key === \"ArrowRight\") return select(parentOf(cur, id)?.id ?? cur.id);\n // Up and down: the nearest topic above or below in the same column\n // (same side, same depth) — a sibling, or a cousin across a gap.\n const down = key === \"ArrowDown\";\n const column = board.nodes.filter((n) => n.id !== id && n.data.side === side && n.data.depth === me.data.depth);\n const cand = column\n .map((n) => ({ n, d: n.position.y + n.height / 2 - cy }))\n .filter(({ d }) => (down ? d > 1 : d < -1))\n .sort((a, b) => Math.abs(a.d) - Math.abs(b.d));\n if (cand[0]) select(cand[0].n.id);\n };\n\n // ── the keyboard ─────────────────────────────────────────────────────\n const [help, setHelp] = useState(false);\n const onKeyDownCapture = (e) => {\n if (e.nativeEvent.isComposing) return;\n const target = e.target;\n const k = e.key;\n const mod = e.metaKey || e.ctrlKey;\n const done = () => {\n e.preventDefault();\n e.stopPropagation();\n };\n const cur = treeRef.current;\n const ed = editingRef.current;\n\n // Typing a topic's words.\n if (ed && target?.closest?.(\"[role=textbox]\")) {\n const t = findTopic(cur, ed.id);\n const empty = !String(t?.text ?? \"\").trim();\n const isRoot = ed.id === cur.id;\n if (k === \"Enter\" && !e.shiftKey) {\n done();\n if (mod || isRoot) return finishEdit();\n if (empty && ed.isNew) {\n // An empty new line: step out a level, or stop.\n if (pathTo(cur, ed.id).length >= 3) return restructure(outdentTopic, ed.id, { keepEditing: true });\n return finishEdit();\n }\n finishEdit();\n return addSibling(ed.id);\n }\n if (k === \"Tab\") {\n done();\n if (isRoot) {\n finishEdit();\n return addChild(cur.id);\n }\n if (e.shiftKey) return restructure(outdentTopic, ed.id, { keepEditing: true });\n if (empty && ed.isNew) return restructure(indentTopic, ed.id, { keepEditing: true });\n finishEdit();\n return addChild(ed.id);\n }\n if (k === \"Escape\") {\n done();\n return finishEdit();\n }\n if (k === \"Backspace\" && empty && !isRoot && !(t?.children?.length)) {\n done();\n const prev = neighbourAfterRemove(cur, ed.id, { preferBefore: true });\n setEditing(null);\n if (ed.isNew) {\n setTree(removeTopic(cur, ed.id));\n const past = histRef.current.past;\n if (past[past.length - 1] === ed.before) setHist({ past: past.slice(0, -1), future: histRef.current.future });\n scheduleEmit();\n } else commit(removeTopic(cur, ed.id));\n if (prev && prev !== cur.id) startEdit(prev);\n else select(prev);\n return;\n }\n return;\n }\n if (target?.closest?.(EDITING_TARGET)) return;\n\n const sel = selectedRef.current && findTopic(cur, selectedRef.current) ? selectedRef.current : null;\n const low = k.length === 1 ? k.toLowerCase() : k;\n\n if (k === \"?\" && !mod) return done(), setHelp(true);\n if (mod && low === \"z\" && !readOnly) return done(), e.shiftKey ? redo() : undo();\n if (mod && low === \"y\" && !readOnly) return done(), redo();\n if (k === \"Escape\") {\n if (sel) return done(), select(null);\n setTimeout(hand, 0);\n return;\n }\n if (!sel) {\n if (k.startsWith(\"Arrow\") || k === \"Enter\" || k === \"Home\") return done(), select(cur.id);\n return;\n }\n if (k === \"Home\") return done(), select(cur.id);\n if (k.startsWith(\"Arrow\")) {\n if (mod && (k === \"ArrowUp\" || k === \"ArrowDown\")) return done(), toggleFold(sel, k === \"ArrowUp\");\n if (e.altKey && (k === \"ArrowUp\" || k === \"ArrowDown\")) return done(), !readOnly && restructure((r, id) => shiftTopic(r, id, k === \"ArrowUp\" ? -1 : 1), sel);\n if (mod || e.altKey) return;\n return done(), navigate(k);\n }\n if (readOnly) {\n if (k === \" \" || k === \"Enter\") return done(), toggleFold(sel);\n return;\n }\n if (k === \"Tab\") return done(), e.shiftKey ? restructure(outdentTopic, sel) : addChild(sel);\n if (k === \"Enter\") return done(), addSibling(sel, { before: e.shiftKey });\n if (k === \" \" || k === \"F2\") return done(), startEdit(sel);\n if (k === \"Delete\" || k === \"Backspace\") return done(), remove(sel);\n if (mod && low === \"d\") {\n done();\n const r = duplicateTopic(cur, sel);\n if (r.id) {\n commit(r.root);\n select(r.id);\n }\n return;\n }\n // ⇧1 and ⇧2 stay the canvas's (fit, zoom to selection).\n if (e.shiftKey && (e.code === \"Digit1\" || e.code === \"Digit2\")) return;\n if (!mod && !e.altKey && k.length === 1) return done(), startEdit(sel, { replace: k });\n };\n\n // Copy, cut, paste: a branch as an outline, and an outline (from notes,\n // a doc, an agent) as subtopics of the selection.\n useEffect(() => {\n const here = () => {\n const a = document.activeElement;\n return boxRef.current?.contains(a) && !a?.closest?.(EDITING_TARGET) && !a?.closest?.(\"[role=textbox]\");\n };\n const copy = (e) => {\n const id = selectedRef.current;\n const t = id && findTopic(treeRef.current, id);\n if (!here() || !t) return false;\n e.clipboardData?.setData(\"text/plain\", mindMapToText(t));\n e.preventDefault();\n e.stopPropagation();\n return true;\n };\n const onCopy = (e) => {\n if (copy(e)) say(\"Copied as an outline\");\n };\n const onCut = (e) => {\n if (readOnly) return;\n const id = selectedRef.current;\n if (id && id !== treeRef.current.id && copy(e)) remove(id);\n };\n const onPaste = (e) => {\n if (!here() || readOnly) return;\n const text = e.clipboardData?.getData(\"text/plain\") ?? \"\";\n const topics = outlineToTopics(text).slice(0, 200);\n e.preventDefault();\n e.stopPropagation();\n if (!topics.length) return;\n const cur = treeRef.current;\n const parent = selectedRef.current && findTopic(cur, selectedRef.current) ? selectedRef.current : cur.id;\n let next = cur;\n for (const t of topics) next = insertTopic(next, parent, t);\n commit(next);\n select(topics[0].id);\n say(`${topics.length} topic${topics.length === 1 ? \"\" : \"s\"} pasted`);\n };\n window.addEventListener(\"copy\", onCopy, true);\n window.addEventListener(\"cut\", onCut, true);\n window.addEventListener(\"paste\", onPaste, true);\n return () => {\n window.removeEventListener(\"copy\", onCopy, true);\n window.removeEventListener(\"cut\", onCut, true);\n window.removeEventListener(\"paste\", onPaste, true);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [readOnly]);\n\n // ── the pointer: select, drag a topic onto another ───────────────────\n const [drag, setDrag] = useState(null); // { id, text, x, y }\n const [dropId, setDropId] = useState(null);\n const onTopicPointerDown = useCallback(\n (e, id) => {\n if (editingRef.current && editingRef.current.id !== id) finishEdit();\n select(id);\n const cur = treeRef.current;\n if (readOnly || id === cur.id || e.pointerType !== \"mouse\") return;\n const start = { x: e.clientX, y: e.clientY };\n let active = false;\n let over = null;\n const t = findTopic(cur, id);\n const move = (ev) => {\n if (!active && Math.hypot(ev.clientX - start.x, ev.clientY - start.y) < 5) return;\n active = true;\n const host = boxRef.current?.getBoundingClientRect();\n if (!host) return;\n const hit = document\n .elementsFromPoint(ev.clientX, ev.clientY)\n .map((el) => el.closest?.(\"[data-topic]\"))\n .find((el) => el && el.dataset.topic !== id);\n const tid = hit?.dataset.topic ?? null;\n over = tid && !isWithin(treeRef.current, id, tid) && parentOf(treeRef.current, id)?.id !== tid ? tid : null;\n setDropId(over);\n setDrag({ id, text: t?.text || \"Untitled\", x: ev.clientX - host.left, y: ev.clientY - host.top });\n };\n const up = () => {\n window.removeEventListener(\"pointermove\", move);\n window.removeEventListener(\"pointerup\", up);\n window.removeEventListener(\"pointercancel\", up);\n if (!active) return;\n setDrag(null);\n setDropId(null);\n if (!over) return;\n const cur2 = treeRef.current;\n const next = moveTopic(cur2, id, over);\n if (next !== cur2) {\n commit(next);\n select(id);\n const to = findTopic(next, over);\n say(`Moved under ${to?.text || \"Untitled\"}`);\n }\n };\n window.addEventListener(\"pointermove\", move);\n window.addEventListener(\"pointerup\", up);\n window.addEventListener(\"pointercancel\", up);\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [readOnly, finishEdit, select],\n );\n\n // A click on the empty canvas (not a pan) clears the selection.\n const paneDown = useRef(null);\n const onPointerDownCapture = (e) => {\n paneDown.current = e.target?.classList?.contains(\"react-flow__pane\") ? { x: e.clientX, y: e.clientY } : null;\n };\n const onPointerUpCapture = (e) => {\n const p = paneDown.current;\n paneDown.current = null;\n if (!p || Math.hypot(e.clientX - p.x, e.clientY - p.y) > 4) return;\n finishEdit();\n select(null);\n };\n\n // ── the context the topics read ──────────────────────────────────────\n const ctx = useMemo(\n () => ({\n selectedId,\n editingId: editing?.id ?? null,\n dropId,\n dragId: drag?.id ?? null,\n readOnly,\n coarse,\n placeholder,\n onTopicPointerDown,\n startEdit,\n setText,\n finishEdit,\n toggleFold,\n addChild,\n }),\n [selectedId, editing, dropId, drag, readOnly, coarse, placeholder, onTopicPointerDown, startEdit, setText, finishEdit, toggleFold, addChild],\n );\n\n // ── the chrome ───────────────────────────────────────────────────────\n const [view, setView] = useState(\"map\");\n const selTopic = selectedId ? findTopic(tree, selectedId) : null;\n const selIsRoot = selTopic?.id === tree.id;\n const selColor = selectedId ? itemsById.get(selectedId)?.data.color : null;\n const empty = !(tree.children?.length ?? 0);\n const palette = useMemo(() => [...new Set([...colors, ...EXTRA_COLORS])], [colors]);\n const starters = templates ?? mindMapTemplates();\n const hint = editing\n ? [[\"Enter\", \"next topic\"], [\"Tab\", \"subtopic\"], [\"Esc\", \"done\"]]\n : selTopic\n ? readOnly\n ? [[\"Arrows\", \"move\"], [\"Space\", selTopic.collapsed ? \"unfold\" : \"fold\"]]\n : [[\"Tab\", \"subtopic\"], [\"Enter\", \"topic\"], [\"Space\", \"rename\"], [\"⌫\", \"delete\"]]\n : [[\"Click\", \"a topic\"], [\"Drag\", \"to pan\"], [\"?\", \"shortcuts\"]];\n const copyMarkdown = async () => {\n try {\n await navigator.clipboard.writeText(mindMapToMarkdown(treeRef.current));\n toast.success(\"Copied as Markdown\");\n } catch {\n toast.error(\"Couldn't copy\", { description: \"Your browser kept the clipboard closed.\" });\n }\n };\n const sized = /(^|\\s)(h-|min-h-|size-|flex-1|grow)/.test(className ?? \"\");\n\n return (\n <section\n aria-label={label}\n className={cx(\"flex flex-col overflow-hidden rounded-lg border border-border bg-background text-foreground\", className)}\n style={sized ? undefined : { height }}\n >\n <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\">\n {!readOnly ? (\n <>\n <Tooltip label={`Undo · ${isMac() ? \"⌘\" : \"Ctrl+\"}Z`}>\n <IconButton icon=\"undo\" label=\"Undo\" size=\"sm\" disabled={!hist.past.length} onClick={undo} />\n </Tooltip>\n <Tooltip label={`Redo · ${isMac() ? \"⇧⌘Z\" : \"Ctrl+Shift+Z\"}`}>\n <IconButton icon=\"redo\" label=\"Redo\" size=\"sm\" disabled={!hist.future.length} onClick={redo} />\n </Tooltip>\n <SaveState save={save} onRetry={() => runSave(treeRef.current)} />\n </>\n ) : (\n <p className=\"px-2 text-sm font-medium\">{clip(tree.text, 48) || \"Mind map\"}</p>\n )}\n <div className=\"flex-1\" />\n {view === \"map\" && !readOnly ? (\n <div role=\"toolbar\" aria-label=\"Topic\" className=\"flex items-center gap-1\">\n <Tooltip label={selTopic && !selIsRoot ? \"Add a topic below · Enter\" : \"Add a main topic\"}>\n <Button size=\"sm\" variant=\"quiet\" icon=\"plus\" onClick={() => (selTopic ? addSibling(selTopic.id) : addChild(tree.id))} aria-label=\"Add topic\">\n <span className=\"max-sm:hidden\">Topic</span>\n </Button>\n </Tooltip>\n <Tooltip label=\"Add a subtopic · Tab\">\n <Button size=\"sm\" variant=\"quiet\" icon=\"git-branch\" onClick={() => addChild(selTopic?.id ?? tree.id)} aria-label=\"Add subtopic\">\n <span className=\"max-sm:hidden\">Subtopic</span>\n </Button>\n </Tooltip>\n <Separator orientation=\"vertical\" className=\"mx-0.5 h-5 max-sm:hidden\" />\n <Popover\n label=\"Branch colour\"\n width=\"auto\"\n className=\"p-1.5\"\n trigger={(p) => (\n <Button {...p} variant=\"quiet\" size=\"sm\" iconOnly aria-label=\"Branch colour\" title=\"Branch colour\" disabled={!selTopic || selIsRoot}>\n <ColorDot color={selColor ?? \"muted\"} />\n </Button>\n )}\n >\n <Palette colors={palette} value={selTopic?.color ?? selColor} swatch=\"dot\" onPick={(c) => selTopic && setColor(selTopic.id, c)} label=\"Branch colour\" />\n {selTopic?.color ? (\n <Button variant=\"quiet\" size=\"sm\" className=\"mt-1 w-full\" onClick={() => setColor(selTopic.id, undefined)}>\n Use the branch's colour\n </Button>\n ) : null}\n </Popover>\n <Tooltip label={selTopic?.collapsed ? \"Unfold · ⌘↓\" : \"Fold · ⌘↑\"}>\n <IconButton\n icon={selTopic?.collapsed ? \"eye\" : \"eye-off\"}\n label={selTopic?.collapsed ? \"Unfold branch\" : \"Fold branch\"}\n size=\"sm\"\n disabled={!selTopic || selIsRoot || !selTopic.children?.length}\n onClick={() => toggleFold(selTopic.id)}\n />\n </Tooltip>\n <Tooltip label=\"Delete · ⌫\">\n <IconButton icon=\"trash\" label=\"Delete topic\" size=\"sm\" disabled={!selTopic || selIsRoot} onClick={() => remove(selTopic.id)} />\n </Tooltip>\n </div>\n ) : null}\n <ToggleGroup\n size=\"sm\"\n className=\"max-sm:hidden\"\n aria-label=\"View\"\n value={view}\n onChange={setView}\n items={[\n { value: \"map\", label: \"Map\", icon: \"git-branch\", iconOnly: true },\n { value: \"outline\", label: \"Outline\", icon: \"list\", iconOnly: true },\n ]}\n />\n <Menu\n label=\"Map actions\"\n items={[\n { label: \"Unfold everything\", icon: \"eye\", onSelect: () => (readOnly ? setTree(setAllCollapsed(treeRef.current, false)) : commit(setAllCollapsed(treeRef.current, false))) },\n { label: \"Fold every branch\", icon: \"eye-off\", onSelect: () => (readOnly ? setTree(setAllCollapsed(treeRef.current, true)) : commit(setAllCollapsed(treeRef.current, true))) },\n { label: \"Fit to screen\", icon: \"maximize\", shortcut: \"⇧1\", onSelect: () => canvas.current?.fitView() },\n { type: \"checkbox\", label: \"Outline view\", checked: view === \"outline\", onCheckedChange: (on) => setView(on ? \"outline\" : \"map\") },\n { type: \"separator\" },\n { label: \"Copy as Markdown\", icon: \"copy\", onSelect: copyMarkdown },\n { label: \"Keyboard shortcuts\", icon: \"keyboard\", shortcut: \"?\", onSelect: () => setHelp(true) },\n ]}\n />\n </header>\n\n <div\n ref={boxRef}\n className=\"relative min-h-0 flex-1\"\n onKeyDownCapture={onKeyDownCapture}\n onPointerDownCapture={onPointerDownCapture}\n onPointerUpCapture={onPointerUpCapture}\n >\n <p className=\"sr-only\" aria-live=\"polite\">\n {announce}\n </p>\n <div className=\"absolute inset-0\" inert={view !== \"map\" || undefined} aria-hidden={view !== \"map\" || undefined}>\n <MindContext.Provider value={ctx}>\n <FlowCanvas\n value={board}\n readOnly\n nodeTypes={NODE_TYPES}\n canvasRef={canvas}\n label={label}\n height=\"100%\"\n bare\n emptyState={false}\n templates={[]}\n describeNode={describeTopic}\n className=\"[&_.react-flow__node]:transition-transform [&_.react-flow__node]:duration-200 [&_.react-flow__node]:ease-out motion-reduce:[&_.react-flow__node]:transition-none\"\n />\n </MindContext.Provider>\n </div>\n\n {view === \"map\" && drag ? (\n <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 }}>\n <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>\n <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>\n </div>\n ) : null}\n\n {view === \"map\" && empty && !readOnly && !editing ? (\n <Starter templates={starters} onPick={(t) => loadMap(typeof t.map === \"function\" ? t.map() : t.map)} onStart={() => addChild(tree.id)} />\n ) : null}\n\n {view === \"map\" ? (\n <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\">\n {hint.map(([key, what]) => (\n <span key={key} className=\"flex items-center gap-1.5 whitespace-nowrap\">\n <Kbd>{key}</Kbd>\n {what}\n </span>\n ))}\n </div>\n ) : null}\n\n {view === \"outline\" ? (\n <OutlineView\n tree={tree}\n items={itemsById}\n onPick={(id) => {\n setView(\"map\");\n // Open the folds on the way down, so the topic can be seen.\n const path = pathTo(treeRef.current, id);\n let next = treeRef.current;\n for (const p of path.slice(0, -1)) if (p.collapsed) next = updateTopic(next, p.id, { collapsed: false });\n if (next !== treeRef.current) (readOnly ? setTree(next) : commit(next));\n select(id);\n }}\n onCopy={copyMarkdown}\n />\n ) : null}\n </div>\n\n <Shortcuts open={help} onClose={() => setHelp(false)} readOnly={readOnly} />\n </section>\n );\n}\n\n// ── pieces ─────────────────────────────────────────────────────────────\n\nconst clip = (s, n = 60) => {\n const t = String(s ?? \"\").replace(/\\s+/g, \" \").trim();\n return t.length > n ? `${t.slice(0, n - 1)}…` : t;\n};\n\nfunction describeTopic(n) {\n if (n.type !== \"topic\") return undefined;\n const d = n.data ?? {};\n return `${d.depth === 0 ? \"Centre\" : `Level ${d.depth}`}: \"${clip(d.text) || \"Untitled\"}\"${d.collapsed ? ` (folded, ${d.hidden} hidden)` : \"\"}`;\n}\n\n/** Where the selection goes when a topic leaves: the next sibling, else\n * the one before, else the parent. */\nfunction neighbourAfterRemove(root, id, { preferBefore = false } = {}) {\n const parent = parentOf(root, id);\n if (!parent) return root.id;\n const kids = parent.children;\n const i = kids.findIndex((c) => c.id === id);\n const order = preferBefore ? [kids[i - 1], kids[i + 1]] : [kids[i + 1], kids[i - 1]];\n return (order.find(Boolean) ?? parent).id;\n}\n\nfunction SaveState({ save, onRetry }) {\n if (save.state === \"error\") {\n return (\n <div role=\"alert\" className=\"flex min-w-0 items-center gap-1 pl-1 text-xs\">\n <Icon name=\"warning\" size={14} className=\"shrink-0 text-destructive\" />\n <span className=\"truncate max-md:sr-only\">{save.error || \"Couldn't save\"}</span>\n <Button variant=\"quiet\" size=\"sm\" onClick={onRetry}>\n Retry\n </Button>\n </div>\n );\n }\n if (save.state === \"idle\") return null;\n const saving = save.state === \"saving\";\n return (\n <p role=\"status\" className=\"flex shrink-0 items-center gap-1 px-1 text-xs text-muted-foreground\">\n <Icon name={saving ? \"loader\" : \"check\"} size={14} className={cx(saving && \"motion-safe:animate-spin\")} />\n <span className=\"max-sm:sr-only\">{saving ? \"Saving…\" : \"Saved\"}</span>\n </p>\n );\n}\n\n/** The first run: how the map works, and a few starting points. */\nfunction Starter({ templates, onPick, onStart }) {\n return (\n <div className=\"pointer-events-none absolute inset-x-3 bottom-3 z-10 flex justify-center md:bottom-14\">\n <div className=\"pointer-events-auto w-full max-w-md rounded-xl border border-border bg-popover p-4 text-popover-foreground shadow-lg\">\n <p className=\"text-sm font-semibold\">Start with the big idea</p>\n <p className=\"mt-1 text-sm text-muted-foreground\">\n Double-click the centre to name it. <Kbd>Tab</Kbd> adds a branch, <Kbd>Enter</Kbd> the next one — no mouse needed.\n </p>\n <div className=\"mt-3 flex flex-wrap gap-2\">\n <Button size=\"sm\" variant=\"primary\" icon=\"plus\" onClick={onStart}>\n Add a branch\n </Button>\n {templates.map((t) => (\n <Tooltip key={t.id} label={t.description ?? t.label}>\n <Button size=\"sm\" icon={t.icon} onClick={() => onPick(t)}>\n {t.label}\n </Button>\n </Tooltip>\n ))}\n </div>\n </div>\n </div>\n );\n}\n\n/** The map as a nested list: the reading of it for a screen reader, and\n * a quick way to jump to a topic. */\nfunction OutlineView({ tree, items, onPick, onCopy }) {\n const rows = [];\n const walk = (t, depth, color) => {\n const c = items.get(t.id)?.data.color ?? color;\n rows.push({ t, depth, color: c });\n for (const k of t.children ?? []) walk(k, depth + 1, c);\n };\n walk(tree, 0, null);\n return (\n <div className=\"absolute inset-0 z-10 overflow-y-auto bg-background\">\n <div className=\"mx-auto grid max-w-2xl gap-4 px-4 py-6 sm:px-6\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-base font-semibold\">{tree.text || \"Untitled\"}</h3>\n <p className=\"text-sm text-muted-foreground\">\n {countTopics(tree)} topic{countTopics(tree) === 1 ? \"\" : \"s\"}. Choose one to find it on the map.\n </p>\n </div>\n <Button size=\"sm\" icon=\"copy\" onClick={onCopy}>\n Copy as Markdown\n </Button>\n </div>\n {rows.length > 1 ? (\n <ul className=\"grid gap-0.5\" aria-label=\"Outline\">\n {rows.slice(1).map(({ t, depth, color }) => (\n <li key={t.id} style={{ paddingLeft: (depth - 1) * 20 }}>\n <Button variant=\"quiet\" size=\"sm\" align=\"start\" className=\"w-full min-w-0\" onClick={() => onPick(t.id)}>\n <ColorDot color={color ?? \"muted\"} className={depth > 1 ? \"size-2\" : \"size-2.5\"} />\n <span className={cx(\"truncate\", depth === 1 ? \"font-medium\" : \"font-normal\")}>{t.text || \"Untitled\"}</span>\n {t.collapsed && t.children?.length ? <span className=\"ml-auto shrink-0 text-xs text-muted-foreground tabular-nums\">{countTopics(t)} folded</span> : null}\n </Button>\n </li>\n ))}\n </ul>\n ) : (\n <div className=\"grid justify-items-center gap-2 rounded-lg border border-dashed border-border px-4 py-10 text-center\">\n <p className=\"text-sm text-muted-foreground\">Only the centre so far. Branches you add show up here as a list.</p>\n </div>\n )}\n </div>\n </div>\n );\n}\n\nconst KEYS = [\n [\"Tab\", \"Add a subtopic\"],\n [\"Enter\", \"Add the next topic (⇧ above)\"],\n [\"Space\", \"Rename (or just start typing)\"],\n [\"⌫\", \"Delete the topic and its branch\"],\n [\"← → ↑ ↓\", \"Move around the map\"],\n [\"⌥↑ ⌥↓\", \"Move the topic up or down\"],\n [\"⇧Tab\", \"Move the topic up a level\"],\n [\"⌘↑ ⌘↓\", \"Fold, unfold\"],\n [\"⌘D\", \"Duplicate the branch\"],\n [\"⌘C ⌘V\", \"Copy a branch, paste an outline\"],\n [\"⌘Z\", \"Undo (⇧⌘Z redo)\"],\n [\"⇧1\", \"Fit the map to the screen\"],\n];\nconst TYPING = [\n [\"Enter\", \"Next topic (an empty one steps out a level)\"],\n [\"Tab\", \"Subtopic (an empty one moves in a level)\"],\n [\"⇧Enter\", \"A new line in the words\"],\n [\"Esc\", \"Done\"],\n];\n\nfunction Shortcuts({ open, onClose, readOnly }) {\n const mac = isMac();\n const key = (k) => (mac ? k : k.replace(/⌘/g, \"Ctrl+\").replace(/⌥/g, \"Alt+\").replace(/⇧/g, \"Shift+\"));\n const rows = readOnly ? [[\"← → ↑ ↓\", \"Move around the map\"], [\"Space\", \"Fold, unfold\"], [\"⇧1\", \"Fit the map to the screen\"]] : KEYS;\n const list = (items) => (\n <dl className=\"grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2 text-sm\">\n {items.map(([k, what]) => (\n <div key={k} className=\"contents\">\n <dt className=\"justify-self-start\">\n <Kbd>{key(k)}</Kbd>\n </dt>\n <dd className=\"text-muted-foreground\">{what}</dd>\n </div>\n ))}\n </dl>\n );\n return (\n <Sheet open={open} onClose={onClose} title=\"Keyboard shortcuts\" description=\"A mind map you can build without the mouse.\">\n <div className=\"grid gap-6\">\n <section className=\"grid gap-3\">\n <h3 className=\"text-sm font-medium\">With a topic selected</h3>\n {list(rows)}\n </section>\n {!readOnly ? (\n <section className=\"grid gap-3\">\n <h3 className=\"text-sm font-medium\">While typing a topic</h3>\n {list(TYPING)}\n </section>\n ) : null}\n </div>\n </Sheet>\n );\n}\n"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"path": "registry/blocks/mind-map/topic.jsx",
|
|
27
|
+
"type": "registry:component",
|
|
28
|
+
"target": "@components/makersclaw/blocks/mind-map/topic.jsx",
|
|
29
|
+
"content": "\"use client\";\n\n// One topic on the map, as a FlowCanvas node type. It draws three things:\n// its box (the centre, a tinted main-branch pill, or words over a coloured\n// line for a subtopic), the curve back to its parent (an SVG that reaches\n// outside the box, so the canvas needs no connectors), and the small fold /\n// add button at its outer edge.\n//\n// Everything it knows about the map — who is selected or being edited,\n// what a click does — comes from MindContext, which MindMap provides\n// around the canvas.\nimport { createContext, useContext } from \"react\";\nimport { Icon, cx } from \"../../ui\";\nimport { EditableText, colorClasses } from \"../flow-canvas\";\nimport { UNDERLINE, branchPath, levelOf } from \"./tree\";\n\nexport const MindContext = createContext(null);\n\nconst STROKE_WIDTH = [0, 3, 2, 1.5];\n\nexport function TopicNode({ id, data }) {\n const m = useContext(MindContext);\n if (!m) return null;\n const { text, depth = 0, side = \"right\", color, kids = 0, collapsed = false, hidden = 0, link } = data ?? {};\n const selected = m.selectedId === id;\n const editing = !m.readOnly && m.editingId === id;\n const isDrop = m.dropId === id;\n const isDragged = m.dragId === id;\n const L = levelOf(depth);\n const cc = colorClasses(color ?? \"muted\");\n const left = side === \"left\";\n\n // Where lines to the children leave: the outer edge (a subtopic's foot).\n const outY = depth >= 2 ? `calc(100% - ${UNDERLINE / 2}px)` : \"50%\";\n const outer = left ? { right: \"100%\" } : { left: \"100%\" };\n\n const box = cx(\n \"relative size-full whitespace-pre-wrap break-words outline-none\",\n \"transition-[box-shadow,background-color,opacity,outline-color] duration-150 motion-reduce:transition-none\",\n m.coarse ? \"\" : \"nopan\",\n editing ? \"cursor-text\" : \"cursor-default\",\n depth === 0 && \"rounded-2xl bg-primary text-center text-primary-foreground shadow-sm\",\n depth === 1 && cx(\"rounded-xl border bg-card text-foreground shadow-xs hover:shadow-sm\", cc.edge, left ? \"text-right\" : \"text-left\"),\n depth >= 2 && cx(\"rounded-md text-foreground hover:bg-muted/70\", left ? \"text-right\" : \"text-left\"),\n selected && \"ring-2 ring-primary ring-offset-2 ring-offset-background\",\n isDrop && \"outline-2 outline-offset-4 outline-primary [outline-style:dashed]\",\n isDragged && \"opacity-40\",\n );\n\n return (\n <div data-topic={id} className=\"group/topic relative size-full\">\n {link ? (\n <svg aria-hidden=\"true\" width=\"1\" height=\"1\" className=\"pointer-events-none absolute top-0 left-0 overflow-visible\">\n <path d={branchPath(link)} className={cx(\"fill-none\", cc.stroke)} strokeWidth={STROKE_WIDTH[Math.min(depth, 3)]} strokeLinecap=\"round\" />\n </svg>\n ) : null}\n {/* raw: the topic's own box on the canvas — it takes the pointer\n (select, drag onto another topic, double-click to rename); the\n keys arrive through MindMap's handler on the canvas. */}\n <div\n className={box}\n style={{ padding: `${L.padY}px ${L.padX}px ${L.padY + (depth >= 2 ? UNDERLINE : 0)}px`, fontSize: L.font, lineHeight: `${L.line}px`, fontWeight: L.weight }}\n onPointerDown={(e) => {\n if (editing || e.button !== 0) return;\n m.onTopicPointerDown(e, id);\n }}\n onDoubleClick={(e) => {\n e.stopPropagation();\n if (!m.readOnly) m.startEdit(id);\n }}\n >\n {depth === 1 ? <span aria-hidden=\"true\" className={cx(\"pointer-events-none absolute inset-0 rounded-[inherit] opacity-60\", cc.tint)} /> : null}\n <EditableText\n value={text}\n editing={editing}\n onChange={(v) => m.setText(id, v)}\n onDone={() => m.finishEdit(id)}\n placeholder={m.placeholder}\n label={depth === 0 ? \"Central topic\" : \"Topic\"}\n className=\"relative min-h-full\"\n />\n {depth >= 2 ? <span aria-hidden=\"true\" className={cx(\"pointer-events-none absolute inset-x-0 bottom-0 h-0.5 rounded-full\", cc.solid)} /> : null}\n </div>\n\n {depth > 0 && kids > 0 ? (\n // raw: a 20px chip on the branch's end, smaller than any kit\n // button — the canvas is zoomed, and a 36px button would cover the\n // line. Kept out of the tab order: the keys fold (⌘↑ / ⌘↓).\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={collapsed ? `Unfold, ${hidden} hidden` : \"Fold branch\"}\n title={collapsed ? `${hidden} hidden · click to unfold` : \"Fold branch\"}\n onMouseDown={(e) => e.preventDefault()}\n onPointerDown={(e) => e.stopPropagation()}\n onClick={(e) => {\n e.stopPropagation();\n m.toggleFold(id);\n }}\n className={cx(\n \"nodrag nopan absolute z-10 grid h-5 min-w-5 place-items-center rounded-full border bg-card px-1 text-[11px] leading-none font-medium tabular-nums text-foreground shadow-xs\",\n \"transition-[opacity,scale] duration-150 hover:scale-110 motion-reduce:transition-none\",\n left ? \"translate-x-[-6px]\" : \"translate-x-[6px]\",\n cc.edge,\n collapsed ? \"opacity-100\" : \"opacity-0 group-hover/topic:opacity-100 focus-visible:opacity-100\",\n !collapsed && selected && \"opacity-100\",\n )}\n style={{ ...outer, top: outY, marginTop: -10 }}\n >\n {collapsed ? hidden : <span aria-hidden=\"true\" className=\"block h-0.5 w-2 rounded-full bg-muted-foreground\" />}\n </button>\n ) : null}\n\n {!m.readOnly && kids === 0 && !editing ? (\n // raw: as the fold chip — a 20px \"add a subtopic\" dot at the end.\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label=\"Add subtopic\"\n title=\"Add subtopic (Tab)\"\n onMouseDown={(e) => e.preventDefault()}\n onPointerDown={(e) => e.stopPropagation()}\n onClick={(e) => {\n e.stopPropagation();\n m.addChild(id);\n }}\n className={cx(\n \"nodrag nopan absolute z-10 grid size-5 place-items-center rounded-full border border-border bg-card text-muted-foreground shadow-xs\",\n \"opacity-0 transition-[opacity,scale,color] duration-150 hover:scale-110 hover:text-foreground group-hover/topic:opacity-100 motion-reduce:transition-none\",\n selected && \"opacity-100\",\n left ? \"translate-x-[-8px]\" : \"translate-x-[8px]\",\n depth === 0 && \"hidden\",\n )}\n style={{ ...outer, top: outY, marginTop: -10 }}\n >\n <Icon name=\"plus\" size={12} />\n </button>\n ) : null}\n </div>\n );\n}\n\nexport const NODE_TYPES = { topic: TopicNode };\n"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"path": "registry/blocks/mind-map/tree.js",
|
|
33
|
+
"type": "registry:component",
|
|
34
|
+
"target": "@components/makersclaw/blocks/mind-map/tree.js",
|
|
35
|
+
"content": "// The mind map's data and its layout — plain functions, no React, no DOM.\n// They run anywhere (the server, an agent's tool, a test).\n//\n// A map is one tree of topics:\n//\n// { id: \"root\", text: \"Launch plan\", children: [\n// { id: \"a\", text: \"Research\", side: \"right\", color: \"chart-3\", children: [...] },\n// { id: \"b\", text: \"Pricing\", collapsed: true, children: [...] },\n// ] }\n//\n// id any string, unique in the map\n// text the words (a newline is allowed)\n// children the subtopics, in order\n// collapsed true: the branch is folded (its subtopics hidden)\n// color a colour TOKEN (\"chart-3\"), the branch's hue; subtopics\n// inherit it. Unset on a main branch: the next in the palette\n// side \"left\" | \"right\", only on main branches (the root's\n// children): which side of the centre it grows on. Unset:\n// balanced by size\n// note any extra text you keep with it (shown nowhere by the map)\n//\n// Every operation returns a NEW tree (the old one is left as it was), so\n// a tree is safe to keep for undo.\n\n/** The branch colours, in order: chart hues, which every theme derives\n * from its own primary. The root wears the primary itself. */\nexport const BRANCH_COLORS = [\"chart-2\", \"chart-3\", \"chart-4\", \"chart-5\", \"chart-1\"];\n\nlet seq = 0;\n/** A fresh topic id: `t-lx3k9f2a`. */\nexport function topicId() {\n seq = (seq + 1) % 1296;\n return `t-${Date.now().toString(36)}${seq.toString(36).padStart(2, \"0\")}${Math.floor(Math.random() * 1296)\n .toString(36)\n .padStart(2, \"0\")}`;\n}\n\n/** A new topic. */\nexport function createTopic(text = \"\", extra) {\n return { id: topicId(), text, children: [], ...(extra ?? {}) };\n}\n\nconst isObj = (v) => v !== null && typeof v === \"object\" && !Array.isArray(v);\n\n/**\n * Anything map-shaped → a clean tree: an object with `text`/`children`\n * (also `title`/`label`/`name` for the words and `topics`/`items` for the\n * children), an outline string (see outlineToMindMap), or nothing (a\n * fresh map). Missing and repeated ids are made unique. Never throws.\n */\nexport function normalizeMap(value, { rootText = \"Central topic\" } = {}) {\n if (typeof value === \"string\") return outlineToMindMap(value, { rootText });\n const seen = new Set();\n const walk = (t, depth) => {\n const text = typeof t?.text === \"string\" ? t.text : String(t?.title ?? t?.label ?? t?.name ?? \"\");\n let id = t?.id != null && t.id !== \"\" ? String(t.id) : depth === 0 ? \"root\" : topicId();\n while (seen.has(id)) id = topicId();\n seen.add(id);\n const kids = Array.isArray(t?.children) ? t.children : Array.isArray(t?.topics) ? t.topics : Array.isArray(t?.items) ? t.items : [];\n const out = { ...t, id, text, children: kids.filter((k) => isObj(k) || typeof k === \"string\").map((k) => walk(typeof k === \"string\" ? { text: k } : k, depth + 1)) };\n delete out.topics;\n delete out.items;\n if (depth !== 1) delete out.side;\n else if (out.side !== \"left\" && out.side !== \"right\") delete out.side;\n if (!out.collapsed) delete out.collapsed;\n if (!out.color) delete out.color;\n return out;\n };\n if (!isObj(value)) return { id: \"root\", text: rootText, children: [] };\n return walk(value, 0);\n}\n\n// ── reading ──────────────────────────────────────────────────────────────\n\n/** Every topic, depth first, with its parent and depth. */\nexport function flattenMap(root) {\n const out = [];\n const walk = (t, parent, depth, index) => {\n out.push({ topic: t, parent, depth, index });\n (t.children ?? []).forEach((c, i) => walk(c, t, depth + 1, i));\n };\n if (root) walk(root, null, 0, 0);\n return out;\n}\n\n/** The topic with this id, or null. */\nexport function findTopic(root, id) {\n if (!root) return null;\n if (root.id === id) return root;\n for (const c of root.children ?? []) {\n const hit = findTopic(c, id);\n if (hit) return hit;\n }\n return null;\n}\n\n/** [root, …, the topic] — the path down to it, or [] when it is not there. */\nexport function pathTo(root, id) {\n if (!root) return [];\n if (root.id === id) return [root];\n for (const c of root.children ?? []) {\n const p = pathTo(c, id);\n if (p.length) return [root, ...p];\n }\n return [];\n}\n\n/** The parent of a topic (null for the root or a stranger). */\nexport function parentOf(root, id) {\n const p = pathTo(root, id);\n return p.length > 1 ? p[p.length - 2] : null;\n}\n\n/** How many topics sit under this one, at every depth. */\nexport function countTopics(topic) {\n let n = 0;\n for (const c of topic?.children ?? []) n += 1 + countTopics(c);\n return n;\n}\n\n/** Is `id` the topic `ancestor` or anywhere under it? */\nexport function isWithin(root, ancestorId, id) {\n const a = findTopic(root, ancestorId);\n return Boolean(a && findTopic(a, id));\n}\n\n// ── changing ─────────────────────────────────────────────────────────────\n\n/** A copy of the tree with one topic replaced by fn(topic). */\nexport function mapTopic(root, id, fn) {\n if (!root) return root;\n if (root.id === id) return fn(root);\n let changed = false;\n const kids = (root.children ?? []).map((c) => {\n const next = mapTopic(c, id, fn);\n if (next !== c) changed = true;\n return next;\n });\n return changed ? { ...root, children: kids } : root;\n}\n\n/** Merge a patch into one topic (`{ text }`, `{ color }`, `{ collapsed }`…). */\nexport function updateTopic(root, id, patch) {\n return mapTopic(root, id, (t) => {\n const next = { ...t, ...(typeof patch === \"function\" ? patch(t) : patch) };\n for (const k of [\"color\", \"collapsed\", \"side\", \"note\"]) if (next[k] === undefined || next[k] === false || next[k] === \"\") delete next[k];\n return next;\n });\n}\n\n// Main branches keep the side they are on once the map changes around\n// them, so adding one never flips the others across the centre.\nfunction pinSides(root) {\n const kids = root.children ?? [];\n if (kids.every((c) => c.side)) return root;\n const sides = splitSides(root);\n return { ...root, children: kids.map((c) => (c.side ? c : { ...c, side: sides.get(c.id) })) };\n}\n\n/** Put `topic` under `parentId` at `index` (default: last). A new main\n * branch goes on the lighter side unless it names one. */\nexport function insertTopic(root, parentId, topic, index) {\n const t = topic ?? createTopic();\n if (parentId === root.id) {\n const pinned = pinSides(root);\n const kids = [...(pinned.children ?? [])];\n let side = t.side;\n if (!side) {\n const l = kids.filter((c) => c.side === \"left\").reduce((n, c) => n + 1 + visibleCount(c), 0);\n const r = kids.filter((c) => c.side !== \"left\").reduce((n, c) => n + 1 + visibleCount(c), 0);\n side = l < r ? \"left\" : \"right\";\n }\n const at = index === undefined ? kids.length : Math.max(0, Math.min(kids.length, index));\n kids.splice(at, 0, { ...t, side });\n return { ...pinned, children: kids };\n }\n return mapTopic(root, parentId, (p) => {\n const kids = [...(p.children ?? [])];\n const { side: _s, ...rest } = t;\n const at = index === undefined ? kids.length : Math.max(0, Math.min(kids.length, index));\n kids.splice(at, 0, rest);\n const next = { ...p, children: kids };\n delete next.collapsed;\n return next;\n });\n}\n\nfunction visibleCount(t) {\n if (t.collapsed) return 0;\n let n = 0;\n for (const c of t.children ?? []) n += 1 + visibleCount(c);\n return n;\n}\n\n/** A new topic next to `id` (after it, or before with `before`). On the\n * root there is no next to: it becomes a child. */\nexport function insertSibling(root, id, topic, { before = false } = {}) {\n const parent = parentOf(root, id);\n if (!parent) return insertTopic(root, root.id, topic);\n const i = parent.children.findIndex((c) => c.id === id);\n const me = parent.children[i];\n const t = parent.id === root.id && me?.side ? { ...topic, side: topic.side ?? me.side } : topic;\n return insertTopic(root, parent.id, t, before ? i : i + 1);\n}\n\n/** Take a topic (and its branch) out. The root stays. */\nexport function removeTopic(root, id) {\n if (!root || root.id === id) return root;\n const parent = parentOf(root, id);\n if (!parent) return root;\n return mapTopic(root, parent.id, (p) => ({ ...p, children: p.children.filter((c) => c.id !== id) }));\n}\n\n/** Move a topic (with its branch) under `parentId` at `index` (default\n * last). Refused (the same tree back) into its own branch. */\nexport function moveTopic(root, id, parentId, index) {\n if (id === root.id || isWithin(root, id, parentId)) return root;\n const t = findTopic(root, id);\n if (!t) return root;\n const from = parentOf(root, id);\n let at = index;\n if (from?.id === parentId && at !== undefined) {\n const was = from.children.findIndex((c) => c.id === id);\n if (was < at) at -= 1;\n }\n const without = removeTopic(root, id);\n const moved = { ...t };\n if (parentId !== root.id) delete moved.side;\n else if (from?.id !== root.id) delete moved.side;\n return insertTopic(without, parentId, moved, at);\n}\n\n/** Swap a topic with its neighbour: `delta` -1 (up) or +1 (down). A main\n * branch trades places with the next one on its own side. */\nexport function shiftTopic(root, id, delta) {\n const parent = parentOf(root, id);\n if (!parent) return root;\n const base = parent.id === root.id ? pinSides(root) : parent;\n const kids = base.children;\n const i = kids.findIndex((c) => c.id === id);\n const sameSide = (c) => parent.id !== root.id || (c.side ?? \"right\") === (kids[i].side ?? \"right\");\n let j = i + delta;\n while (j >= 0 && j < kids.length && !sameSide(kids[j])) j += delta;\n if (j < 0 || j >= kids.length) return root;\n const next = [...kids];\n [next[i], next[j]] = [next[j], next[i]];\n if (parent.id === root.id) return { ...base, children: next };\n return mapTopic(root, parent.id, (p) => ({ ...p, children: next }));\n}\n\n/** Tab on an outline: the topic becomes the last child of the topic\n * before it. Nothing before it: the same tree back. */\nexport function indentTopic(root, id) {\n const parent = parentOf(root, id);\n if (!parent) return root;\n const kids = parent.id === root.id ? pinSides(root).children : parent.children;\n const i = kids.findIndex((c) => c.id === id);\n const me = kids[i];\n let j = i - 1;\n if (parent.id === root.id) while (j >= 0 && (kids[j].side ?? \"right\") !== (me.side ?? \"right\")) j -= 1;\n if (j < 0) return root;\n return moveTopic(root, id, kids[j].id);\n}\n\n/** Shift+Tab: the topic moves up a level, to just after its parent. */\nexport function outdentTopic(root0, id) {\n const root = pinSides(root0);\n const path = pathTo(root, id);\n if (path.length < 3) return root0;\n const parent = path[path.length - 2];\n const grand = path[path.length - 3];\n const at = grand.children.findIndex((c) => c.id === parent.id) + 1;\n const t = findTopic(root, id);\n const moved = grand.id === root.id ? { ...t, side: parent.side } : t;\n const without = removeTopic(root, id);\n return insertTopic(without, grand.id, moved, at);\n}\n\n/** A copy of a branch with fresh ids, placed right after it. */\nexport function duplicateTopic(root, id) {\n const t = findTopic(root, id);\n if (!t || id === root.id) return { root, id: null };\n const copy = (x) => ({ ...x, id: topicId(), children: (x.children ?? []).map(copy) });\n const c = copy(t);\n return { root: insertSibling(root, id, c), id: c.id };\n}\n\n/** Fold (or unfold) every branch that has subtopics, the root aside. */\nexport function setAllCollapsed(root, collapsed) {\n const walk = (t, depth) => {\n const kids = (t.children ?? []).map((c) => walk(c, depth + 1));\n const next = { ...t, children: kids };\n if (depth > 0 && collapsed && kids.length) next.collapsed = true;\n else delete next.collapsed;\n return next;\n };\n return walk(root, 0);\n}\n\n// ── outline in, outline out ──────────────────────────────────────────────\n\n/**\n * An indented outline → topics (a forest: an array of top-level topics).\n * Bullets (-, *, •, 1.), markdown headings (# is the top), tabs or spaces;\n * blank lines skipped. What an agent writes, or a list pasted from notes.\n */\nexport function outlineToTopics(text) {\n const lines = String(text ?? \"\")\n .replace(/\\r\\n?/g, \"\\n\")\n .split(\"\\n\")\n .filter((l) => l.trim());\n if (!lines.length) return [];\n const rows = lines.map((l) => {\n const heading = /^\\s*(#{1,6})\\s+(.*)$/.exec(l);\n if (heading) return { level: heading[1].length - 7, text: heading[2].trim() };\n const lead = /^[\\t ]*/.exec(l)[0].replace(/\\t/g, \" \").length;\n const body = l.trim().replace(/^(?:[-*+•]|\\d+[.)])\\s+/, \"\").replace(/^\\[[ xX]\\]\\s+/, \"\");\n return { level: lead, text: body.trim() };\n });\n // Headings sit above every indented line; indents rank by their width.\n const levels = [...new Set(rows.map((r) => r.level))].sort((a, b) => a - b);\n const rank = new Map(levels.map((l, i) => [l, i]));\n const forest = [];\n const stack = [];\n for (const r of rows) {\n const depth = rank.get(r.level);\n const t = createTopic(r.text);\n while (stack.length && stack[stack.length - 1].depth >= depth) stack.pop();\n if (stack.length) stack[stack.length - 1].topic.children.push(t);\n else forest.push(t);\n stack.push({ depth, topic: t });\n }\n return forest;\n}\n\n/** An outline → a map. One top line: it is the centre. Several: they\n * hang off a centre called `rootText`. */\nexport function outlineToMindMap(text, { rootText = \"Central topic\" } = {}) {\n const forest = outlineToTopics(text);\n if (forest.length === 1) return { ...forest[0], id: \"root\" };\n return { id: \"root\", text: rootText, children: forest };\n}\n\n/** The map as an indented outline, two spaces a level — for an agent, a\n * clipboard, a search column. Folded branches are included. */\nexport function mindMapToText(topic) {\n const out = [];\n const walk = (t, depth) => {\n out.push(`${\" \".repeat(depth)}${depth ? \"- \" : \"\"}${String(t.text ?? \"\").replace(/\\n/g, \" \") || \"Untitled\"}`);\n for (const c of t.children ?? []) walk(c, depth + 1);\n };\n if (topic) walk(topic, 0);\n return out.join(\"\\n\");\n}\n\n/** The map as Markdown: the centre as a heading, the branches as a\n * nested list. */\nexport function mindMapToMarkdown(root) {\n if (!root) return \"\";\n const lines = [`# ${String(root.text ?? \"\").replace(/\\n/g, \" \") || \"Untitled\"}`, \"\"];\n const walk = (t, depth) => {\n lines.push(`${\" \".repeat(depth)}- ${String(t.text ?? \"\").replace(/\\n/g, \" \") || \"Untitled\"}`);\n for (const c of t.children ?? []) walk(c, depth + 1);\n };\n for (const c of root.children ?? []) walk(c, 0);\n return lines.join(\"\\n\");\n}\n\n// ── layout ───────────────────────────────────────────────────────────────\n\n// Type and box per level, in px. The node draws with exactly these, so\n// what is laid out is what is shown. level 0 = the centre, 1 = a main\n// branch (a tinted pill), 2+ = a subtopic (words over a coloured line).\nexport const LEVELS = [\n { font: 18, weight: 600, line: 26, padX: 20, padY: 12, min: 120, max: 280, gap: 56 },\n { font: 14, weight: 500, line: 20, padX: 14, padY: 8, min: 64, max: 220, gap: 36 },\n { font: 14, weight: 400, line: 20, padX: 6, padY: 5, min: 44, max: 240, gap: 28 },\n];\nexport const levelOf = (depth) => LEVELS[Math.min(depth, LEVELS.length - 1)];\nconst SIBLING_GAP = [0, 20, 8];\nconst siblingGap = (depth) => SIBLING_GAP[Math.min(depth, SIBLING_GAP.length - 1)];\n/** Room left under a subtopic for its coloured line. */\nexport const UNDERLINE = 2;\n\n/** A width guess without a DOM: average glyph widths for a UI sans. */\nexport function estimateText(text, { font = 14, weight = 400 } = {}) {\n let w = 0;\n for (const ch of String(text ?? \"\")) w += /[A-Z]/.test(ch) ? 0.66 : /[mwMW]/.test(ch) ? 0.8 : /[ilj.,:;'!|]/.test(ch) ? 0.28 : ch === \" \" ? 0.28 : 0.54;\n return w * font * (weight >= 600 ? 1.05 : weight >= 500 ? 1.02 : 1);\n}\n\n/** Words wrapped at `max` px → { width, lines }. */\nexport function wrapText(text, max, measure, style) {\n const paras = String(text ?? \"\").split(\"\\n\");\n let width = 0;\n let lines = 0;\n const space = measure(\" \", style);\n for (const para of paras) {\n const words = para.split(/\\s+/).filter(Boolean);\n if (!words.length) {\n lines += 1;\n continue;\n }\n let line = 0;\n for (const w of words) {\n const ww = measure(w, style);\n if (line && line + space + ww > max) {\n width = Math.max(width, line);\n lines += 1;\n line = 0;\n }\n if (ww > max) {\n // One word longer than the line: it breaks where it must.\n const extra = Math.floor(ww / max);\n lines += extra;\n width = max;\n line = ww - extra * max;\n continue;\n }\n line = line ? line + space + ww : ww;\n }\n width = Math.max(width, line);\n lines += 1;\n }\n return { width: Math.min(max, width), lines: Math.max(1, lines) };\n}\n\n/** The box a topic needs at a depth: { width, height }. `placeholder`\n * sizes an empty topic. */\nexport function topicSize(text, depth, measure = estimateText, placeholder = \"New topic\") {\n const L = levelOf(depth);\n const style = { font: L.font, weight: L.weight };\n const words = String(text ?? \"\").trim() ? text : placeholder;\n const { width, lines } = wrapText(words, L.max - L.padX * 2, measure, style);\n // +2: a browser wraps a hair sooner than the sum of the words' widths.\n const w = Math.max(L.min, Math.ceil(width + 2) + L.padX * 2);\n const h = lines * L.line + L.padY * 2 + (depth >= 2 ? UNDERLINE : 0);\n return { width: w, height: h };\n}\n\n/** Which side each main branch grows on: its own `side`, else split in\n * order so the two sides hold about as many rows. Counted in topics, not\n * pixels, so the answer never depends on the font. Map(id → side). */\nexport function splitSides(root) {\n const kids = root?.children ?? [];\n const rows = (t) => (t.collapsed || !t.children?.length ? 1 : t.children.reduce((n, c) => n + rows(c), 0));\n const w = kids.map(rows);\n const out = new Map();\n let L = 0;\n let R = 0;\n const free = [];\n kids.forEach((c, i) => {\n if (c.side === \"left\") (L += w[i]), out.set(c.id, \"left\");\n else if (c.side === \"right\") (R += w[i]), out.set(c.id, \"right\");\n else free.push(i);\n });\n // The first k free branches go right, the rest left: the k that leaves\n // the two sides closest wins (a tie: more on the right).\n let best = 0;\n let bestDiff = Infinity;\n const total = free.reduce((n, i) => n + w[i], 0);\n let acc = 0;\n for (let k = 0; k <= free.length; k++) {\n const diff = Math.abs(R + acc - (L + total - acc));\n if (diff <= bestDiff) {\n bestDiff = diff;\n best = k;\n }\n if (k < free.length) acc += w[free[k]];\n }\n free.forEach((i, n) => out.set(kids[i].id, n < best ? \"right\" : \"left\"));\n return out;\n}\n\n/**\n * Where every visible topic goes. The centre at (0, 0); main branches\n * on both sides, balanced; each branch a tidy tree growing outwards, a\n * topic centred on its subtopics. Folded branches stop at the fold.\n *\n * measure(text, { font, weight }) → px (default: estimateText)\n * colors the branch palette (default BRANCH_COLORS)\n *\n * Returns { items, bounds }. Each item: { id, topic, parentId, depth,\n * side (\"root\" | \"left\" | \"right\"), color, x, y, width, height (top-left\n * box), hidden (topics folded away under it), from / to (where the line\n * from the parent leaves and arrives, board px) }.\n */\nexport function layoutMindMap(root, { measure = estimateText, colors = BRANCH_COLORS } = {}) {\n if (!root) return { items: [], bounds: { x: 0, y: 0, width: 0, height: 0 } };\n const sizes = new Map();\n const sizeOf = (t, depth) => {\n const k = t.id;\n if (!sizes.has(k)) sizes.set(k, topicSize(t.text, depth, measure));\n return sizes.get(k);\n };\n const block = new Map();\n const blockOf = (t, depth) => {\n if (block.has(t.id)) return block.get(t.id);\n const own = sizeOf(t, depth).height;\n let h = own;\n if (!t.collapsed && t.children?.length) {\n const sum = t.children.reduce((n, c) => n + blockOf(c, depth + 1), 0) + siblingGap(depth + 1) * (t.children.length - 1);\n h = Math.max(own, sum);\n }\n block.set(t.id, h);\n return h;\n };\n\n const items = [];\n const rs = sizeOf(root, 0);\n const rootItem = { id: root.id, topic: root, parentId: null, depth: 0, side: \"root\", color: null, x: -rs.width / 2, y: -rs.height / 2, width: rs.width, height: rs.height, hidden: root.collapsed ? countTopics(root) : 0 };\n items.push(rootItem);\n\n // Where a line leaves a topic towards its children on `side`, and where\n // a line arrives at it.\n const outAnchor = (it, side) => {\n const dir = side === \"left\" ? -1 : 1;\n const x = dir > 0 ? it.x + it.width : it.x;\n const y = it.depth >= 2 ? it.y + it.height - UNDERLINE / 2 : it.y + it.height / 2;\n return { x, y, dir };\n };\n const inAnchor = (it) => {\n const x = it.side === \"left\" ? it.x + it.width : it.x;\n const y = it.depth >= 2 ? it.y + it.height - UNDERLINE / 2 : it.y + it.height / 2;\n return { x, y };\n };\n\n const place = (t, parentItem, depth, side, top, color) => {\n const s = sizeOf(t, depth);\n const b = blockOf(t, depth);\n const from = outAnchor(parentItem, side);\n const gap = levelOf(depth - 1).gap;\n const cy = top + b / 2;\n const x = side === \"left\" ? from.x - gap - s.width : from.x + gap;\n // A subtopic's line is its foot: centre the words so the line sits\n // at the middle of its children.\n const y = cy - s.height / 2;\n const it = { id: t.id, topic: t, parentId: parentItem.id, depth, side, color, x, y, width: s.width, height: s.height, hidden: t.collapsed ? countTopics(t) : 0 };\n it.from = { x: from.x, y: from.y };\n it.to = inAnchor(it);\n items.push(it);\n if (!t.collapsed && t.children?.length) {\n const sum = t.children.reduce((n, c) => n + blockOf(c, depth + 1), 0) + siblingGap(depth + 1) * (t.children.length - 1);\n let y2 = cy - sum / 2;\n for (const c of t.children) {\n place(c, it, depth + 1, side, y2, c.color ?? color);\n y2 += blockOf(c, depth + 1) + siblingGap(depth + 1);\n }\n }\n };\n\n if (!root.collapsed) {\n const sides = splitSides(root);\n const kids = root.children ?? [];\n for (const side of [\"right\", \"left\"]) {\n const mine = kids.map((c, i) => ({ c, i })).filter(({ c }) => sides.get(c.id) === side);\n if (!mine.length) continue;\n const sum = mine.reduce((n, { c }) => n + blockOf(c, 1), 0) + siblingGap(1) * (mine.length - 1);\n let y = -sum / 2;\n for (const { c, i } of mine) {\n place(c, rootItem, 1, side, y, c.color ?? colors[i % colors.length]);\n y += blockOf(c, 1) + siblingGap(1);\n }\n }\n }\n\n let x1 = Infinity;\n let y1 = Infinity;\n let x2 = -Infinity;\n let y2 = -Infinity;\n for (const it of items) {\n x1 = Math.min(x1, it.x);\n y1 = Math.min(y1, it.y);\n x2 = Math.max(x2, it.x + it.width);\n y2 = Math.max(y2, it.y + it.height);\n }\n return { items, bounds: { x: x1, y: y1, width: x2 - x1, height: y2 - y1 } };\n}\n\n/** The map as a FlowCanvas board: one `topic` node per visible topic,\n * laid out (layoutMindMap), no connectors (each topic draws the line to\n * its parent). What MindMap hands the canvas. */\nexport function mindMapToBoard(root, opts) {\n const { items } = layoutMindMap(root, opts);\n const round = (n) => Math.round(n * 100) / 100;\n return {\n nodes: items.map((it) => ({\n id: it.id,\n type: \"topic\",\n position: { x: round(it.x), y: round(it.y) },\n width: Math.round(it.width),\n height: Math.round(it.height),\n draggable: false,\n connectable: false,\n data: {\n text: it.topic.text,\n depth: it.depth,\n side: it.side,\n color: it.color,\n kids: it.topic.children?.length ?? 0,\n collapsed: Boolean(it.topic.collapsed),\n hidden: it.hidden,\n link: it.from ? { x1: round(it.from.x - it.x), y1: round(it.from.y - it.y), x2: round(it.to.x - it.x), y2: round(it.to.y - it.y) } : null,\n },\n })),\n edges: [],\n };\n}\n\n/** The curve from a parent to a child, in the child's own coordinates. */\nexport function branchPath({ x1, y1, x2, y2 }) {\n const dx = (x2 - x1) * 0.55;\n return `M${x1},${y1} C${x1 + dx},${y1} ${x2 - dx},${y2} ${x2},${y2}`;\n}\n\n// ── starters ─────────────────────────────────────────────────────────────\n\nconst t = (text, children = []) => ({ text, children: children.map((c) => (typeof c === \"string\" ? t(c) : c)) });\n\n/** What an empty map offers. Each `map()` makes a fresh tree. */\nexport function mindMapTemplates() {\n return [\n {\n id: \"project\",\n label: \"Project plan\",\n icon: \"flag\",\n description: \"Goals, scope, people, risks\",\n map: () => normalizeMap(t(\"Project\", [t(\"Goals\", [\"Why now\", \"What success looks like\"]), t(\"Scope\", [\"In\", \"Out\"]), t(\"People\", [\"Owner\", \"Helpers\"]), t(\"Milestones\"), t(\"Risks\")])),\n },\n {\n id: \"brainstorm\",\n label: \"Brainstorm\",\n icon: \"lightbulb\",\n description: \"One question, ideas around it\",\n map: () => normalizeMap(t(\"How might we…?\", [\"Idea\", \"Idea\", \"Idea\", \"Idea\"])),\n },\n {\n id: \"meeting\",\n label: \"Meeting notes\",\n icon: \"message-square\",\n description: \"Agenda, decisions, actions\",\n map: () => normalizeMap(t(\"Meeting\", [t(\"Agenda\"), t(\"Notes\"), t(\"Decisions\"), t(\"Actions\")])),\n },\n ];\n}\n"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"path": "registry/blocks/mind-map/README.md",
|
|
39
|
+
"type": "registry:file",
|
|
40
|
+
"target": "@components/makersclaw/blocks/mind-map/README.md",
|
|
41
|
+
"content": "# MindMap\n\nA mind map you can build without touching the mouse. One centre topic,\nmain branches balanced on both sides of it, each branch a tidy tree in its\nown colour. **Tab** adds a subtopic, **Enter** the next topic, typing\nrenames, the arrows walk the map. The layout is automatic: topics never\nneed dragging into place, and the map re-balances as it grows.\n\nA bar on top holds undo and redo, the save state, the selected topic's\nactions (add, colour, fold, delete) and a Map / Outline switch. The\nOutline view is the map as a nested list — the reading of it for a screen\nreader, and a quick way to jump to a topic.\n\nIt is built on `flow-canvas` (which draws it, pans and zooms): this block\nowns the tree, the layout and the keys.\n\n```jsx\nimport { MindMap } from \"../components/blocks/mind-map\"; // a Makersclaw app (\"./blocks/mind-map\" from components/)\nimport { MindMap } from \"@/components/makersclaw/blocks/mind-map\"; // a shadcn project\n```\n\n`MindMap` is a client component: render it from a `\"use client\"`\ncomponent. `makersclaw add mind-map` adds `flow-canvas` with it (and its\nnpm package, `@xyflow/react`: run `npm install` after). The tree helpers\n(`mindMapToText`, `outlineToMindMap`, `layoutMindMap`…) are plain\nfunctions and work on the server too.\n\n## The data\n\nA map is one tree of topics. Store it as it comes out of `onChange` (one\n`jsonb` column), and keep `mindMapToText(map)` beside it for search and\nagents.\n\n```js\n{\n id: \"root\", text: \"Relay 2.0 launch\",\n children: [\n { id: \"why\", text: \"Why now\", side: \"right\", children: [\n { id: \"why-1\", text: \"Churn up 3 pts\" },\n ] },\n { id: \"risks\", text: \"Risks\", side: \"left\", color: \"chart-4\", collapsed: true, children: [ … ] },\n ],\n}\n```\n\n- `id` — any string, unique in the map. `text` — the words (a newline is\n allowed). `children` — the subtopics, in order.\n- `collapsed: true` — the branch is folded; the map shows how many topics\n it hides.\n- `color` — a colour **token**, never a colour value: `\"chart-2\"` …\n `\"chart-5\"`, `\"chart-1\"` (the theme's own), `\"warning\"`, `\"success\"`,\n `\"info\"`, `\"destructive\"`. Subtopics inherit their branch's. A main\n branch with none takes the next of `colors`.\n- `side` — `\"left\"` or `\"right\"`, main branches only. Missing, the map\n splits the branches to balance the two sides; the first change pins\n them, so adding a branch never flips the others across the centre.\n- Anything else on a topic (`note`, your own keys) is kept as it is.\n\n`normalizeMap(anything)` cleans what it is given: missing or repeated ids,\n`title`/`label` for `text`, `topics`/`items` for `children`, an outline\nstring, or nothing (a fresh map). The component runs it on `value`.\n\n## Props\n\n- `value` — the map, controlled. A new `value` that is not what the map\n last sent through `onChange` replaces it (loading another record); that\n replacement can be undone. Pass back exactly what `onChange` gave you.\n- `defaultValue` — the starting map, uncontrolled. With neither, a centre\n topic called \"Central topic\" and the first-run card.\n- `onChange(map)` — the whole tree after a change settles (debounced by\n `changeDelay`; typing is saved as it goes). Selection, folding in\n read-only mode, panning and zooming never call it.\n- `onSave(map)` — `async`, returns `{ ok: true }` or `{ ok: false,\n error? }` (a throw counts as failed). Called right after `onChange`. The\n bar shows \"Saving…\", \"Saved\", or the error with Retry. A failure keeps\n the map as it is on screen (nothing typed is thrown away); Retry sends\n it again.\n- `changeDelay` — ms of quiet before `onChange` / `onSave` (default 400).\n- `readOnly` — `false` (default). `true`: no editing; the arrows still\n walk the map, Space folds and unfolds (not saved), the Outline view and\n copying work.\n- `colors` — the branch palette, token names (default `BRANCH_COLORS`:\n chart-2, 3, 4, 5, 1). The colour picker offers these plus warning,\n success, info and destructive.\n- `templates` — what the first-run card offers: `[{ id, label, icon?,\n description?, map }]`, `map` a tree or a function returning one (default\n `mindMapTemplates()`: Project plan, Brainstorm, Meeting notes). `[]` for\n none.\n- `placeholder` — what an empty topic shows while it is typed (default\n \"New topic\").\n- `height` — px or any CSS length, default `600`. A height class in\n `className` (`h-dvh`, `h-full`, `flex-1`) replaces it.\n- `label` — the map's accessible name (default \"Mind map\").\n- `onSelectionChange(topic | null)` — the selected topic changed (a side\n panel with its notes).\n- `className` — layout around it (margin, width, height). Never colour.\n\n## Keyboard\n\nWith a topic selected (click one, or press an arrow):\n\n| Key | Does |\n| --- | --- |\n| Tab | add a subtopic and type it |\n| Enter / Shift+Enter | add the next topic below / above |\n| Space, F2, or just type | rename |\n| Delete / Backspace | delete the topic and its branch (a toast offers Undo) |\n| ← → ↑ ↓ | walk the map: outwards to a child, inwards to the parent, up and down the column |\n| ⌥↑ / ⌥↓ | move the topic up or down among its siblings |\n| Shift+Tab | move the topic up a level |\n| ⌘↑ / ⌘↓ | fold / unfold the branch |\n| ⌘D | duplicate the branch |\n| ⌘C / ⌘X / ⌘V | copy or cut a branch as an outline; paste an outline (from notes, a doc, an agent) as subtopics |\n| ⌘Z / ⇧⌘Z | undo / redo |\n| Home | the centre |\n| Esc | clear the selection |\n| ⇧1 | fit the map to the screen |\n| ? | the shortcuts sheet |\n\nWhile typing a topic: **Enter** finishes it and starts the next one (on\nan empty one it steps out a level, so a run of topics never needs the\nmouse), **Tab** finishes it and starts a subtopic (on an empty one it\nmoves in a level), **Backspace** on an empty topic removes it and goes\nback to the one before, **Esc** finishes. Shift+Enter is a new line.\n\nWith the mouse: click selects, double-click renames, the **+** at a\ntopic's end adds a subtopic, the chip at a branch's end folds it (folded,\nit shows the count it hides), and dragging a topic onto another moves its\nbranch there. A plain drag on the empty canvas pans; pinch or ⌘-scroll\nzooms. On a touch screen a drag always pans; tap selects, double-tap\nrenames, and the bar's buttons add.\n\n## The helpers\n\nPlain functions over a tree; each returns a new tree and leaves the old\none as it was.\n\n- Read: `findTopic(map, id)`, `pathTo(map, id)` → `[root, …, topic]`,\n `parentOf(map, id)`, `countTopics(topic)` (everything under it).\n- Change: `updateTopic(map, id, patch)`, `insertTopic(map, parentId,\n topic, index?)`, `insertSibling(map, id, topic, { before })`,\n `removeTopic(map, id)`, `moveTopic(map, id, parentId, index?)` (refused\n into its own branch), `shiftTopic(map, id, ±1)`, `indentTopic`,\n `outdentTopic`, `duplicateTopic(map, id)` → `{ root, id }`,\n `setAllCollapsed(map, on)`, `createTopic(text)`.\n- Text: `mindMapToText(map)` (an indented outline, for an agent or search),\n `mindMapToMarkdown(map)` (a heading and a nested list),\n `outlineToMindMap(text)` and `outlineToTopics(text)` (bullets, numbers,\n headings, tabs or spaces — what an agent writes).\n- Layout: `layoutMindMap(map, { measure?, colors? })` → `{ items, bounds }`\n (every visible topic's box, side, depth, colour and the line to its\n parent), `mindMapToBoard(map)` (the same as a flow-canvas board).\n- `mindMapTemplates()`, `BRANCH_COLORS`.\n\n## Example\n\n```jsx\n// components/PlanMap.jsx — client\n\"use client\";\nimport { useState } from \"react\";\nimport { updateRow } from \"../lib/actions\";\nimport { MindMap, mindMapToText } from \"./blocks/mind-map\";\n\nexport default function PlanMap({ plan }) {\n const [map, setMap] = useState(plan.map); // jsonb, may be null\n return (\n <MindMap\n value={map ?? undefined}\n defaultValue={{ text: plan.name }}\n onChange={setMap}\n onSave={(m) => updateRow({ source: \"public.plans\", id: plan.id, patch: { map: m, map_text: mindMapToText(m) } })}\n label={`${plan.name} map`}\n className=\"h-[calc(100dvh-8rem)]\"\n />\n );\n}\n```\n\nAn agent's outline, straight onto a map:\n\n```jsx\nconst map = outlineToMindMap(`# Hiring plan\n- Roles\n - Founding engineer\n - Designer\n- Sourcing\n - Referrals\n - Communities`);\n<MindMap defaultValue={map} onSave={save} />\n```\n\n## What it will not do\n\n- Free placement: topics sit where the layout puts them (drag moves a\n branch to another topic, not to a spot). For free boards use\n `flow-canvas` or `whiteboard`.\n- Cross-links between branches, images or icons on topics.\n- Collaborate live: saves are last-write-wins.\n- Export an image.\n- Render the map on the server: the server shows it as text until the\n canvas mounts. For a read-only page that must be rich in the HTML,\n render `mindMapToMarkdown` yourself.\n\nA map it cannot be is your own component in `components/` — not an edit\nto this block. Its files are fixed: `npm run check` refuses a change, and\n`makersclaw add mind-map` restores them.\n"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"categories": [
|
|
45
|
+
"mind map",
|
|
46
|
+
"mindmap",
|
|
47
|
+
"brainstorm",
|
|
48
|
+
"tree",
|
|
49
|
+
"outline",
|
|
50
|
+
"ideas",
|
|
51
|
+
"topics",
|
|
52
|
+
"branches",
|
|
53
|
+
"planning",
|
|
54
|
+
"xmind",
|
|
55
|
+
"mindnode",
|
|
56
|
+
"keyboard",
|
|
57
|
+
"canvas",
|
|
58
|
+
"map"
|
|
59
|
+
],
|
|
60
|
+
"docs": "Installed to components/makersclaw/blocks/mind-map. Import: import { MindMap, BRANCH_COLORS, normalizeMap, createTopic, findTopic, updateTopic, insertTopic, removeTopic, moveTopic, layoutMindMap, mindMapToBoard, mindMapToText, mindMapToMarkdown, outlineToMindMap, outlineToTopics, mindMapTemplates } from \"@/components/makersclaw/blocks/mind-map\". Props and an example: components/makersclaw/blocks/mind-map/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
61
|
+
"meta": {
|
|
62
|
+
"makersclaw": {
|
|
63
|
+
"version": "0.3.1",
|
|
64
|
+
"useWhen": "A screen for thinking in a tree: brainstorming around one idea, planning a project, breaking a goal down, notes from a meeting, a topic outline an agent drafted. Not for free-form boards with stickies and arrows (flow-canvas, whiteboard), a people hierarchy (org-chart), or a process with steps (process-map)."
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "navigation-menu",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Navigation Menu",
|
|
6
|
+
"description": "A site's top nav: plain links, and triggers that open a mega-menu panel of links (icon, title, one-line description) with an optional featured card; hover with intent, click, arrow keys and Escape; a menu button and a sheet of collapsible groups on a phone.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.3.1/r/core.json"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "registry/blocks/navigation-menu/index.js",
|
|
14
|
+
"type": "registry:component",
|
|
15
|
+
"target": "@components/makersclaw/blocks/navigation-menu/index.js",
|
|
16
|
+
"content": "// The NavigationMenu block — `makersclaw add navigation-menu`. Fixed:\n// compose it through its props (README.md beside this file); a nav it\n// cannot be is your own component in components/.\nexport { NavigationMenu } from \"./navigation-menu\";\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "registry/blocks/navigation-menu/navigation-menu.jsx",
|
|
20
|
+
"type": "registry:component",
|
|
21
|
+
"target": "@components/makersclaw/blocks/navigation-menu/navigation-menu.jsx",
|
|
22
|
+
"content": "\"use client\";\n\nimport { useEffect, useId, useLayoutEffect, useRef, useState } from \"react\";\nimport { Button, Collapsible, Icon, IconButton, Item, Kicker, Sheet, cx, panelClass, usePopoverPosition } from \"../../ui\";\n\n// NavigationMenu — a site's top nav: plain links, and triggers that open\n// one shared panel (a mega-menu) of links with an icon, a title and a\n// short description, plus an optional featured card. The block owns\n// what a hand-drawn nav gets wrong: hover with intent (a delay to open,\n// a longer one to close, so the pointer can cross the gap), click and\n// Enter too, one panel at a time that stays open and slides when the\n// pointer moves to the next trigger, arrow keys along the bar, Escape\n// back to the trigger, Tab through the open panel in reading order,\n// aria-expanded and aria-current. Below the md breakpoint it is a menu\n// button opening a Sheet, each group a Collapsible.\n//\n// <NavigationMenu items={NAV} currentHref={pathname} brand={<Logo />}\n// actions={<Button href=\"/signup\" variant=\"primary\">Start free</Button>} />\n//\n// README.md beside this file lists every prop.\n\nconst OPEN_DELAY = 150;\nconst CLOSE_DELAY = 300;\nconst SLIDE_PX = 24;\nconst LINKS = \"a[href]\";\n\nfunction isActive(href, current) {\n if (!href || current == null) return false;\n if (href === current) return true;\n return href !== \"/\" && !href.startsWith(\"#\") && current.startsWith(href.endsWith(\"/\") ? href : `${href}/`);\n}\n\nfunction reducedMotion() {\n return typeof window !== \"undefined\" && window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches;\n}\n\n// A group's featured card as onNavigate hears it: keyed inside the group.\nconst featuredOf = (group) => ({ ...group.featured, key: `${group.key}/featured` });\n\n// Items as the block uses them: a key for each, a group's links keyed\n// inside it, and whether the current page is in it.\nfunction normalize(items, current) {\n return (items ?? []).filter(Boolean).map((it, i) => {\n const key = it.key ?? `${i}-${it.label}`;\n if (!Array.isArray(it.items)) return { ...it, key, kind: \"link\", active: isActive(it.href, current) };\n const links = it.items.filter(Boolean).map((l, j) => ({ ...l, key: `${key}/${l.key ?? j}`, active: isActive(l.href, current) }));\n return { ...it, key, kind: \"group\", items: links, active: links.some((l) => l.active) || isActive(it.featured?.href, current) };\n });\n}\n\nexport function NavigationMenu({\n items,\n currentHref,\n onNavigate,\n brand,\n actions,\n label = \"Main\",\n menuLabel = \"Menu\",\n openDelay = OPEN_DELAY,\n closeDelay = CLOSE_DELAY,\n className,\n}) {\n const entries = normalize(items, currentHref);\n const groups = entries.filter((e) => e.kind === \"group\");\n\n const id = useId();\n const panelId = `${id}-panel`;\n const root = useRef(null);\n const list = useRef(null);\n const panel = useRef(null);\n const content = useRef(null);\n const triggers = useRef(new Map());\n const timer = useRef(0);\n const hoverOpenedAt = useRef(0);\n const focusOnOpen = useRef(null); // \"first\" | \"last\" once a key opened it\n const was = useRef({ key: null, w: 0, h: 0, x: 0, index: -1 });\n\n const [openKey, setOpenKey] = useState(null);\n const [shownKey, setShownKey] = useState(null); // stays through the fade-out\n const [sheetOpen, setSheetOpen] = useState(false);\n\n const shown = groups.find((g) => g.key === (openKey ?? shownKey)) ?? null;\n\n // The panel sits under the trigger that opened it (the bar, before any\n // has). The hook reads the ref on every commit, so this re-anchors it.\n const anchor = useRef(null);\n anchor.current = triggers.current.get(openKey ?? shownKey) ?? list.current;\n usePopoverPosition(anchor, panel, { side: \"bottom\", align: \"start\", offset: 6 });\n\n function clearTimer() {\n clearTimeout(timer.current);\n timer.current = 0;\n }\n function show(key) {\n clearTimer();\n setOpenKey(key);\n if (key) setShownKey(key);\n }\n function scheduleClose() {\n clearTimer();\n timer.current = setTimeout(() => setOpenKey(null), closeDelay);\n }\n useEffect(() => () => clearTimeout(timer.current), []);\n\n // The panel follows the state; when it moves from one group to another\n // its box eases to the new size and the links slide in from the side\n // the pointer came from.\n useLayoutEffect(() => {\n const p = panel.current;\n if (!p) return;\n const isShown = p.matches(\":popover-open\");\n if (!openKey) {\n if (isShown) p.hidePopover();\n was.current = { key: null, w: 0, h: 0, x: 0, index: -1 };\n return;\n }\n if (!isShown) p.showPopover();\n const index = entries.findIndex((e) => e.key === openKey);\n const w = p.offsetWidth;\n const h = p.offsetHeight;\n const x = p.getBoundingClientRect().left;\n const prev = was.current;\n if (prev.key && prev.key !== openKey && !reducedMotion()) {\n const from = index > prev.index ? 1 : -1;\n p.animate(\n [\n { width: `${prev.w}px`, height: `${prev.h}px`, transform: `translateX(${prev.x - x}px)` },\n { width: `${w}px`, height: `${h}px`, transform: \"none\" },\n ],\n { duration: 200, easing: \"ease-out\" },\n );\n content.current?.animate(\n [\n { transform: `translateX(${from * SLIDE_PX}px)`, opacity: 0 },\n { transform: \"none\", opacity: 1 },\n ],\n { duration: 200, easing: \"ease-out\" },\n );\n }\n was.current = { key: openKey, w, h, x, index };\n const want = focusOnOpen.current;\n focusOnOpen.current = null;\n if (want) {\n const links = [...p.querySelectorAll(LINKS)];\n (want === \"last\" ? links.at(-1) : links[0])?.focus({ preventScroll: true });\n }\n });\n\n // A press anywhere outside the bar and its panel closes it; Escape\n // closes it wherever focus is (a hover-opened panel has none), and\n // brings focus back to the trigger when it was inside.\n useEffect(() => {\n if (!openKey) return;\n function onDown(e) {\n if (!root.current?.contains(e.target)) setOpenKey(null);\n }\n function onKey(e) {\n if (e.key !== \"Escape\") return;\n e.preventDefault();\n if (root.current?.contains(document.activeElement)) closeAndFocus(openKey);\n else setOpenKey(null);\n }\n document.addEventListener(\"pointerdown\", onDown);\n document.addEventListener(\"keydown\", onKey);\n return () => {\n document.removeEventListener(\"pointerdown\", onDown);\n document.removeEventListener(\"keydown\", onKey);\n };\n }, [openKey]);\n\n function tops() {\n return [...(list.current?.querySelectorAll(\"[data-nav-top]\") ?? [])];\n }\n\n function closeAndFocus(key) {\n setOpenKey(null);\n triggers.current.get(key)?.focus({ preventScroll: true });\n }\n\n // Along the bar: arrows and Home/End move between its items. While a\n // panel is open, arriving on another trigger opens that one instead.\n function onBarKeyDown(e) {\n const all = tops();\n const i = all.indexOf(e.target);\n if (i < 0) return;\n let next = null;\n if (e.key === \"ArrowRight\") next = (i + 1) % all.length;\n else if (e.key === \"ArrowLeft\") next = (i - 1 + all.length) % all.length;\n else if (e.key === \"Home\") next = 0;\n else if (e.key === \"End\") next = all.length - 1;\n if (next === null) return;\n e.preventDefault();\n const el = all[next];\n el.focus();\n if (openKey) show(el.dataset.navGroup ?? null);\n }\n\n function onTriggerKeyDown(e, key) {\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n focusOnOpen.current = e.key === \"ArrowUp\" ? \"last\" : \"first\";\n if (openKey === key) {\n const links = [...(panel.current?.querySelectorAll(LINKS) ?? [])];\n (e.key === \"ArrowUp\" ? links.at(-1) : links[0])?.focus();\n focusOnOpen.current = null;\n } else show(key);\n } else if (e.key === \"Tab\" && !e.shiftKey && openKey === key) {\n // The panel sits after the bar in the page; Tab goes into it first.\n const first = panel.current?.querySelector(LINKS);\n if (first) {\n e.preventDefault();\n first.focus();\n }\n }\n }\n\n function onPanelKeyDown(e) {\n if (!openKey) return;\n const links = [...(panel.current?.querySelectorAll(LINKS) ?? [])];\n const i = links.indexOf(document.activeElement);\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n if (e.key === \"ArrowUp\" && i <= 0) triggers.current.get(openKey)?.focus();\n else links[e.key === \"ArrowDown\" ? Math.min(i + 1, links.length - 1) : i - 1]?.focus();\n } else if (e.key === \"Tab\" && e.shiftKey && i === 0) {\n e.preventDefault();\n triggers.current.get(openKey)?.focus();\n } else if (e.key === \"Tab\" && !e.shiftKey && i === links.length - 1) {\n // Out of the panel's end: on to the bar's next item. After the bar's\n // last one, the page's own order carries on and the blur closes it.\n const all = tops();\n const after = all[all.indexOf(triggers.current.get(openKey)) + 1];\n if (after) {\n e.preventDefault();\n setOpenKey(null);\n after.focus();\n }\n }\n }\n\n\n // Focus leaving the bar and its panel altogether closes the panel.\n function onRootBlur(e) {\n if (openKey && e.relatedTarget && !root.current?.contains(e.relatedTarget)) setOpenKey(null);\n }\n\n // A followed link: the surface hears of it (and may preventDefault),\n // then the panel or the sheet closes. Only a link with an href is one.\n function follow(link) {\n if (!link.href) return undefined;\n return (e) => {\n onNavigate?.(link, e);\n setOpenKey(null);\n setSheetOpen(false);\n };\n }\n\n function onTriggerEnter(e, key) {\n if (e.pointerType !== \"mouse\") return;\n clearTimer();\n if (openKey) {\n show(key);\n return;\n }\n timer.current = setTimeout(() => {\n hoverOpenedAt.current = Date.now();\n show(key);\n }, openDelay);\n }\n\n function onTriggerClick(key) {\n // A click just after hover opened it confirms the hover, not a close.\n if (openKey === key && Date.now() - hoverOpenedAt.current > 400) setOpenKey(null);\n else show(key);\n }\n\n const topClass =\n \"aria-[current=page]:bg-accent data-[active=true]:bg-accent aria-expanded:bg-accent\";\n\n return (\n <nav ref={root} aria-label={label} onBlur={onRootBlur} className={cx(\"flex min-h-14 items-center gap-2\", className)}>\n {brand ? <div className=\"flex shrink-0 items-center\">{brand}</div> : null}\n\n <ul ref={list} role=\"list\" onKeyDown={onBarKeyDown} className=\"hidden items-center gap-1 md:flex\">\n {entries.map((e) =>\n e.kind === \"link\" ? (\n <li key={e.key} onPointerEnter={(ev) => ev.pointerType === \"mouse\" && openKey && scheduleClose()}>\n <Button\n href={e.href}\n variant=\"quiet\"\n size=\"sm\"\n data-nav-top=\"\"\n aria-current={e.active ? \"page\" : undefined}\n onClick={follow(e)}\n className={topClass}\n >\n {e.label}\n </Button>\n </li>\n ) : (\n <li key={e.key} onPointerEnter={(ev) => onTriggerEnter(ev, e.key)} onPointerLeave={(ev) => ev.pointerType === \"mouse\" && scheduleClose()}>\n <Button\n ref={(el) => (el ? triggers.current.set(e.key, el) : triggers.current.delete(e.key))}\n variant=\"quiet\"\n size=\"sm\"\n data-nav-top=\"\"\n data-nav-group={e.key}\n data-active={e.active ? \"true\" : undefined}\n aria-expanded={openKey === e.key}\n aria-controls={panelId}\n onClick={() => onTriggerClick(e.key)}\n onKeyDown={(ev) => onTriggerKeyDown(ev, e.key)}\n className={cx(topClass, \"gap-1\")}\n >\n {e.label}\n <Icon\n name=\"chevron-down\"\n size={14}\n className={cx(\"text-muted-foreground transition-transform duration-200 motion-reduce:transition-none\", openKey === e.key && \"rotate-180\")}\n />\n </Button>\n </li>\n ),\n )}\n </ul>\n\n {/* The one panel, after the bar so Tab reaches it in reading order. */}\n <div\n ref={panel}\n id={panelId}\n popover=\"manual\"\n role=\"region\"\n aria-label={shown?.label}\n onKeyDown={onPanelKeyDown}\n onPointerEnter={(ev) => ev.pointerType === \"mouse\" && clearTimer()}\n onPointerLeave={(ev) => ev.pointerType === \"mouse\" && scheduleClose()}\n onTransitionEnd={(ev) => ev.target === ev.currentTarget && !openKey && setShownKey(null)}\n className={panelClass(\"auto\", \"overflow-hidden p-2\")}\n >\n <div ref={content}>{shown ? <Panel group={shown} follow={follow} /> : null}</div>\n </div>\n\n <div className=\"ml-auto flex items-center gap-2\">\n {actions ? <div className=\"hidden items-center gap-2 md:flex\">{actions}</div> : null}\n <IconButton\n icon=\"menu\"\n label={menuLabel}\n aria-expanded={sheetOpen}\n onClick={() => setSheetOpen(true)}\n className=\"md:hidden\"\n />\n </div>\n\n <Sheet open={sheetOpen} onClose={() => setSheetOpen(false)} title={menuLabel} footer={actions ? <div className=\"flex w-full flex-wrap gap-2 [&>*]:flex-1\">{actions}</div> : null}>\n <div className=\"grid\">\n {entries.map((e) =>\n e.kind === \"link\" ? (\n <div key={e.key} className=\"-mx-2.5\">\n <Item size=\"sm\" href={e.href} title={e.label} aria-current={e.active ? \"page\" : undefined} onClick={follow(e)} />\n </div>\n ) : (\n <Collapsible key={e.key} title={e.label} defaultOpen={e.active}>\n <ul role=\"list\" className=\"-mx-2.5 grid gap-0.5\">\n {e.items.map((l) => (\n <li key={l.key}>\n <Item\n size=\"sm\"\n href={l.href}\n icon={l.icon}\n title={l.label}\n description={l.description}\n aria-current={l.active ? \"page\" : undefined}\n onClick={follow(l)}\n />\n </li>\n ))}\n {e.featured ? (\n <li>\n <Item\n size=\"sm\"\n variant=\"muted\"\n href={e.featured.href}\n icon={e.featured.icon ?? \"sparkles\"}\n title={e.featured.title}\n description={e.featured.description}\n onClick={follow(featuredOf(e))}\n className=\"mt-1\"\n />\n </li>\n ) : null}\n </ul>\n </Collapsible>\n ),\n )}\n </div>\n </Sheet>\n </nav>\n );\n}\n\n// One group's panel (never wider than the window: panelClass caps it and\n// the grid shrinks): its links in a grid (two columns past three links,\n// or `columns`), the featured card first when there is one.\nfunction Panel({ group, follow }) {\n const cols = group.columns ?? (group.items.length > 3 ? 2 : 1);\n return (\n <div className=\"flex gap-2\">\n {group.featured ? <Featured card={group.featured} onClick={follow(featuredOf(group))} /> : null}\n <ul role=\"list\" className={cx(\"grid min-w-0 content-start gap-1\", cols === 2 ? \"w-[38rem] grid-cols-2\" : \"w-80 grid-cols-1\")}>\n {group.items.map((l) => (\n <li key={l.key}>\n <Item\n href={l.href}\n icon={l.icon}\n title={l.label}\n description={l.description}\n aria-current={l.active ? \"page\" : undefined}\n onClick={follow(l)}\n />\n </li>\n ))}\n </ul>\n </div>\n );\n}\n\n// The featured card: a filled panel with a kicker, a title, a line and\n// its own link — the thing the group most wants read.\nfunction Featured({ card, onClick }) {\n return (\n <div className=\"flex w-56 shrink-0 flex-col justify-end gap-2 rounded-md bg-muted p-4\">\n {card.icon ? (\n <span className=\"mb-auto flex size-9 items-center justify-center rounded-md bg-background text-foreground\">\n <Icon name={card.icon} size={18} />\n </span>\n ) : null}\n {card.kicker ? <Kicker>{card.kicker}</Kicker> : null}\n <p className=\"text-base font-semibold leading-tight text-foreground\">{card.title}</p>\n {card.description ? <p className=\"text-sm text-muted-foreground\">{card.description}</p> : null}\n {card.href ? (\n <Button href={card.href} variant=\"link\" size=\"sm\" onClick={onClick}>\n {card.cta ?? \"Learn more\"}\n <Icon name=\"arrow-right\" size={14} />\n </Button>\n ) : null}\n </div>\n );\n}\n"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "registry/blocks/navigation-menu/README.md",
|
|
26
|
+
"type": "registry:file",
|
|
27
|
+
"target": "@components/makersclaw/blocks/navigation-menu/README.md",
|
|
28
|
+
"content": "# Navigation Menu\n\nA site's top nav. Plain links sit in the bar; a trigger opens a panel — a\nmega-menu — of links, each with an icon, a title and a short\ndescription, and an optional featured card. The panel opens on hover\n(after a short delay, so a pointer passing by does not open it), on click\nand on Enter. One panel at a time: moving to the next trigger keeps it\nopen, glides it under that trigger and slides the links across. Arrow\nkeys move along the bar, Escape closes and returns focus to the trigger,\nTab goes through the open panel in reading order. The current page is\nmarked: its link carries `aria-current=\"page\"` (a screen reader says\n\"current page\", and the row is filled to match), and its group's trigger\nis highlighted. Below\nthe `md` breakpoint the bar is a menu button that opens a Sheet, each\ngroup a Collapsible.\n\n```jsx\nimport { NavigationMenu } from \"../components/blocks/navigation-menu\"; // a Makersclaw app (\"./blocks/navigation-menu\" from components/)\nimport { NavigationMenu } from \"@/components/makersclaw/blocks/navigation-menu\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component (a\nlayout's header is the usual place).\n\n## The data\n\n`items` is the bar, in order. Each entry is a link or a group:\n\n```js\n{ label: \"Pricing\", href: \"/pricing\" } // a link\n{\n label: \"Product\", // a group: a trigger and its panel\n items: [{ label, href, icon?, description? }, …], // the panel's links\n featured?: { title, href?, description?, kicker?, icon?, cta? }, // a card before them\n columns?: 1 | 2, // default: 2 past three links\n key?: \"product\", // default: made from the position and label\n}\n```\n\n`icon` is a name from the kit's `Icon`. Keep a `description` short:\nthe panel clips it at two lines.\n\n## Props\n\n- `items` — the bar, as above.\n- `currentHref` — the page you are on (`usePathname()` in a Next app). A\n link whose `href` equals it, or is its parent path, is marked current;\n a group holding it is highlighted. Hash links match exactly.\n- `onNavigate(link, event)` — called when any link in the bar, a panel or\n the phone sheet is followed, before navigation. `event.preventDefault()`\n stops it. The panel and the sheet close either way.\n- `brand` — the node on the left (a logo that links home). Shown at every\n width.\n- `actions` — nodes on the right, e.g. \"Sign in\" and \"Start free\"\n Buttons. In the bar from `md` up; in the sheet's footer on a phone.\n- `label` — what a screen reader calls the nav, default `\"Main\"`.\n- `menuLabel` — the phone's menu button and the sheet's title, default\n `\"Menu\"`.\n- `openDelay` — ms the pointer rests on a trigger before it opens,\n default `150`. Moving between triggers while one is open is immediate.\n- `closeDelay` — ms after the pointer leaves the trigger and panel before\n it closes, default `300`.\n- `className` — layout around it (width, padding). Never colour.\n\n## Example\n\n```jsx\n// components/SiteHeader.jsx\n\"use client\";\nimport { usePathname } from \"next/navigation\";\nimport { Button, Icon } from \"./ui\";\nimport { NavigationMenu } from \"./blocks/navigation-menu\";\n\nconst NAV = [\n {\n label: \"Product\",\n items: [\n { label: \"Analytics\", href: \"/product/analytics\", icon: \"chart\", description: \"See which accounts are warming up.\" },\n { label: \"Automations\", href: \"/product/automations\", icon: \"zap\", description: \"Route leads without a spreadsheet.\" },\n { label: \"Integrations\", href: \"/product/integrations\", icon: \"link\", description: \"Sync with your CRM and calendar.\" },\n { label: \"Security\", href: \"/product/security\", icon: \"lock\", description: \"SSO, audit logs and SOC 2.\" },\n ],\n featured: { kicker: \"New\", title: \"Forecasts that explain themselves\", href: \"/changelog/forecasts\", cta: \"Read the release\" },\n },\n { label: \"Pricing\", href: \"/pricing\" },\n { label: \"Docs\", href: \"/docs\" },\n];\n\nexport default function SiteHeader() {\n const pathname = usePathname();\n return (\n <header className=\"border-b border-border bg-background px-4\">\n <NavigationMenu\n items={NAV}\n currentHref={pathname}\n brand={\n <Button href=\"/\" variant=\"quiet\" size=\"sm\" className=\"gap-2 font-semibold\">\n <Icon name=\"zap\" /> Fieldline\n </Button>\n }\n actions={\n <>\n <Button href=\"/login\" variant=\"quiet\" size=\"sm\">Sign in</Button>\n <Button href=\"/signup\" variant=\"primary\" size=\"sm\">Start free</Button>\n </>\n }\n />\n </header>\n );\n}\n```\n\n## What it will not do\n\n- Nest: a panel holds links, not another menu. A deeper tree is a page.\n- Hold arbitrary content in a panel: links and one featured card. A\n panel of your own is a `Popover` in your own component.\n- Stick to the top or blur on scroll: put `sticky top-0` on the element\n around it.\n- Know the route by itself: pass `currentHref`.\n\nA nav it cannot be is your own component in `components/`, built from\nthe kit — not an edit to this block. Its files are fixed: `npm run check`\nrefuses a change, and `makersclaw add navigation-menu` restores them.\n"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"categories": [
|
|
32
|
+
"navigation",
|
|
33
|
+
"nav",
|
|
34
|
+
"menu",
|
|
35
|
+
"mega-menu",
|
|
36
|
+
"header",
|
|
37
|
+
"navbar",
|
|
38
|
+
"top nav",
|
|
39
|
+
"dropdown",
|
|
40
|
+
"links",
|
|
41
|
+
"site",
|
|
42
|
+
"marketing",
|
|
43
|
+
"mobile menu"
|
|
44
|
+
],
|
|
45
|
+
"docs": "Installed to components/makersclaw/blocks/navigation-menu. Import: import { NavigationMenu } from \"@/components/makersclaw/blocks/navigation-menu\". Props and an example: components/makersclaw/blocks/navigation-menu/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
46
|
+
"meta": {
|
|
47
|
+
"makersclaw": {
|
|
48
|
+
"version": "0.3.1",
|
|
49
|
+
"useWhen": "The header of a public-facing site or a product with many destinations: a marketing site, a docs hub, a customer portal. Not for an app's own sidebar or a row of tabs that switch views on one page."
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "order-detail",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "OrderDetail",
|
|
6
|
+
"description": "One placed order on a page: its number, status and date, the items, the delivery address and contact, a timeline from placed to delivered (Steps), and the totals, with Track order and Invoice actions. Loading, error and not-found states built in.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.3.1/r/core.json"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "registry/blocks/order-detail/index.js",
|
|
14
|
+
"type": "registry:component",
|
|
15
|
+
"target": "@components/makersclaw/blocks/order-detail/index.js",
|
|
16
|
+
"content": "// The OrderDetail block — `makersclaw add order-detail`. Fixed: compose\n// it through its props (README.md beside this file); an order page it\n// cannot be is your own component in components/.\nexport { OrderDetail } from \"./order-detail\";\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "registry/blocks/order-detail/order-detail.jsx",
|
|
20
|
+
"type": "registry:component",
|
|
21
|
+
"target": "@components/makersclaw/blocks/order-detail/order-detail.jsx",
|
|
22
|
+
"content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { Badge, Button, Card, Empty, Icon, Image, Notice, Skeleton, Steps, cx, formatDate } from \"../../ui\";\n\n// One placed order: its number and status, the items, where it goes and\n// who hears about it, a timeline from placed to delivered, and the\n// totals. Read-only; the actions (track, invoice, edit contact) are the\n// app's through callbacks or links.\n\n// Server and browser may differ in locale, time zone and even the day:\n// the first render uses fixed ones (en-US, UTC) so it matches the server's\n// HTML, then the reader's own take over.\nfunction useHydrated() {\n const [hydrated, setHydrated] = useState(false);\n useEffect(() => setHydrated(true), []);\n return hydrated;\n}\n\nconst DEFAULT_STAGES = [\n { key: \"placed\", label: \"Order placed\" },\n { key: \"confirmed\", label: \"Confirmed\" },\n { key: \"shipped\", label: \"Shipped\" },\n { key: \"delivered\", label: \"Delivered\" },\n];\n\nconst STATUS_TONE = {\n placed: \"info\",\n confirmed: \"info\",\n processing: \"info\",\n shipped: \"primary\",\n \"in transit\": \"primary\",\n delivered: \"success\",\n cancelled: \"danger\",\n refunded: \"warning\",\n returned: \"warning\",\n};\n\nconst formats = new Map();\nfunction money(amount, currency = \"USD\", locale) {\n const key = `${locale ?? \"\"}|${currency}`;\n let f = formats.get(key);\n if (!f) {\n try {\n f = new Intl.NumberFormat(locale, { style: \"currency\", currency });\n } catch {\n f = new Intl.NumberFormat(locale, { style: \"currency\", currency: \"USD\" });\n }\n formats.set(key, f);\n }\n return f.format(Number(amount) || 0);\n}\n\n// \"2026-09-21\" or a full ISO timestamp → \"Sep 21, 2026\".\nfunction day(value, locale, timeZone) {\n if (!value) return \"\";\n if (typeof value === \"string\" && /^\\d{4}-\\d{2}-\\d{2}$/.test(value)) return formatDate(value, { dateStyle: \"medium\" }, locale);\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? String(value) : new Intl.DateTimeFormat(locale, { dateStyle: \"medium\", timeZone }).format(d);\n}\n\nconst num = (v) => (typeof v === \"number\" && Number.isFinite(v) ? v : 0);\n\n// The stages as Steps: before the reached one complete, the reached one\n// current (complete when it is the last), after it pending. A stage may\n// carry its own status (a failed delivery). A cancelled order stops where\n// it was and says so.\nfunction timeline(order, locale, timeZone) {\n const stages = order.timeline?.length ? order.timeline : DEFAULT_STAGES;\n const reached = Math.max(0, stages.findIndex((s) => s.key === order.stage));\n const cancelled = String(order.status ?? \"\").toLowerCase() === \"cancelled\";\n const steps = stages.map((s, i) => {\n let status = s.status;\n if (!status) {\n if (i < reached) status = \"complete\";\n else if (i === reached) status = cancelled ? \"failed\" : i === stages.length - 1 ? \"complete\" : \"current\";\n else status = cancelled ? \"skipped\" : \"pending\";\n }\n const when = s.date ? day(s.date, locale, timeZone) : null;\n const description = s.note ?? (when ? when : status === \"pending\" && i === stages.length - 1 && order.estimatedDelivery ? `Expected ${day(order.estimatedDelivery, locale, timeZone)}` : undefined);\n return { key: s.key ?? String(i), label: s.label, description, status };\n });\n return steps;\n}\n\nfunction Loading() {\n return (\n <div aria-busy=\"true\" aria-label=\"Loading the order\" className=\"grid gap-4\">\n <Skeleton className=\"h-7 w-48\" />\n <Skeleton className=\"w-64\" />\n <Card>\n <div className=\"flex gap-4\">\n <Skeleton shape=\"rect\" className=\"size-20\" />\n <div className=\"grid flex-1 content-start gap-2\">\n <Skeleton className=\"w-1/2\" />\n <Skeleton className=\"w-1/4\" />\n </div>\n </div>\n </Card>\n <Card>\n <div className=\"grid gap-3\">\n <Skeleton className=\"w-full\" />\n <Skeleton className=\"w-full\" />\n <Skeleton className=\"w-2/3\" />\n </div>\n </Card>\n </div>\n );\n}\n\nfunction Detail({ icon, title, action, children }) {\n return (\n <div className=\"grid content-start gap-2\">\n <div className=\"flex items-center gap-2\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-full bg-primary/10 text-primary\">\n <Icon name={icon} size={16} />\n </span>\n <h3 className=\"text-sm font-semibold\">{title}</h3>\n </div>\n <div className=\"text-sm text-muted-foreground\">{children}</div>\n {action}\n </div>\n );\n}\n\nfunction ActionButton({ label, icon, href, onAction, variant = \"secondary\", size = \"md\" }) {\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState(null);\n async function run() {\n setBusy(true);\n setError(null);\n try {\n const res = await onAction();\n if (res && res.ok === false) setError(res.error || \"That did not work.\");\n } catch (e) {\n setError(e?.message || \"That did not work.\");\n }\n setBusy(false);\n }\n return (\n <span className=\"grid justify-items-start gap-1\">\n <Button variant={variant} size={size} icon={icon} href={href} onClick={href ? undefined : run} loading={busy}>\n {label}\n </Button>\n {error ? (\n <span role=\"alert\" className=\"text-xs text-destructive\">\n {error}\n </span>\n ) : null}\n </span>\n );\n}\n\nexport function OrderDetail({\n order,\n currency: currencyProp,\n locale: localeProp,\n loading = false,\n error,\n onTrack,\n trackHref,\n trackLabel = \"Track order\",\n onInvoice,\n invoiceHref,\n invoiceLabel = \"Invoice\",\n onEditContact,\n emptyTitle = \"Order not found\",\n emptyBody = \"It may have been removed, or the link is out of date.\",\n className,\n}) {\n const hydrated = useHydrated();\n const locale = localeProp ?? (hydrated ? undefined : \"en-US\");\n const timeZone = hydrated ? undefined : \"UTC\";\n if (loading) return <Loading />;\n if (!order) {\n return (\n <div className={cx(\"grid gap-4\", className)}>\n {error ? <Notice tone=\"danger\">{error}</Notice> : null}\n <Empty icon=\"package\" title={emptyTitle} body={emptyBody} />\n </div>\n );\n }\n\n const currency = order.currency ?? currencyProp ?? \"USD\";\n const items = (order.items ?? []).filter(Boolean);\n const count = items.reduce((n, i) => n + (Number(i.quantity) || 0), 0);\n const subtotal = order.subtotal ?? items.reduce((s, i) => s + (Number(i.price) || 0) * (Number(i.quantity) || 0), 0);\n const discount = order.discount ? Math.abs(num(order.discount.amount ?? order.discount)) : 0;\n const total = order.total ?? Math.max(0, subtotal - discount + num(order.shipping) + num(order.tax));\n const status = order.status ? String(order.status) : null;\n const tone = order.statusTone ?? STATUS_TONE[status?.toLowerCase()] ?? \"neutral\";\n const address = Array.isArray(order.address) ? order.address : order.address ? String(order.address).split(\"\\n\") : [];\n const contact = order.contact ?? {};\n const canTrack = Boolean(onTrack || trackHref);\n\n const chargeRow = (label, v, freeLabel) =>\n v === undefined || v === null ? null : (\n <div className=\"flex justify-between gap-3\">\n <dt className=\"text-muted-foreground\">{label}</dt>\n <dd className={cx(\"font-medium tabular-nums\", v === 0 && freeLabel && \"text-success\")}>\n {typeof v === \"string\" ? v : v === 0 && freeLabel ? freeLabel : money(v, currency, locale)}\n </dd>\n </div>\n );\n\n return (\n <div className={cx(\"grid gap-5\", className)}>\n <header className=\"flex flex-wrap items-start justify-between gap-4\">\n <div className=\"grid min-w-0 gap-1.5\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <h2 className=\"text-xl font-semibold\">Order {order.number ?? order.id}</h2>\n {status ? (\n <Badge tone={tone} variant=\"soft\">\n {status}\n </Badge>\n ) : null}\n </div>\n <p className=\"flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted-foreground\">\n {order.placedAt ? (\n <span className=\"inline-flex items-center gap-1.5\">\n <Icon name=\"clock\" size={14} />\n Placed {day(order.placedAt, locale, timeZone)}\n </span>\n ) : null}\n <span className=\"inline-flex items-center gap-1.5\">\n <Icon name=\"package\" size={14} />\n {count} {count === 1 ? \"item\" : \"items\"}\n </span>\n </p>\n </div>\n {onInvoice || invoiceHref ? (\n <ActionButton label={invoiceLabel} icon=\"receipt\" href={invoiceHref} onAction={onInvoice} />\n ) : null}\n </header>\n\n {error ? <Notice tone=\"danger\">{error}</Notice> : null}\n\n <Card padding=\"none\">\n <div className=\"grid gap-6 p-4 sm:p-6 md:grid-cols-2\">\n <ul aria-label=\"Items in this order\" className=\"grid content-start gap-4\">\n {items.map((item) => (\n <li key={item.id} className=\"flex gap-3 sm:gap-4\">\n <Image src={item.image} alt=\"\" ratio=\"square\" fit=\"cover\" className=\"size-20 shrink-0 sm:size-24\" />\n <div className=\"grid min-w-0 flex-1 content-start gap-1\">\n <p className=\"font-medium leading-snug\">{item.name}</p>\n {item.variant ? <p className=\"text-sm text-muted-foreground\">{item.variant}</p> : null}\n <div className=\"mt-1 flex flex-wrap items-baseline justify-between gap-2\">\n <span className=\"text-sm text-muted-foreground\">Qty {item.quantity}</span>\n <span className=\"font-semibold tabular-nums\">\n {money((Number(item.price) || 0) * (Number(item.quantity) || 0), currency, locale)}\n </span>\n </div>\n </div>\n </li>\n ))}\n {!items.length ? <li className=\"text-sm text-muted-foreground\">No items on this order.</li> : null}\n </ul>\n\n <div className=\"grid content-start gap-6 sm:grid-cols-2\">\n {address.length ? (\n <Detail icon=\"map\" title=\"Delivery address\">\n <address className=\"not-italic leading-relaxed\">\n {address.map((line, i) => (\n <span key={i} className=\"block\">\n {line}\n </span>\n ))}\n </address>\n </Detail>\n ) : null}\n {contact.email || contact.phone ? (\n <Detail\n icon=\"mail\"\n title=\"Updates go to\"\n action={\n onEditContact ? (\n <ActionButton label=\"Edit contact\" size=\"sm\" onAction={onEditContact} />\n ) : null\n }\n >\n <div className=\"grid gap-1\">\n {contact.email ? (\n <span className=\"inline-flex min-w-0 items-center gap-1.5\">\n <Icon name=\"mail\" size={14} className=\"shrink-0\" />\n <span className=\"truncate\">{contact.email}</span>\n </span>\n ) : null}\n {contact.phone ? (\n <span className=\"inline-flex items-center gap-1.5\">\n <Icon name=\"phone\" size={14} className=\"shrink-0\" />\n {contact.phone}\n </span>\n ) : null}\n </div>\n </Detail>\n ) : null}\n </div>\n </div>\n\n <div className=\"grid gap-6 border-t border-border p-4 sm:p-6 lg:grid-cols-[1fr_20rem] lg:gap-10\">\n <div className=\"grid content-start gap-4\">\n <h3 className=\"font-semibold\">Timeline</h3>\n <Steps steps={timeline(order, locale, timeZone)} orientation=\"vertical\" label=\"Order progress\" hideLabel />\n </div>\n\n <div className=\"grid content-start gap-4 rounded-lg border border-border bg-muted/40 p-4\">\n <h3 className=\"font-semibold\">Summary</h3>\n <dl className=\"grid gap-2.5 text-sm\">\n {chargeRow(\"Subtotal\", subtotal)}\n {discount ? (\n <div className=\"flex justify-between gap-3\">\n <dt className=\"text-muted-foreground\">\n Discount{order.discount?.code ? ` (${order.discount.code})` : \"\"}\n </dt>\n <dd className=\"font-medium tabular-nums text-success\">−{money(discount, currency, locale)}</dd>\n </div>\n ) : null}\n {chargeRow(\"Shipping\", order.shipping, \"Free\")}\n {chargeRow(\"Tax\", order.tax)}\n <div className=\"mt-1.5 flex items-baseline justify-between gap-3 border-t border-border pt-3\">\n <dt className=\"text-base font-semibold\">Total</dt>\n <dd className=\"text-xl font-semibold tabular-nums\">{money(total, currency, locale)}</dd>\n </div>\n </dl>\n {order.paidWith ? (\n <p className=\"flex items-center gap-1.5 text-xs text-muted-foreground\">\n <Icon name=\"credit-card\" size={14} />\n Paid with {order.paidWith}\n </p>\n ) : null}\n {canTrack || order.estimatedDelivery ? (\n <div className=\"grid gap-3 rounded-md bg-primary/10 p-3\">\n <div className=\"flex items-start gap-2\">\n <Icon name=\"truck\" size={16} className=\"mt-0.5 shrink-0 text-primary\" />\n <div className=\"text-sm\">\n <p className=\"font-medium\">\n {order.stage === \"delivered\" ? \"Delivered\" : order.estimatedDelivery ? \"On its way\" : \"Tracking\"}\n </p>\n {order.estimatedDelivery && order.stage !== \"delivered\" ? (\n <p className=\"text-xs text-muted-foreground\">Expected {day(order.estimatedDelivery, locale, timeZone)}</p>\n ) : null}\n </div>\n </div>\n {canTrack ? (\n <ActionButton label={trackLabel} icon=\"external\" href={trackHref} onAction={onTrack} size=\"sm\" />\n ) : null}\n </div>\n ) : null}\n </div>\n </div>\n </Card>\n </div>\n );\n}\n"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "registry/blocks/order-detail/README.md",
|
|
26
|
+
"type": "registry:file",
|
|
27
|
+
"target": "@components/makersclaw/blocks/order-detail/README.md",
|
|
28
|
+
"content": "# OrderDetail\n\nOne placed order on a page. The header says the order's number, its\nstatus and when it was placed, with an Invoice action. Under it: the items,\nwhere the order is going and who hears about it, a timeline from placed to\ndelivered, and the totals with a Track order action.\n\nIt only shows. The actions are the app's: links, or callbacks that may\nreturn `{ ok: false, error }` and have the reason shown under the button.\n\n```jsx\nimport { OrderDetail } from \"../components/blocks/order-detail\"; // a Makersclaw app (\"./blocks/order-detail\" from components/)\nimport { OrderDetail } from \"@/components/makersclaw/blocks/order-detail\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component, and\nload the order in the server surface (README §4).\n\n## The data\n\nOne order:\n\n```js\n{\n number: \"#10482\", // or `id`\n status: \"Shipped\", // the badge's words\n stage: \"shipped\", // the timeline stage it has reached\n placedAt: \"2026-09-18\", // a date or an ISO timestamp\n estimatedDelivery: \"2026-09-24\",\n items: [{ id, name, image, price, quantity, variant }],\n address: [\"Maya Chen\", \"18 Harbour Street\", \"Leith, Edinburgh EH6 6PU\", \"United Kingdom\"],\n contact: { email: \"maya@northwind.co\", phone: \"+44 7700 900123\" },\n shipping: 0, tax: 21.4, discount: { code: \"WELCOME10\", amount: 18.4 },\n currency: \"GBP\",\n paidWith: \"Visa ending 4242\",\n timeline: [{ key: \"placed\", label: \"Order placed\", date: \"2026-09-18\" }, …],\n}\n```\n\n- `items` use the line-item shape `cart` uses: `price` is one unit, in the\n currency's major unit.\n- `address` is lines, or one string with line breaks.\n- `shipping` · `tax` — a number (0 shipping reads \"Free\") or words\n (\"Included\"). Leave one out and its row is not drawn.\n- `subtotal` · `total` — worked out from the items and charges when left\n out; pass them when the stored numbers are the truth.\n- `timeline` — the stages in order, `{ key, label, date?, note?, status? }`.\n Default: Order placed → Confirmed → Shipped → Delivered (keys `placed`,\n `confirmed`, `shipped`, `delivered`). Stages before `stage` are\n complete, `stage` is current (complete when it is the last), later ones\n pending; a stage's own `status` (`failed`, `skipped`…) wins. A\n `status` of \"Cancelled\" marks the reached stage failed and the rest\n skipped. A stage shows its `date`, or its `note`; the last pending one\n shows the expected delivery.\n- `statusTone` — the badge's tone when the status word is not one it knows\n (placed, confirmed, processing, shipped, in transit, delivered,\n cancelled, refunded, returned).\n\n## Props\n\n- `order` — the order above. `null` (and not loading) shows \"Order not\n found\".\n- `loading` — skeletons. `error` — a line under the header (the order\n loaded but something did not), or above the not-found state.\n- `currency` — used when the order has no `currency`; default `\"USD\"`.\n `locale` — for money and dates, default the reader's.\n- `onTrack()` or `trackHref` — Track order, in the summary. `trackLabel`\n renames it.\n- `onInvoice()` or `invoiceHref` — Invoice, in the header.\n `invoiceLabel` renames it.\n- `onEditContact()` — an Edit contact button under the contact details\n (open your `Sheet`).\n- `emptyTitle` · `emptyBody` — the not-found state's words.\n- `className` — layout around it (width, margin). Never colour.\n\n## Example\n\n```jsx\n// components/OrderPage.jsx — client\n\"use client\";\nimport { OrderDetail } from \"./blocks/order-detail\";\n\nexport default function OrderPage({ order, error }) {\n return (\n <OrderDetail\n order={order}\n error={error}\n trackHref={order?.trackingUrl}\n invoiceHref={order ? `/api/invoices/${order.id}` : undefined}\n />\n );\n}\n```\n\n## What it will not do\n\n- Change the order: cancelling, returning or editing items is your own\n screen, reached from `onEditContact` or a button beside it.\n- Fetch tracking: pass `stage` and the stage dates in, or link out with\n `trackHref`.\n- List orders: that is a `data-table`.\n\nAn order page it cannot be is your own component in `components/`, built\nfrom the kit — not an edit to this block. Its files are fixed: `npm run\ncheck` refuses a change, and `makersclaw add order-detail` restores them.\n\n## Credits\n\nShape and behaviour from Creative Tim UI (https://github.com/creativetimofficial/ui), MIT; this is a rewrite on the Makersclaw kit.\n"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"categories": [
|
|
32
|
+
"order",
|
|
33
|
+
"order detail",
|
|
34
|
+
"purchase",
|
|
35
|
+
"receipt",
|
|
36
|
+
"tracking",
|
|
37
|
+
"delivery",
|
|
38
|
+
"shipping",
|
|
39
|
+
"timeline",
|
|
40
|
+
"status",
|
|
41
|
+
"invoice",
|
|
42
|
+
"ecommerce",
|
|
43
|
+
"commerce",
|
|
44
|
+
"shop",
|
|
45
|
+
"store"
|
|
46
|
+
],
|
|
47
|
+
"docs": "Installed to components/makersclaw/blocks/order-detail. Import: import { OrderDetail } from \"@/components/makersclaw/blocks/order-detail\". Props and an example: components/makersclaw/blocks/order-detail/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
48
|
+
"meta": {
|
|
49
|
+
"makersclaw": {
|
|
50
|
+
"version": "0.3.1",
|
|
51
|
+
"useWhen": "A buyer or an operator opens one order: \"where is my order\", an order confirmation page, a support view of a purchase. Not for a list of orders (data-table) or for a basket before paying (cart)."
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|