makersclaw 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +180 -0
- package/package.json +3 -2
- package/r/annotate.json +62 -0
- package/r/approval-request.json +52 -0
- package/r/audio-player.json +74 -0
- package/r/auth-form.json +55 -0
- package/r/board.json +10 -6
- package/r/calendar-view.json +105 -0
- package/r/cart.json +62 -0
- package/r/chart.json +83 -0
- package/r/chat.json +95 -0
- package/r/code-editor.json +94 -0
- package/r/comment-box.json +64 -0
- package/r/contact-section.json +53 -0
- package/r/core.json +205 -8
- package/r/csv-import.json +63 -0
- package/r/data-table.json +69 -0
- package/r/diagram.json +74 -0
- package/r/doc-editor.json +82 -0
- package/r/doc-page.json +61 -0
- package/r/email-render.json +73 -0
- package/r/faq.json +50 -0
- package/r/file-upload.json +54 -0
- package/r/flow-canvas.json +93 -0
- package/r/footer.json +52 -0
- package/r/gantt.json +70 -0
- package/r/job-panel.json +12 -7
- package/r/json-editor.json +66 -0
- package/r/map.json +89 -0
- package/r/menubar.json +52 -0
- package/r/mind-map.json +67 -0
- package/r/navigation-menu.json +52 -0
- package/r/order-detail.json +54 -0
- package/r/org-chart.json +66 -0
- package/r/page-builder.json +105 -0
- package/r/payment-methods.json +51 -0
- package/r/pdf-document.json +79 -0
- package/r/pdf-viewer.json +88 -0
- package/r/product-detail.json +76 -0
- package/r/product-grid.json +65 -0
- package/r/record-form.json +67 -0
- package/r/record-notes.json +55 -0
- package/r/registry.json +3411 -29
- package/r/search-dialog.json +51 -0
- package/r/settings-list.json +66 -0
- package/r/setup-checklist.json +51 -0
- package/r/share-dialog.json +52 -0
- package/r/sidebar.json +69 -0
- package/r/signature-pad.json +72 -0
- package/r/sketch-canvas.json +99 -0
- package/r/spreadsheet.json +90 -0
- package/r/stat-cards.json +66 -0
- package/r/task-list.json +63 -0
- package/r/task-model.json +98 -0
- package/r/testimonials.json +52 -0
- package/r/transaction-list.json +53 -0
- package/r/whiteboard.json +66 -0
- package/registry/blocks/annotate/README.md +179 -0
- package/registry/blocks/annotate/annotate-data.js +250 -0
- package/registry/blocks/annotate/annotate.jsx +646 -0
- package/registry/blocks/annotate/block.json +10 -0
- package/registry/blocks/annotate/demo.jsx +135 -0
- package/registry/blocks/annotate/index.js +15 -0
- package/registry/blocks/approval-request/README.md +116 -0
- package/registry/blocks/approval-request/approval-request.jsx +386 -0
- package/registry/blocks/approval-request/block.json +10 -0
- package/registry/blocks/approval-request/demo.jsx +127 -0
- package/registry/blocks/approval-request/index.js +4 -0
- package/registry/blocks/audio-player/README.md +167 -0
- package/registry/blocks/audio-player/audio-player.jsx +623 -0
- package/registry/blocks/audio-player/block.json +14 -0
- package/registry/blocks/audio-player/colors.js +109 -0
- package/registry/blocks/audio-player/demo.jsx +209 -0
- package/registry/blocks/audio-player/index.js +6 -0
- package/registry/blocks/audio-player/time.js +67 -0
- package/registry/blocks/audio-player/wavesurfer.js +9 -0
- package/registry/blocks/auth-form/README.md +126 -0
- package/registry/blocks/auth-form/auth-form.jsx +568 -0
- package/registry/blocks/auth-form/block.json +10 -0
- package/registry/blocks/auth-form/demo.jsx +131 -0
- package/registry/blocks/auth-form/index.js +3 -0
- package/registry/blocks/board/README.md +55 -5
- package/registry/blocks/board/block.json +2 -2
- package/registry/blocks/board/board.jsx +230 -86
- package/registry/blocks/board/demo.jsx +103 -24
- package/registry/blocks/calendar-view/README.md +180 -0
- package/registry/blocks/calendar-view/agenda.jsx +147 -0
- package/registry/blocks/calendar-view/block.json +76 -0
- package/registry/blocks/calendar-view/calendar-model.js +442 -0
- package/registry/blocks/calendar-view/calendar.jsx +627 -0
- package/registry/blocks/calendar-view/demo.jsx +195 -0
- package/registry/blocks/calendar-view/draft.jsx +49 -0
- package/registry/blocks/calendar-view/index.js +39 -0
- package/registry/blocks/calendar-view/month.jsx +346 -0
- package/registry/blocks/calendar-view/paint.js +69 -0
- package/registry/blocks/calendar-view/time-grid.jsx +527 -0
- package/registry/blocks/calendar-view/tray.jsx +87 -0
- package/registry/blocks/cart/README.md +146 -0
- package/registry/blocks/cart/block.json +10 -0
- package/registry/blocks/cart/cart.jsx +597 -0
- package/registry/blocks/cart/demo.jsx +98 -0
- package/registry/blocks/cart/index.js +4 -0
- package/registry/blocks/cart/money.js +28 -0
- package/registry/blocks/chart/README.md +316 -0
- package/registry/blocks/chart/block.json +10 -0
- package/registry/blocks/chart/cartesian.jsx +642 -0
- package/registry/blocks/chart/core.jsx +262 -0
- package/registry/blocks/chart/demo.jsx +357 -0
- package/registry/blocks/chart/heatmap.jsx +336 -0
- package/registry/blocks/chart/index.js +8 -0
- package/registry/blocks/chart/pie.jsx +245 -0
- package/registry/blocks/chart/sparkline.jsx +88 -0
- package/registry/blocks/chat/README.md +273 -0
- package/registry/blocks/chat/attachment.jsx +95 -0
- package/registry/blocks/chat/block.json +10 -0
- package/registry/blocks/chat/chat.jsx +132 -0
- package/registry/blocks/chat/composer.jsx +364 -0
- package/registry/blocks/chat/demo.jsx +578 -0
- package/registry/blocks/chat/header.jsx +34 -0
- package/registry/blocks/chat/index.js +9 -0
- package/registry/blocks/chat/message.jsx +450 -0
- package/registry/blocks/chat/scroller.jsx +247 -0
- package/registry/blocks/chat/text.jsx +104 -0
- package/registry/blocks/code-editor/README.md +210 -0
- package/registry/blocks/code-editor/block.json +22 -0
- package/registry/blocks/code-editor/code-editor.jsx +534 -0
- package/registry/blocks/code-editor/codemirror.js +38 -0
- package/registry/blocks/code-editor/demo.jsx +205 -0
- package/registry/blocks/code-editor/index.js +8 -0
- package/registry/blocks/code-editor/json.js +317 -0
- package/registry/blocks/code-editor/sql.js +136 -0
- package/registry/blocks/code-editor/text.js +101 -0
- package/registry/blocks/code-editor/theme.js +186 -0
- package/registry/blocks/comment-box/README.md +123 -0
- package/registry/blocks/comment-box/block.json +10 -0
- package/registry/blocks/comment-box/comment-body.jsx +217 -0
- package/registry/blocks/comment-box/comment-box.jsx +198 -0
- package/registry/blocks/comment-box/comment-time.jsx +77 -0
- package/registry/blocks/comment-box/demo.jsx +141 -0
- package/registry/blocks/comment-box/index.js +8 -0
- package/registry/blocks/contact-section/README.md +152 -0
- package/registry/blocks/contact-section/block.json +11 -0
- package/registry/blocks/contact-section/contact-section.jsx +509 -0
- package/registry/blocks/contact-section/demo.jsx +93 -0
- package/registry/blocks/contact-section/index.js +4 -0
- package/registry/blocks/csv-import/README.md +133 -0
- package/registry/blocks/csv-import/block.json +13 -0
- package/registry/blocks/csv-import/csv-import.jsx +479 -0
- package/registry/blocks/csv-import/demo.jsx +106 -0
- package/registry/blocks/csv-import/import.js +200 -0
- package/registry/blocks/csv-import/index.js +7 -0
- package/registry/blocks/data-table/README.md +386 -0
- package/registry/blocks/data-table/block.json +10 -0
- package/registry/blocks/data-table/data-table.jsx +1164 -0
- package/registry/blocks/data-table/demo.jsx +588 -0
- package/registry/blocks/data-table/index.js +5 -0
- package/registry/blocks/data-table/model.js +169 -0
- package/registry/blocks/diagram/README.md +108 -0
- package/registry/blocks/diagram/block.json +13 -0
- package/registry/blocks/diagram/demo.jsx +113 -0
- package/registry/blocks/diagram/diagram.jsx +481 -0
- package/registry/blocks/diagram/index.js +7 -0
- package/registry/blocks/diagram/mermaid.js +52 -0
- package/registry/blocks/diagram/palette.js +313 -0
- package/registry/blocks/diagram/source.js +98 -0
- package/registry/blocks/doc-editor/README.md +215 -0
- package/registry/blocks/doc-editor/block.json +15 -0
- package/registry/blocks/doc-editor/demo.jsx +225 -0
- package/registry/blocks/doc-editor/doc-editor.jsx +291 -0
- package/registry/blocks/doc-editor/doc.js +309 -0
- package/registry/blocks/doc-editor/index.js +7 -0
- package/registry/blocks/doc-editor/schema.jsx +56 -0
- package/registry/blocks/doc-editor/shadcn-classes.js +110 -0
- package/registry/blocks/doc-editor/theme.js +82 -0
- package/registry/blocks/doc-page/README.md +119 -0
- package/registry/blocks/doc-page/autosave.js +104 -0
- package/registry/blocks/doc-page/block.json +10 -0
- package/registry/blocks/doc-page/demo.jsx +137 -0
- package/registry/blocks/doc-page/doc-page.jsx +276 -0
- package/registry/blocks/doc-page/index.js +4 -0
- package/registry/blocks/email-render/README.md +151 -0
- package/registry/blocks/email-render/block.json +14 -0
- package/registry/blocks/email-render/demo.jsx +153 -0
- package/registry/blocks/email-render/doc-email.jsx +210 -0
- package/registry/blocks/email-render/email.jsx +221 -0
- package/registry/blocks/email-render/index.js +10 -0
- package/registry/blocks/email-render/render.js +50 -0
- package/registry/blocks/email-render/theme.js +215 -0
- package/registry/blocks/faq/README.md +101 -0
- package/registry/blocks/faq/block.json +11 -0
- package/registry/blocks/faq/demo.jsx +89 -0
- package/registry/blocks/faq/faq.jsx +220 -0
- package/registry/blocks/faq/index.js +4 -0
- package/registry/blocks/file-upload/README.md +138 -0
- package/registry/blocks/file-upload/block.json +10 -0
- package/registry/blocks/file-upload/demo.jsx +204 -0
- package/registry/blocks/file-upload/file-upload.jsx +635 -0
- package/registry/blocks/file-upload/index.js +4 -0
- package/registry/blocks/flow-canvas/README.md +420 -0
- package/registry/blocks/flow-canvas/block.json +13 -0
- package/registry/blocks/flow-canvas/board.js +819 -0
- package/registry/blocks/flow-canvas/chrome.jsx +649 -0
- package/registry/blocks/flow-canvas/context.jsx +35 -0
- package/registry/blocks/flow-canvas/demo.jsx +249 -0
- package/registry/blocks/flow-canvas/edges.jsx +107 -0
- package/registry/blocks/flow-canvas/flow-canvas.jsx +1329 -0
- package/registry/blocks/flow-canvas/index.js +41 -0
- package/registry/blocks/flow-canvas/nodes.jsx +392 -0
- package/registry/blocks/flow-canvas/theme.js +174 -0
- package/registry/blocks/footer/README.md +133 -0
- package/registry/blocks/footer/block.json +11 -0
- package/registry/blocks/footer/demo.jsx +119 -0
- package/registry/blocks/footer/footer.jsx +406 -0
- package/registry/blocks/footer/index.js +4 -0
- package/registry/blocks/gantt/README.md +220 -0
- package/registry/blocks/gantt/block.json +10 -0
- package/registry/blocks/gantt/demo.jsx +148 -0
- package/registry/blocks/gantt/gantt.jsx +923 -0
- package/registry/blocks/gantt/index.js +7 -0
- package/registry/blocks/gantt/rows.jsx +468 -0
- package/registry/blocks/gantt/schedule.js +278 -0
- package/registry/blocks/job-panel/README.md +80 -0
- package/registry/blocks/job-panel/block.json +3 -3
- package/registry/blocks/job-panel/demo.jsx +230 -3
- package/registry/blocks/job-panel/job-panel.jsx +169 -3
- package/registry/blocks/json-editor/README.md +133 -0
- package/registry/blocks/json-editor/block.json +10 -0
- package/registry/blocks/json-editor/demo.jsx +92 -0
- package/registry/blocks/json-editor/index.js +7 -0
- package/registry/blocks/json-editor/json-editor.jsx +305 -0
- package/registry/blocks/json-editor/json-tree.jsx +393 -0
- package/registry/blocks/json-editor/tree.js +225 -0
- package/registry/blocks/map/README.md +221 -0
- package/registry/blocks/map/block.json +14 -0
- package/registry/blocks/map/demo.jsx +201 -0
- package/registry/blocks/map/geo.js +198 -0
- package/registry/blocks/map/index.js +8 -0
- package/registry/blocks/map/list.jsx +128 -0
- package/registry/blocks/map/map-view.jsx +715 -0
- package/registry/blocks/map/maplibre.js +30 -0
- package/registry/blocks/map/marks.jsx +146 -0
- package/registry/blocks/map/theme.js +142 -0
- package/registry/blocks/menubar/README.md +109 -0
- package/registry/blocks/menubar/block.json +10 -0
- package/registry/blocks/menubar/demo.jsx +128 -0
- package/registry/blocks/menubar/index.js +4 -0
- package/registry/blocks/menubar/menubar.jsx +199 -0
- package/registry/blocks/mind-map/README.md +206 -0
- package/registry/blocks/mind-map/block.json +10 -0
- package/registry/blocks/mind-map/demo.jsx +103 -0
- package/registry/blocks/mind-map/index.js +31 -0
- package/registry/blocks/mind-map/mind-map.jsx +1125 -0
- package/registry/blocks/mind-map/topic.jsx +143 -0
- package/registry/blocks/mind-map/tree.js +645 -0
- package/registry/blocks/navigation-menu/README.md +124 -0
- package/registry/blocks/navigation-menu/block.json +10 -0
- package/registry/blocks/navigation-menu/demo.jsx +84 -0
- package/registry/blocks/navigation-menu/index.js +4 -0
- package/registry/blocks/navigation-menu/navigation-menu.jsx +448 -0
- package/registry/blocks/order-detail/README.md +109 -0
- package/registry/blocks/order-detail/block.json +10 -0
- package/registry/blocks/order-detail/demo.jsx +88 -0
- package/registry/blocks/order-detail/index.js +4 -0
- package/registry/blocks/order-detail/order-detail.jsx +354 -0
- package/registry/blocks/org-chart/README.md +145 -0
- package/registry/blocks/org-chart/block.json +10 -0
- package/registry/blocks/org-chart/demo.jsx +94 -0
- package/registry/blocks/org-chart/index.js +7 -0
- package/registry/blocks/org-chart/org-chart.jsx +631 -0
- package/registry/blocks/org-chart/org.js +245 -0
- package/registry/blocks/org-chart/person.jsx +96 -0
- package/registry/blocks/page-builder/README.md +257 -0
- package/registry/blocks/page-builder/block.json +14 -0
- package/registry/blocks/page-builder/demo.jsx +145 -0
- package/registry/blocks/page-builder/grapes.js +18 -0
- package/registry/blocks/page-builder/index.js +11 -0
- package/registry/blocks/page-builder/page-builder.jsx +704 -0
- package/registry/blocks/page-builder/page-view.jsx +33 -0
- package/registry/blocks/page-builder/page.js +118 -0
- package/registry/blocks/page-builder/panels.jsx +588 -0
- package/registry/blocks/page-builder/sections.js +209 -0
- package/registry/blocks/page-builder/theme.js +87 -0
- package/registry/blocks/page-builder/thumbs.jsx +189 -0
- package/registry/blocks/page-builder/tokens.js +329 -0
- package/registry/blocks/payment-methods/README.md +104 -0
- package/registry/blocks/payment-methods/block.json +10 -0
- package/registry/blocks/payment-methods/demo.jsx +71 -0
- package/registry/blocks/payment-methods/index.js +4 -0
- package/registry/blocks/payment-methods/payment-methods.jsx +302 -0
- package/registry/blocks/pdf-document/README.md +213 -0
- package/registry/blocks/pdf-document/block.json +13 -0
- package/registry/blocks/pdf-document/demo.jsx +136 -0
- package/registry/blocks/pdf-document/index.js +33 -0
- package/registry/blocks/pdf-document/money.js +83 -0
- package/registry/blocks/pdf-document/pdf-document.jsx +144 -0
- package/registry/blocks/pdf-document/primitives.jsx +271 -0
- package/registry/blocks/pdf-document/quote.jsx +179 -0
- package/registry/blocks/pdf-document/theme.js +193 -0
- package/registry/blocks/pdf-viewer/README.md +191 -0
- package/registry/blocks/pdf-viewer/block.json +14 -0
- package/registry/blocks/pdf-viewer/demo.jsx +252 -0
- package/registry/blocks/pdf-viewer/helpers.js +270 -0
- package/registry/blocks/pdf-viewer/index.js +8 -0
- package/registry/blocks/pdf-viewer/notes.jsx +195 -0
- package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1368 -0
- package/registry/blocks/pdf-viewer/pdfjs.js +37 -0
- package/registry/blocks/pdf-viewer/ssr-shim-end.js +6 -0
- package/registry/blocks/pdf-viewer/ssr-shim.js +44 -0
- package/registry/blocks/product-detail/README.md +217 -0
- package/registry/blocks/product-detail/block.json +10 -0
- package/registry/blocks/product-detail/demo.jsx +233 -0
- package/registry/blocks/product-detail/index.js +7 -0
- package/registry/blocks/product-detail/price.jsx +115 -0
- package/registry/blocks/product-detail/product-detail.jsx +348 -0
- package/registry/blocks/product-detail/product-media.jsx +102 -0
- package/registry/blocks/product-detail/product-options.jsx +144 -0
- package/registry/blocks/product-grid/README.md +162 -0
- package/registry/blocks/product-grid/block.json +10 -0
- package/registry/blocks/product-grid/demo.jsx +255 -0
- package/registry/blocks/product-grid/index.js +5 -0
- package/registry/blocks/product-grid/product-card.jsx +232 -0
- package/registry/blocks/product-grid/product-grid.jsx +235 -0
- package/registry/blocks/record-form/README.md +196 -0
- package/registry/blocks/record-form/block.json +10 -0
- package/registry/blocks/record-form/demo.jsx +525 -0
- package/registry/blocks/record-form/index.js +4 -0
- package/registry/blocks/record-form/record-fields.jsx +457 -0
- package/registry/blocks/record-form/record-form.jsx +415 -0
- package/registry/blocks/record-form/rules.js +81 -0
- package/registry/blocks/record-notes/README.md +139 -0
- package/registry/blocks/record-notes/block.json +10 -0
- package/registry/blocks/record-notes/demo.jsx +179 -0
- package/registry/blocks/record-notes/index.js +4 -0
- package/registry/blocks/record-notes/record-notes.jsx +266 -0
- package/registry/blocks/search-dialog/README.md +108 -0
- package/registry/blocks/search-dialog/block.json +10 -0
- package/registry/blocks/search-dialog/demo.jsx +116 -0
- package/registry/blocks/search-dialog/index.js +3 -0
- package/registry/blocks/search-dialog/search-dialog.jsx +235 -0
- package/registry/blocks/settings-list/README.md +171 -0
- package/registry/blocks/settings-list/block.json +10 -0
- package/registry/blocks/settings-list/demo.jsx +333 -0
- package/registry/blocks/settings-list/index.js +4 -0
- package/registry/blocks/settings-list/row-state.jsx +93 -0
- package/registry/blocks/settings-list/settings-list.jsx +349 -0
- package/registry/blocks/settings-list/settings-matrix.jsx +118 -0
- package/registry/blocks/setup-checklist/README.md +142 -0
- package/registry/blocks/setup-checklist/block.json +10 -0
- package/registry/blocks/setup-checklist/demo.jsx +159 -0
- package/registry/blocks/setup-checklist/index.js +4 -0
- package/registry/blocks/setup-checklist/setup-checklist.jsx +446 -0
- package/registry/blocks/share-dialog/README.md +113 -0
- package/registry/blocks/share-dialog/block.json +10 -0
- package/registry/blocks/share-dialog/demo.jsx +140 -0
- package/registry/blocks/share-dialog/index.js +3 -0
- package/registry/blocks/share-dialog/share-dialog.jsx +384 -0
- package/registry/blocks/sidebar/README.md +190 -0
- package/registry/blocks/sidebar/block.json +43 -0
- package/registry/blocks/sidebar/demo.jsx +577 -0
- package/registry/blocks/sidebar/index.js +4 -0
- package/registry/blocks/sidebar/sidebar-panel.jsx +149 -0
- package/registry/blocks/sidebar/sidebar.jsx +864 -0
- package/registry/blocks/signature-pad/README.md +137 -0
- package/registry/blocks/signature-pad/block.json +13 -0
- package/registry/blocks/signature-pad/colors.js +115 -0
- package/registry/blocks/signature-pad/demo.jsx +94 -0
- package/registry/blocks/signature-pad/index.js +6 -0
- package/registry/blocks/signature-pad/pad.js +7 -0
- package/registry/blocks/signature-pad/signature-pad.jsx +434 -0
- package/registry/blocks/signature-pad/signature.js +68 -0
- package/registry/blocks/sketch-canvas/README.md +280 -0
- package/registry/blocks/sketch-canvas/block.json +13 -0
- package/registry/blocks/sketch-canvas/demo.jsx +159 -0
- package/registry/blocks/sketch-canvas/excalidraw.js +40 -0
- package/registry/blocks/sketch-canvas/index.js +9 -0
- package/registry/blocks/sketch-canvas/palette.js +226 -0
- package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1188 -0
- package/registry/blocks/sketch-canvas/sketch.js +179 -0
- package/registry/blocks/sketch-canvas/ssr-shim-end.js +6 -0
- package/registry/blocks/sketch-canvas/ssr-shim.js +45 -0
- package/registry/blocks/sketch-canvas/stamps.js +104 -0
- package/registry/blocks/sketch-canvas/theme.js +131 -0
- package/registry/blocks/spreadsheet/README.md +307 -0
- package/registry/blocks/spreadsheet/block.json +13 -0
- package/registry/blocks/spreadsheet/cells.jsx +455 -0
- package/registry/blocks/spreadsheet/chrome.jsx +325 -0
- package/registry/blocks/spreadsheet/demo.jsx +177 -0
- package/registry/blocks/spreadsheet/grid.js +387 -0
- package/registry/blocks/spreadsheet/index.js +34 -0
- package/registry/blocks/spreadsheet/spreadsheet.jsx +1732 -0
- package/registry/blocks/spreadsheet/types.js +473 -0
- package/registry/blocks/stat-cards/README.md +173 -0
- package/registry/blocks/stat-cards/block.json +10 -0
- package/registry/blocks/stat-cards/demo.jsx +257 -0
- package/registry/blocks/stat-cards/index.js +4 -0
- package/registry/blocks/stat-cards/stat-cards.jsx +307 -0
- package/registry/blocks/stat-cards/usage.jsx +243 -0
- package/registry/blocks/task-list/README.md +176 -0
- package/registry/blocks/task-list/block.json +10 -0
- package/registry/blocks/task-list/demo.jsx +134 -0
- package/registry/blocks/task-list/index.js +5 -0
- package/registry/blocks/task-list/task-list.jsx +907 -0
- package/registry/blocks/task-model/README.md +455 -0
- package/registry/blocks/task-model/block.json +167 -0
- package/registry/blocks/task-model/changes.js +89 -0
- package/registry/blocks/task-model/dates.js +371 -0
- package/registry/blocks/task-model/demo.jsx +215 -0
- package/registry/blocks/task-model/dnd.jsx +240 -0
- package/registry/blocks/task-model/index.js +97 -0
- package/registry/blocks/task-model/model.js +919 -0
- package/registry/blocks/task-model/parts.jsx +449 -0
- package/registry/blocks/task-model/toolbar.jsx +271 -0
- package/registry/blocks/testimonials/README.md +139 -0
- package/registry/blocks/testimonials/block.json +11 -0
- package/registry/blocks/testimonials/demo.jsx +137 -0
- package/registry/blocks/testimonials/index.js +4 -0
- package/registry/blocks/testimonials/testimonials.jsx +430 -0
- package/registry/blocks/transaction-list/README.md +126 -0
- package/registry/blocks/transaction-list/block.json +10 -0
- package/registry/blocks/transaction-list/demo.jsx +79 -0
- package/registry/blocks/transaction-list/index.js +4 -0
- package/registry/blocks/transaction-list/transaction-list.jsx +383 -0
- package/registry/blocks/whiteboard/README.md +179 -0
- package/registry/blocks/whiteboard/block.json +10 -0
- package/registry/blocks/whiteboard/demo.jsx +169 -0
- package/registry/blocks/whiteboard/index.js +6 -0
- package/registry/blocks/whiteboard/templates.js +150 -0
- package/registry/blocks/whiteboard/whiteboard.jsx +721 -0
- package/src/cli.mjs +19 -6
- package/src/install.mjs +18 -0
- package/src/registry.mjs +38 -4
- package/src/shadcn.mjs +14 -1
- package/templates/next/files/README.md +145 -11
- package/templates/next/files/app/globals.css +55 -4
- package/templates/next/files/check/run.mjs +3 -3
- package/templates/next/files/components/ui/avatar.jsx +141 -0
- package/templates/next/files/components/ui/breadcrumb.jsx +149 -0
- package/templates/next/files/components/ui/button-group.jsx +70 -0
- package/templates/next/files/components/ui/button.jsx +132 -12
- package/templates/next/files/components/ui/calendar.jsx +609 -0
- package/templates/next/files/components/ui/carousel.jsx +281 -0
- package/templates/next/files/components/ui/collapsible.jsx +208 -0
- package/templates/next/files/components/ui/combobox.jsx +608 -0
- package/templates/next/files/components/ui/command.jsx +577 -0
- package/templates/next/files/components/ui/context-menu.jsx +191 -0
- package/templates/next/files/components/ui/date-picker.jsx +360 -0
- package/templates/next/files/components/ui/distribution.jsx +117 -0
- package/templates/next/files/components/ui/feedback.jsx +143 -0
- package/templates/next/files/components/ui/field.jsx +115 -3
- package/templates/next/files/components/ui/file-input.jsx +234 -0
- package/templates/next/files/components/ui/icon.jsx +130 -8
- package/templates/next/files/components/ui/image.jsx +143 -0
- package/templates/next/files/components/ui/index.js +47 -2
- package/templates/next/files/components/ui/input-group.jsx +213 -0
- package/templates/next/files/components/ui/input-otp.jsx +170 -0
- package/templates/next/files/components/ui/item.jsx +206 -0
- package/templates/next/files/components/ui/menu.jsx +454 -82
- package/templates/next/files/components/ui/notice.jsx +126 -14
- package/templates/next/files/components/ui/pagination.jsx +156 -0
- package/templates/next/files/components/ui/popover.jsx +284 -0
- package/templates/next/files/components/ui/radio.jsx +193 -0
- package/templates/next/files/components/ui/rating.jsx +131 -0
- package/templates/next/files/components/ui/resizable.jsx +338 -0
- package/templates/next/files/components/ui/scroll-area.jsx +123 -0
- package/templates/next/files/components/ui/separator.jsx +119 -0
- package/templates/next/files/components/ui/sheet.jsx +96 -11
- package/templates/next/files/components/ui/slider.jsx +174 -0
- package/templates/next/files/components/ui/stat.jsx +574 -0
- package/templates/next/files/components/ui/steps.jsx +338 -0
- package/templates/next/files/components/ui/swatch.jsx +33 -0
- package/templates/next/files/components/ui/table.jsx +130 -0
- package/templates/next/files/components/ui/toast.jsx +440 -0
- package/templates/next/files/components/ui/toggle.jsx +195 -0
- package/templates/next/files/components/ui/tooltip.jsx +270 -0
- package/templates/next/template.json +264 -8
|
@@ -0,0 +1,1188 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// SketchCanvas — Excalidraw, in the workspace theme, behind one set of props.
|
|
4
|
+
//
|
|
5
|
+
// Client only, one static bundle. Excalidraw needs the DOM, and the app's
|
|
6
|
+
// check refuses import() (so no next/dynamic): the modules load with the
|
|
7
|
+
// page (on the server too — see excalidraw.js and ssr-shim.js for how that
|
|
8
|
+
// is made safe), and the canvas itself mounts only after the first effect
|
|
9
|
+
// (the mount guard). Until then — on the server and in the first client
|
|
10
|
+
// paint — the same frame shows, with the sketch's words for a reader.
|
|
11
|
+
//
|
|
12
|
+
// What is ours and what is Excalidraw's: Excalidraw draws, selects, edits
|
|
13
|
+
// text, keeps history and answers its own shortcuts. The tool rail, the
|
|
14
|
+
// context toolbar (colours from the theme), the zoom and undo buttons, the
|
|
15
|
+
// menu, the save state and the empty state are ours, in the kit; theme.js
|
|
16
|
+
// hides the Excalidraw UI they replace. Where ours drives an Excalidraw
|
|
17
|
+
// action that has no API (undo, zoom, layer order, duplicate, delete,
|
|
18
|
+
// font size) it presses that action's own shortcut on the canvas, so the
|
|
19
|
+
// behaviour is Excalidraw's exactly.
|
|
20
|
+
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
21
|
+
import { Button, Confirm, Icon, IconButton, Kbd, Menu, Popover, Separator, Toggle, Tooltip, cx } from "../../ui";
|
|
22
|
+
import {
|
|
23
|
+
CaptureUpdateAction,
|
|
24
|
+
Excalidraw,
|
|
25
|
+
convertToExcalidrawElements,
|
|
26
|
+
exportToBlob,
|
|
27
|
+
getCommonBounds,
|
|
28
|
+
getSceneVersion,
|
|
29
|
+
newElementWith,
|
|
30
|
+
restoreElements,
|
|
31
|
+
sceneCoordsToViewportCoords,
|
|
32
|
+
viewportCoordsToSceneCoords,
|
|
33
|
+
} from "./excalidraw";
|
|
34
|
+
import { FILLS, STROKES, elementTokens, paletteKey, repaint, resolveColours, resolvePalette, tagTokens } from "./palette";
|
|
35
|
+
import { BACKGROUND_ROLE, SKETCH_VERSION, SYSTEM_FONT, filesFor, isBackground, pickAppState, sketchToText, toSketch } from "./sketch";
|
|
36
|
+
import { ROOT_VARS, THEME, TRIM_CSS } from "./theme";
|
|
37
|
+
|
|
38
|
+
/** A ref for the canvas's handle: `const sketch = useSketchCanvas()`, then
|
|
39
|
+
* `<SketchCanvas canvasRef={sketch} />` and `await sketch.current?.exportImage()`. */
|
|
40
|
+
export function useSketchCanvas() {
|
|
41
|
+
return useRef(null);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The built-in tools, in rail order. `tools` picks a subset by id. */
|
|
45
|
+
export const SKETCH_TOOLS = [
|
|
46
|
+
{ id: "select", tool: "selection", label: "Select", icon: "mouse-pointer", key: "V" },
|
|
47
|
+
{ id: "hand", tool: "hand", label: "Pan", icon: "hand", key: "H" },
|
|
48
|
+
{ id: "draw", tool: "freedraw", label: "Draw", icon: "pencil", key: "P" },
|
|
49
|
+
{ id: "rectangle", tool: "rectangle", label: "Rectangle", icon: "square", key: "R" },
|
|
50
|
+
{ id: "ellipse", tool: "ellipse", label: "Ellipse", icon: "circle", key: "O" },
|
|
51
|
+
{ id: "diamond", tool: "diamond", label: "Diamond", icon: "diamond", key: "D" },
|
|
52
|
+
{ id: "arrow", tool: "arrow", label: "Arrow", icon: "arrow-right", key: "A" },
|
|
53
|
+
{ id: "line", tool: "line", label: "Line", icon: "minus", key: "L" },
|
|
54
|
+
{ id: "text", tool: "text", label: "Text", icon: "type", key: "T" },
|
|
55
|
+
{ id: "image", tool: "image", label: "Image", icon: "image", key: null },
|
|
56
|
+
{ id: "eraser", tool: "eraser", label: "Eraser", icon: "eraser", key: "E" },
|
|
57
|
+
{ id: "frame", tool: "frame", label: "Frame", icon: "frame", key: "F" },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// Excalidraw's own single-key tool shortcuts; a tool left out of `tools`
|
|
61
|
+
// has its keys swallowed so it cannot be reached by keyboard either.
|
|
62
|
+
const TOOL_KEYS = {
|
|
63
|
+
selection: ["v", "1"],
|
|
64
|
+
rectangle: ["r", "2"],
|
|
65
|
+
diamond: ["d", "3"],
|
|
66
|
+
ellipse: ["o", "4"],
|
|
67
|
+
arrow: ["a", "5"],
|
|
68
|
+
line: ["l", "6"],
|
|
69
|
+
freedraw: ["p", "x", "7"],
|
|
70
|
+
text: ["t", "8"],
|
|
71
|
+
image: ["9"],
|
|
72
|
+
eraser: ["e", "0"],
|
|
73
|
+
hand: ["h"],
|
|
74
|
+
frame: ["f"],
|
|
75
|
+
laser: ["k"],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Tailwind needs whole class names in the source: one per token.
|
|
79
|
+
const SWATCH = {
|
|
80
|
+
foreground: "bg-foreground",
|
|
81
|
+
"muted-foreground": "bg-muted-foreground",
|
|
82
|
+
primary: "bg-primary",
|
|
83
|
+
destructive: "bg-destructive",
|
|
84
|
+
warning: "bg-warning",
|
|
85
|
+
success: "bg-success",
|
|
86
|
+
info: "bg-info",
|
|
87
|
+
"chart-2": "bg-chart-2",
|
|
88
|
+
"chart-3": "bg-chart-3",
|
|
89
|
+
"chart-4": "bg-chart-4",
|
|
90
|
+
"chart-5": "bg-chart-5",
|
|
91
|
+
none: "bg-background",
|
|
92
|
+
muted: "bg-muted",
|
|
93
|
+
"primary:soft": "bg-primary/25",
|
|
94
|
+
"destructive:soft": "bg-destructive/25",
|
|
95
|
+
"warning:soft": "bg-warning/25",
|
|
96
|
+
"success:soft": "bg-success/25",
|
|
97
|
+
"info:soft": "bg-info/25",
|
|
98
|
+
"chart-2:soft": "bg-chart-2/25",
|
|
99
|
+
"chart-3:soft": "bg-chart-3/25",
|
|
100
|
+
"chart-4:soft": "bg-chart-4/25",
|
|
101
|
+
"chart-5:soft": "bg-chart-5/25",
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const WIDTHS = [
|
|
105
|
+
{ value: 1, label: "Thin" },
|
|
106
|
+
{ value: 2, label: "Medium" },
|
|
107
|
+
{ value: 4, label: "Bold" },
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const BG_ID = "sketch-background";
|
|
111
|
+
const SHAPES = new Set(["rectangle", "ellipse", "diamond"]);
|
|
112
|
+
const LINES = new Set(["arrow", "line", "freedraw"]);
|
|
113
|
+
const isDarwin = () => typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
|
114
|
+
const nonEmpty = (v) => (v && (Array.isArray(v) ? v.length : Array.isArray(v.elements) ? v.elements.length : false) ? v : null);
|
|
115
|
+
|
|
116
|
+
/** A short stable hash (for a background file id). */
|
|
117
|
+
function hash(s) {
|
|
118
|
+
let h = 5381;
|
|
119
|
+
for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) >>> 0;
|
|
120
|
+
return h.toString(36);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function mimeOf(src) {
|
|
124
|
+
const m = /^data:([^;,]+)/.exec(src);
|
|
125
|
+
if (m) return m[1];
|
|
126
|
+
const ext = /\.(png|jpe?g|webp|gif|svg)(?:[?#]|$)/i.exec(src)?.[1]?.toLowerCase();
|
|
127
|
+
return { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", webp: "image/webp", gif: "image/gif", svg: "image/svg+xml" }[ext] ?? "image/png";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The workspace's light or dark where the canvas sits. */
|
|
131
|
+
function schemeOf(root) {
|
|
132
|
+
const scoped = root?.closest?.("[data-theme]")?.getAttribute("data-theme");
|
|
133
|
+
if (scoped === "light" || scoped === "dark") return scoped;
|
|
134
|
+
const cs = root ? getComputedStyle(root).colorScheme || "" : "";
|
|
135
|
+
if (/\bdark\b/.test(cs) && !/\blight\b/.test(cs)) return "dark";
|
|
136
|
+
if (/\blight\b/.test(cs) && !/\bdark\b/.test(cs)) return "light";
|
|
137
|
+
return typeof matchMedia !== "undefined" && matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** What is saved of the scene: live elements with their tokens, the
|
|
141
|
+
* files they show, the appState a sketch keeps. */
|
|
142
|
+
function sceneValue(api, palette) {
|
|
143
|
+
return valueOf(api.getSceneElements(), api.getFiles(), api.getAppState(), palette);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function valueOf(elements, files, appState, palette) {
|
|
147
|
+
const live = tagTokens(
|
|
148
|
+
elements.filter((el) => !el.isDeleted && !isBackground(el)),
|
|
149
|
+
palette,
|
|
150
|
+
);
|
|
151
|
+
return { type: "sketch", version: SKETCH_VERSION, elements: live, files: filesFor(live, files), appState: pickAppState(appState) };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** A cheap fingerprint of a value's elements and files. */
|
|
155
|
+
function signature(value) {
|
|
156
|
+
const s = toSketch(value);
|
|
157
|
+
return `${s.elements.map((e) => `${e.id}:${e.version}`).join(",")}|${Object.keys(s.files).sort().join(",")}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** A box never shorter than the text bound inside it. Excalidraw re-wraps
|
|
161
|
+
* and re-measures text as a scene loads (the system font, a narrower box)
|
|
162
|
+
* but grows a container only while someone types, so text that wrapped to
|
|
163
|
+
* an extra line hung out of its box ("Annual billing toggle" on the
|
|
164
|
+
* whiteboard demo, seen in the pane 2026-09-24). Grows the box and centres
|
|
165
|
+
* the text again; ellipses and diamonds need more room for the same text. */
|
|
166
|
+
const BOUND_PAD = 5;
|
|
167
|
+
function fitContainers(elements) {
|
|
168
|
+
const byId = new Map(elements.map((el) => [el.id, el]));
|
|
169
|
+
const grown = new Map();
|
|
170
|
+
for (const t of elements) {
|
|
171
|
+
if (t?.type !== "text" || !t.containerId || t.isDeleted) continue;
|
|
172
|
+
const box = grown.get(t.containerId) ?? byId.get(t.containerId);
|
|
173
|
+
if (!box || box.type === "arrow" || box.type === "line") continue;
|
|
174
|
+
// A rectangle needs the text plus its padding; an ellipse or diamond
|
|
175
|
+
// fits text of about 1/√2 of its height (a numbered pin: 18px of text
|
|
176
|
+
// in a 28px circle is fine, and must stay a circle).
|
|
177
|
+
const round = box.type === "ellipse" || box.type === "diamond";
|
|
178
|
+
const need = Math.ceil(round ? t.height * Math.SQRT2 : t.height + BOUND_PAD * 2);
|
|
179
|
+
if (box.height + 1 >= need) continue;
|
|
180
|
+
grown.set(box.id, { ...box, height: need, version: (box.version ?? 1) + 1 });
|
|
181
|
+
}
|
|
182
|
+
if (!grown.size) return elements;
|
|
183
|
+
return elements.map((el) => {
|
|
184
|
+
if (grown.has(el.id)) return grown.get(el.id);
|
|
185
|
+
if (el?.type === "text" && grown.has(el.containerId)) {
|
|
186
|
+
const box = grown.get(el.containerId);
|
|
187
|
+
return { ...el, y: box.y + (box.height - el.height) / 2, version: (el.version ?? 1) + 1 };
|
|
188
|
+
}
|
|
189
|
+
return el;
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Elements as they should enter the scene: every text in the system
|
|
194
|
+
* font (re-measured), tokened colours resolved in this theme. */
|
|
195
|
+
function prepareElements(elements, palette) {
|
|
196
|
+
const fonted = (elements ?? []).map((el) => (el?.type === "text" && el.fontFamily !== SYSTEM_FONT ? { ...el, fontFamily: SYSTEM_FONT } : el));
|
|
197
|
+
const restored = fitContainers(restoreElements(fonted, null, { refreshDimensions: true, repairBindings: true }));
|
|
198
|
+
const patches = new Map(repaint(restored, palette, null).map((p) => [p.id, p.patch]));
|
|
199
|
+
return restored.map((el) => (patches.has(el.id) ? { ...el, ...patches.get(el.id) } : el));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** A skeleton (convertToExcalidrawElements' input) ready to convert:
|
|
203
|
+
* token colours resolved, texts in the system font. */
|
|
204
|
+
function prepareSkeleton(sk, palette) {
|
|
205
|
+
const out = resolveColours(sk, palette);
|
|
206
|
+
if (out.type === "text" && !out.fontFamily) out.fontFamily = SYSTEM_FONT;
|
|
207
|
+
if (out.label && !out.label.fontFamily) out.label = { ...out.label, fontFamily: SYSTEM_FONT };
|
|
208
|
+
if (out.type !== "text" && !out.strokeColor && palette.foreground) out.strokeColor = palette.foreground;
|
|
209
|
+
if (out.type === "text" && !out.strokeColor && palette.foreground) out.strokeColor = palette.foreground;
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function boundsOfSkeletons(sks) {
|
|
214
|
+
const boxes = sks.map((s) => {
|
|
215
|
+
const w = s.width ?? (s.type === "text" ? String(s.text ?? "").length * (s.fontSize ?? 20) * 0.55 : 0);
|
|
216
|
+
const h = s.height ?? (s.type === "text" ? (s.fontSize ?? 20) * 1.25 : 0);
|
|
217
|
+
return { x1: s.x ?? 0, y1: s.y ?? 0, x2: (s.x ?? 0) + w, y2: (s.y ?? 0) + h };
|
|
218
|
+
});
|
|
219
|
+
const x1 = Math.min(...boxes.map((b) => b.x1));
|
|
220
|
+
const y1 = Math.min(...boxes.map((b) => b.y1));
|
|
221
|
+
return { x: x1, y: y1, width: Math.max(...boxes.map((b) => b.x2)) - x1, height: Math.max(...boxes.map((b) => b.y2)) - y1 };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function SketchCanvas(props) {
|
|
225
|
+
const [mounted, setMounted] = useState(false);
|
|
226
|
+
useEffect(() => setMounted(true), []);
|
|
227
|
+
return <Frame {...props} mounted={mounted} />;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** The frame, the same before and after the canvas mounts: server HTML
|
|
231
|
+
* already has its size, border and text alternative; nothing jumps. */
|
|
232
|
+
function Frame({ mounted, className, label = "Sketch", ...props }) {
|
|
233
|
+
const rootRef = useRef(null);
|
|
234
|
+
const descId = useId();
|
|
235
|
+
const [text, setText] = useState(() => sketchToText(nonEmpty(props.value) ?? nonEmpty(props.defaultValue)));
|
|
236
|
+
const sized = /(^|\s)(h-|min-h-|size-|flex-1|grow)/.test(className ?? "");
|
|
237
|
+
return (
|
|
238
|
+
<div
|
|
239
|
+
ref={rootRef}
|
|
240
|
+
data-sketch-canvas=""
|
|
241
|
+
role="group"
|
|
242
|
+
aria-label={label}
|
|
243
|
+
aria-describedby={descId}
|
|
244
|
+
className={cx(
|
|
245
|
+
"relative isolate overflow-hidden rounded-lg border border-border bg-background text-foreground",
|
|
246
|
+
!sized && "h-[32rem]",
|
|
247
|
+
ROOT_VARS,
|
|
248
|
+
THEME,
|
|
249
|
+
className,
|
|
250
|
+
)}
|
|
251
|
+
>
|
|
252
|
+
<style>{TRIM_CSS}</style>
|
|
253
|
+
<p id={descId} className="sr-only">
|
|
254
|
+
{text ? `The sketch says: ${text.replace(/\n+/g, ". ")}` : "The sketch is empty."}
|
|
255
|
+
</p>
|
|
256
|
+
{mounted ? (
|
|
257
|
+
<LiveSketch {...props} rootRef={rootRef} onText={setText} />
|
|
258
|
+
) : (
|
|
259
|
+
<div className="grid h-full place-items-center text-sm text-muted-foreground" aria-busy="true">
|
|
260
|
+
Loading the canvas…
|
|
261
|
+
</div>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function LiveSketch({
|
|
268
|
+
rootRef,
|
|
269
|
+
onText,
|
|
270
|
+
value,
|
|
271
|
+
defaultValue,
|
|
272
|
+
onChange,
|
|
273
|
+
onSave,
|
|
274
|
+
changeDelay = 600,
|
|
275
|
+
readOnly = false,
|
|
276
|
+
background,
|
|
277
|
+
tools,
|
|
278
|
+
stamps,
|
|
279
|
+
grid = true,
|
|
280
|
+
empty,
|
|
281
|
+
actions,
|
|
282
|
+
canvasRef,
|
|
283
|
+
onReady,
|
|
284
|
+
}) {
|
|
285
|
+
const apiRef = useRef(null);
|
|
286
|
+
const [api, setApi] = useState(null);
|
|
287
|
+
// Excalidraw hands over its API before it has loaded initialData, then
|
|
288
|
+
// loads it asynchronously and replaces the scene. Anything laid before
|
|
289
|
+
// that is wiped: annotate's screenshot vanished that way (its file stayed,
|
|
290
|
+
// its image element went; seen in the pane 2026-09-24). `ready` turns true
|
|
291
|
+
// once the first load is done, and the effects that write the scene wait
|
|
292
|
+
// for it.
|
|
293
|
+
const [ready, setReady] = useState(false);
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
if (!api) return;
|
|
296
|
+
if (!api.getAppState().isLoading) return setReady(true);
|
|
297
|
+
const off = api.onChange((_els, appState) => {
|
|
298
|
+
if (!appState.isLoading) setReady(true);
|
|
299
|
+
});
|
|
300
|
+
return () => off?.();
|
|
301
|
+
}, [api]);
|
|
302
|
+
const [palette, setPalette] = useState(() => resolvePalette(rootRef.current));
|
|
303
|
+
const paletteRef = useRef(palette);
|
|
304
|
+
const [scheme, setScheme] = useState(() => schemeOf(rootRef.current));
|
|
305
|
+
const [ui, setUi] = useState({ tool: "selection", zoom: 1, selected: [], box: null, count: 0, grid: false });
|
|
306
|
+
const uiKey = useRef("");
|
|
307
|
+
const [stamp, setStamp] = useState(null);
|
|
308
|
+
const [save, setSave] = useState({ state: "idle" });
|
|
309
|
+
const [clearing, setClearing] = useState(false);
|
|
310
|
+
const current = useRef({ strokeColor: "foreground", backgroundColor: "none" });
|
|
311
|
+
|
|
312
|
+
// What was last sent out / taken in, so a controlled `value` that is
|
|
313
|
+
// only our own echo never reloads the scene.
|
|
314
|
+
const initial = useRef(null);
|
|
315
|
+
if (!initial.current) initial.current = toSketch(nonEmpty(value) ?? nonEmpty(defaultValue));
|
|
316
|
+
const lastSig = useRef(null);
|
|
317
|
+
if (lastSig.current === null) lastSig.current = signature(initial.current);
|
|
318
|
+
const lastVersion = useRef(-1);
|
|
319
|
+
const timer = useRef(0);
|
|
320
|
+
const pending = useRef(false);
|
|
321
|
+
const latest = useRef(null);
|
|
322
|
+
const saveSeq = useRef(0);
|
|
323
|
+
const lastValue = useRef(initial.current);
|
|
324
|
+
const cbs = useRef({});
|
|
325
|
+
cbs.current = { onChange, onSave, onText, onReady };
|
|
326
|
+
|
|
327
|
+
const activeTools = useMemo(() => {
|
|
328
|
+
const want = Array.isArray(tools) && tools.length ? new Set(tools) : null;
|
|
329
|
+
return SKETCH_TOOLS.filter((t) => !want || want.has(t.id));
|
|
330
|
+
}, [tools]);
|
|
331
|
+
const blockedKeys = useMemo(() => {
|
|
332
|
+
const on = new Set(activeTools.map((t) => t.tool));
|
|
333
|
+
const keys = new Set();
|
|
334
|
+
for (const [tool, ks] of Object.entries(TOOL_KEYS)) if (!on.has(tool)) ks.forEach((k) => keys.add(k));
|
|
335
|
+
return keys;
|
|
336
|
+
}, [activeTools]);
|
|
337
|
+
const stampList = useMemo(() => (Array.isArray(stamps) ? stamps.filter((s) => s && s.id && typeof s.create === "function") : []), [stamps]);
|
|
338
|
+
|
|
339
|
+
const initialData = useMemo(() => {
|
|
340
|
+
const els = prepareElements(initial.current.elements, palette);
|
|
341
|
+
return {
|
|
342
|
+
elements: els,
|
|
343
|
+
files: initial.current.files,
|
|
344
|
+
appState: {
|
|
345
|
+
viewBackgroundColor: "transparent",
|
|
346
|
+
currentItemFontFamily: SYSTEM_FONT,
|
|
347
|
+
currentItemStrokeColor: palette.foreground,
|
|
348
|
+
currentItemBackgroundColor: "transparent",
|
|
349
|
+
currentItemFillStyle: "solid",
|
|
350
|
+
currentItemRoundness: "round",
|
|
351
|
+
gridModeEnabled: !!initial.current.appState.gridModeEnabled,
|
|
352
|
+
},
|
|
353
|
+
scrollToContent: true,
|
|
354
|
+
};
|
|
355
|
+
// Excalidraw reads initialData once.
|
|
356
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
357
|
+
}, []);
|
|
358
|
+
|
|
359
|
+
// ---- output -------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
const runSave = useCallback(async (v) => {
|
|
362
|
+
const fn = cbs.current.onSave;
|
|
363
|
+
if (!fn) return;
|
|
364
|
+
const seq = ++saveSeq.current;
|
|
365
|
+
setSave({ state: "saving" });
|
|
366
|
+
let res;
|
|
367
|
+
try {
|
|
368
|
+
res = await fn(v);
|
|
369
|
+
} catch (e) {
|
|
370
|
+
res = { ok: false, error: e?.message };
|
|
371
|
+
}
|
|
372
|
+
if (seq !== saveSeq.current) return;
|
|
373
|
+
setSave(res?.ok ? { state: "saved" } : { state: "error", error: res?.error });
|
|
374
|
+
}, []);
|
|
375
|
+
|
|
376
|
+
const emit = useCallback(() => {
|
|
377
|
+
clearTimeout(timer.current);
|
|
378
|
+
pending.current = false;
|
|
379
|
+
// From what the last onChange handed us (so a flush while unmounting
|
|
380
|
+
// never reads a scene Excalidraw is tearing down), else the API.
|
|
381
|
+
const seen = latest.current;
|
|
382
|
+
const a = apiRef.current;
|
|
383
|
+
if (!seen && !a) return;
|
|
384
|
+
const v = seen ? valueOf(seen.elements, seen.files, seen.appState, paletteRef.current) : sceneValue(a, paletteRef.current);
|
|
385
|
+
lastValue.current = v;
|
|
386
|
+
lastSig.current = signature(v);
|
|
387
|
+
cbs.current.onText?.(sketchToText(v));
|
|
388
|
+
cbs.current.onChange?.(v);
|
|
389
|
+
runSave(v);
|
|
390
|
+
}, [runSave]);
|
|
391
|
+
|
|
392
|
+
const schedule = useCallback(() => {
|
|
393
|
+
clearTimeout(timer.current);
|
|
394
|
+
pending.current = true;
|
|
395
|
+
timer.current = setTimeout(emit, Math.max(0, changeDelay));
|
|
396
|
+
}, [emit, changeDelay]);
|
|
397
|
+
|
|
398
|
+
// Leaving with an edit still in its debounce: send it now, not never.
|
|
399
|
+
const flush = useRef(emit);
|
|
400
|
+
flush.current = emit;
|
|
401
|
+
useEffect(
|
|
402
|
+
() => () => {
|
|
403
|
+
clearTimeout(timer.current);
|
|
404
|
+
if (!pending.current) return;
|
|
405
|
+
try {
|
|
406
|
+
flush.current();
|
|
407
|
+
} catch {
|
|
408
|
+
// the canvas is already gone; nothing left to read
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
[],
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
// ---- the scene ----------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
const quietUpdate = useCallback((elements, appState) => {
|
|
417
|
+
const a = apiRef.current;
|
|
418
|
+
if (!a) return;
|
|
419
|
+
a.updateScene({ elements, appState, captureUpdate: CaptureUpdateAction.NEVER });
|
|
420
|
+
lastVersion.current = getSceneVersion(a.getSceneElementsIncludingDeleted());
|
|
421
|
+
}, []);
|
|
422
|
+
|
|
423
|
+
/** Press one of Excalidraw's own shortcuts on the canvas. */
|
|
424
|
+
const press = useCallback((key, code, mods = {}) => {
|
|
425
|
+
const target = rootRef.current?.querySelector(".excalidraw");
|
|
426
|
+
if (!target) return;
|
|
427
|
+
const mac = isDarwin();
|
|
428
|
+
target.dispatchEvent(
|
|
429
|
+
new KeyboardEvent("keydown", {
|
|
430
|
+
key,
|
|
431
|
+
code,
|
|
432
|
+
bubbles: true,
|
|
433
|
+
cancelable: true,
|
|
434
|
+
metaKey: !!mods.cmd && mac,
|
|
435
|
+
ctrlKey: !!mods.cmd && !mac,
|
|
436
|
+
shiftKey: !!mods.shift,
|
|
437
|
+
altKey: !!mods.alt,
|
|
438
|
+
}),
|
|
439
|
+
);
|
|
440
|
+
}, [rootRef]);
|
|
441
|
+
|
|
442
|
+
const layer = useCallback(
|
|
443
|
+
(front) => {
|
|
444
|
+
const mac = isDarwin();
|
|
445
|
+
press(front ? "]" : "[", front ? "BracketRight" : "BracketLeft", mac ? { cmd: true, alt: true } : { cmd: true, shift: true });
|
|
446
|
+
},
|
|
447
|
+
[press],
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
// The locked background image, laid from the prop at the back.
|
|
451
|
+
const bgKey = background?.src ? `${background.src.length}:${hash(background.src)}:${background.width ?? ""}x${background.height ?? ""}` : "";
|
|
452
|
+
useEffect(() => {
|
|
453
|
+
const a = api;
|
|
454
|
+
if (!a || !ready) return;
|
|
455
|
+
const others = () => a.getSceneElementsIncludingDeleted().filter((el) => !isBackground(el));
|
|
456
|
+
if (!background?.src) {
|
|
457
|
+
if (a.getSceneElementsIncludingDeleted().some(isBackground)) quietUpdate(others());
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
let cancelled = false;
|
|
461
|
+
const place = (width, height) => {
|
|
462
|
+
if (cancelled || !width || !height) return;
|
|
463
|
+
const fileId = `bg-${hash(background.src)}`;
|
|
464
|
+
a.addFiles([{ id: fileId, dataURL: background.src, mimeType: mimeOf(background.src), created: Date.now() }]);
|
|
465
|
+
const [bg] = convertToExcalidrawElements(
|
|
466
|
+
[{ type: "image", id: BG_ID, fileId, x: 0, y: 0, width, height, locked: true, status: "saved", customData: { role: BACKGROUND_ROLE } }],
|
|
467
|
+
{ regenerateIds: false },
|
|
468
|
+
);
|
|
469
|
+
const rest = others();
|
|
470
|
+
quietUpdate([bg, ...rest]);
|
|
471
|
+
if (!rest.some((el) => !el.isDeleted)) a.scrollToContent(undefined, { fitToContent: true });
|
|
472
|
+
};
|
|
473
|
+
if (background.width && background.height) place(background.width, background.height);
|
|
474
|
+
else {
|
|
475
|
+
const img = new window.Image();
|
|
476
|
+
img.onload = () => place(img.naturalWidth, img.naturalHeight);
|
|
477
|
+
img.src = background.src;
|
|
478
|
+
}
|
|
479
|
+
return () => {
|
|
480
|
+
cancelled = true;
|
|
481
|
+
};
|
|
482
|
+
// bgKey stands for background's fields.
|
|
483
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
484
|
+
}, [api, ready, bgKey, quietUpdate]);
|
|
485
|
+
|
|
486
|
+
// Controlled: a `value` that is not what we last sent replaces the scene.
|
|
487
|
+
useEffect(() => {
|
|
488
|
+
const a = api;
|
|
489
|
+
if (!a || !ready || value === undefined) return;
|
|
490
|
+
const sig = signature(value);
|
|
491
|
+
if (sig === lastSig.current) return;
|
|
492
|
+
lastSig.current = sig;
|
|
493
|
+
const s = toSketch(value);
|
|
494
|
+
lastValue.current = s;
|
|
495
|
+
const bg = a.getSceneElementsIncludingDeleted().filter(isBackground);
|
|
496
|
+
const files = Object.values(s.files);
|
|
497
|
+
if (files.length) a.addFiles(files);
|
|
498
|
+
quietUpdate([...bg, ...prepareElements(s.elements, paletteRef.current)], { gridModeEnabled: !!s.appState.gridModeEnabled });
|
|
499
|
+
a.history.clear();
|
|
500
|
+
cbs.current.onText?.(sketchToText(s));
|
|
501
|
+
}, [api, ready, value, quietUpdate]);
|
|
502
|
+
|
|
503
|
+
// Theme and scheme: re-read the tokens when anything that sets them
|
|
504
|
+
// changes, and repaint what was drawn in them.
|
|
505
|
+
const refresh = useCallback(() => {
|
|
506
|
+
const root = rootRef.current;
|
|
507
|
+
if (!root) return;
|
|
508
|
+
setScheme(schemeOf(root));
|
|
509
|
+
const next = resolvePalette(root);
|
|
510
|
+
const prev = paletteRef.current;
|
|
511
|
+
if (paletteKey(next) === paletteKey(prev)) return;
|
|
512
|
+
paletteRef.current = next;
|
|
513
|
+
setPalette(next);
|
|
514
|
+
const a = apiRef.current;
|
|
515
|
+
if (!a) return;
|
|
516
|
+
const all = a.getSceneElementsIncludingDeleted();
|
|
517
|
+
const patches = new Map(repaint(all, next, prev).map((p) => [p.id, p.patch]));
|
|
518
|
+
quietUpdate(
|
|
519
|
+
patches.size ? all.map((el) => (patches.has(el.id) ? newElementWith(el, patches.get(el.id)) : el)) : undefined,
|
|
520
|
+
{
|
|
521
|
+
currentItemStrokeColor: next[current.current.strokeColor] ?? next.foreground,
|
|
522
|
+
currentItemBackgroundColor: next[current.current.backgroundColor] ?? "transparent",
|
|
523
|
+
},
|
|
524
|
+
);
|
|
525
|
+
}, [rootRef, quietUpdate]);
|
|
526
|
+
|
|
527
|
+
useEffect(() => {
|
|
528
|
+
let t = 0;
|
|
529
|
+
const soon = () => {
|
|
530
|
+
clearTimeout(t);
|
|
531
|
+
t = setTimeout(refresh, 30);
|
|
532
|
+
};
|
|
533
|
+
const obs = new MutationObserver((records) => {
|
|
534
|
+
for (const r of records) for (const n of r.addedNodes ?? []) if (n.tagName === "LINK") n.addEventListener("load", soon, { once: true });
|
|
535
|
+
soon();
|
|
536
|
+
});
|
|
537
|
+
obs.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "class", "style"] });
|
|
538
|
+
obs.observe(document.head, { childList: true });
|
|
539
|
+
const scoped = rootRef.current?.closest("[data-theme]");
|
|
540
|
+
if (scoped && scoped !== document.documentElement) obs.observe(scoped, { attributes: true, attributeFilter: ["data-theme", "class", "style"] });
|
|
541
|
+
const mq = matchMedia("(prefers-color-scheme: dark)");
|
|
542
|
+
mq.addEventListener("change", soon);
|
|
543
|
+
soon();
|
|
544
|
+
return () => {
|
|
545
|
+
clearTimeout(t);
|
|
546
|
+
obs.disconnect();
|
|
547
|
+
mq.removeEventListener("change", soon);
|
|
548
|
+
};
|
|
549
|
+
}, [refresh, rootRef]);
|
|
550
|
+
|
|
551
|
+
// ---- Excalidraw's events -----------------------------------------------
|
|
552
|
+
|
|
553
|
+
const fontFix = useRef(0);
|
|
554
|
+
const handleChange = useCallback(
|
|
555
|
+
(elements, appState, files) => {
|
|
556
|
+
latest.current = { elements, appState, files };
|
|
557
|
+
// What our overlays show.
|
|
558
|
+
const selected = elements.filter((el) => appState.selectedElementIds[el.id] && !el.isDeleted);
|
|
559
|
+
const busy =
|
|
560
|
+
appState.newElement || appState.selectionElement || appState.selectedElementsAreBeingDragged || appState.resizingElement || appState.isResizing || appState.isRotating || appState.editingTextElement || appState.multiElement || appState.cursorButton === "down";
|
|
561
|
+
let box = null;
|
|
562
|
+
if (selected.length && !busy) {
|
|
563
|
+
const [x1, y1, x2, y2] = getCommonBounds(selected);
|
|
564
|
+
const a = sceneCoordsToViewportCoords({ sceneX: x1, sceneY: y1 }, appState);
|
|
565
|
+
const b = sceneCoordsToViewportCoords({ sceneX: x2, sceneY: y2 }, appState);
|
|
566
|
+
box = { x: a.x - appState.offsetLeft, y: a.y - appState.offsetTop, w: b.x - a.x, h: b.y - a.y, vw: appState.width, vh: appState.height };
|
|
567
|
+
}
|
|
568
|
+
const count = elements.reduce((n, el) => n + (!el.isDeleted && !isBackground(el) ? 1 : 0), 0);
|
|
569
|
+
const next = { tool: appState.activeTool.type, zoom: appState.zoom.value, selected: selected.map((el) => el.id), box, count, grid: !!appState.gridModeEnabled };
|
|
570
|
+
const key = JSON.stringify(next);
|
|
571
|
+
if (key !== uiKey.current) {
|
|
572
|
+
uiKey.current = key;
|
|
573
|
+
setUi(next);
|
|
574
|
+
}
|
|
575
|
+
// Output: only when the drawing changed, not on pan, zoom or select.
|
|
576
|
+
const v = getSceneVersion(elements);
|
|
577
|
+
if (lastVersion.current === -1) lastVersion.current = v;
|
|
578
|
+
else if (v !== lastVersion.current) {
|
|
579
|
+
lastVersion.current = v;
|
|
580
|
+
schedule();
|
|
581
|
+
}
|
|
582
|
+
// Text that arrived in another font (a paste, a dropped file): into
|
|
583
|
+
// the system font, once nothing is being edited.
|
|
584
|
+
if (!busy && elements.some((el) => el.type === "text" && !el.isDeleted && el.fontFamily !== SYSTEM_FONT)) {
|
|
585
|
+
clearTimeout(fontFix.current);
|
|
586
|
+
fontFix.current = setTimeout(() => {
|
|
587
|
+
const a = apiRef.current;
|
|
588
|
+
if (!a) return;
|
|
589
|
+
// The whole scene through restoreElements, as Excalidraw does on
|
|
590
|
+
// load: it re-measures text in its container (a subset would lose
|
|
591
|
+
// the bindings it cannot see).
|
|
592
|
+
const all = a.getSceneElementsIncludingDeleted();
|
|
593
|
+
const need = new Set(all.filter((el) => el.type === "text" && el.fontFamily !== SYSTEM_FONT).map((el) => el.id));
|
|
594
|
+
if (!need.size) return;
|
|
595
|
+
const fixed = restoreElements(
|
|
596
|
+
all.map((el) => (need.has(el.id) ? { ...el, fontFamily: SYSTEM_FONT } : el)),
|
|
597
|
+
null,
|
|
598
|
+
{ refreshDimensions: true, repairBindings: true },
|
|
599
|
+
).map((el) => (need.has(el.id) ? { ...el, version: el.version + 1, versionNonce: Math.floor(Math.random() * 2 ** 31) } : el));
|
|
600
|
+
a.updateScene({ elements: fixed, captureUpdate: CaptureUpdateAction.NEVER });
|
|
601
|
+
}, 0);
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
[schedule],
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
const handleScroll = useCallback(
|
|
608
|
+
(scrollX, scrollY, zoom) => {
|
|
609
|
+
const root = rootRef.current;
|
|
610
|
+
if (!root) return;
|
|
611
|
+
let gap = 24 * zoom.value;
|
|
612
|
+
while (gap < 10) gap *= 4;
|
|
613
|
+
root.style.setProperty("--sketch-dot-gap", `${gap}px`);
|
|
614
|
+
root.style.setProperty("--sketch-dot-x", `${(scrollX * zoom.value) % gap}px`);
|
|
615
|
+
root.style.setProperty("--sketch-dot-y", `${(scrollY * zoom.value) % gap}px`);
|
|
616
|
+
},
|
|
617
|
+
[rootRef],
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
// Pasted Excalidraw content keeps its own fonts: into the system font
|
|
621
|
+
// before it lands (no font is ever fetched from a CDN).
|
|
622
|
+
const handlePaste = useCallback((data) => {
|
|
623
|
+
for (const el of data?.elements ?? []) if (el?.type === "text") el.fontFamily = SYSTEM_FONT;
|
|
624
|
+
return true;
|
|
625
|
+
}, []);
|
|
626
|
+
|
|
627
|
+
// ---- the handle ---------------------------------------------------------
|
|
628
|
+
|
|
629
|
+
const viewCentre = useCallback(() => {
|
|
630
|
+
const st = apiRef.current.getAppState();
|
|
631
|
+
return viewportCoordsToSceneCoords({ clientX: st.offsetLeft + st.width / 2, clientY: st.offsetTop + st.height / 2 }, st);
|
|
632
|
+
}, []);
|
|
633
|
+
|
|
634
|
+
const insertElements = useCallback(
|
|
635
|
+
(skeletons, opts = {}) => {
|
|
636
|
+
const a = apiRef.current;
|
|
637
|
+
if (!a || !Array.isArray(skeletons) || !skeletons.length) return [];
|
|
638
|
+
const pal = paletteRef.current;
|
|
639
|
+
let sks = skeletons.map((sk) => prepareSkeleton(sk, pal));
|
|
640
|
+
if (opts.at !== false) {
|
|
641
|
+
const b = boundsOfSkeletons(sks);
|
|
642
|
+
const at = opts.at && typeof opts.at === "object" ? opts.at : null;
|
|
643
|
+
const c = at ? { x: at.x, y: at.y } : (() => { const v = viewCentre(); return { x: v.x, y: v.y }; })();
|
|
644
|
+
const dx = (opts.anchor === "top-left" && at ? c.x : c.x - b.width / 2) - b.x;
|
|
645
|
+
const dy = (opts.anchor === "top-left" && at ? c.y : c.y - b.height / 2) - b.y;
|
|
646
|
+
sks = sks.map((sk) => ({ ...sk, x: (sk.x ?? 0) + dx, y: (sk.y ?? 0) + dy }));
|
|
647
|
+
}
|
|
648
|
+
let els = convertToExcalidrawElements(sks, { regenerateIds: true });
|
|
649
|
+
const gid = opts.group || opts.stamp ? `g${Math.random().toString(36).slice(2, 10)}` : null;
|
|
650
|
+
els = els.map((el) => ({
|
|
651
|
+
...el,
|
|
652
|
+
groupIds: gid ? [...(el.groupIds ?? []), gid] : el.groupIds,
|
|
653
|
+
customData: { ...(el.customData ?? {}), ...(opts.customData ?? {}), ...(opts.stamp ? { stamp: opts.stamp, stampId: gid } : {}) },
|
|
654
|
+
}));
|
|
655
|
+
if (opts.files?.length) a.addFiles(opts.files);
|
|
656
|
+
const select = opts.select !== false;
|
|
657
|
+
a.updateScene({
|
|
658
|
+
elements: [...a.getSceneElementsIncludingDeleted(), ...els],
|
|
659
|
+
appState: select ? { selectedElementIds: Object.fromEntries(els.filter((e) => !e.containerId).map((e) => [e.id, true])), selectedGroupIds: gid ? { [gid]: true } : {} } : undefined,
|
|
660
|
+
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
|
661
|
+
});
|
|
662
|
+
return els.map((e) => e.id);
|
|
663
|
+
},
|
|
664
|
+
[viewCentre],
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
const exportImage = useCallback(async (opts = {}) => {
|
|
668
|
+
const a = apiRef.current;
|
|
669
|
+
if (!a) throw new Error("The canvas is not ready yet.");
|
|
670
|
+
const st = a.getAppState();
|
|
671
|
+
const all = a.getSceneElements();
|
|
672
|
+
const els = opts.onlySelected ? all.filter((el) => st.selectedElementIds[el.id]) : opts.withBackground === false ? all.filter((el) => !isBackground(el)) : all;
|
|
673
|
+
const scale = opts.scale ?? 2;
|
|
674
|
+
return exportToBlob({
|
|
675
|
+
elements: els,
|
|
676
|
+
files: a.getFiles(),
|
|
677
|
+
mimeType: "image/png",
|
|
678
|
+
exportPadding: opts.padding ?? 16,
|
|
679
|
+
appState: { ...st, exportBackground: opts.transparent !== true, viewBackgroundColor: paletteRef.current.background ?? "transparent", exportWithDarkMode: false, exportScale: scale },
|
|
680
|
+
getDimensions: (w, h) => ({ width: w * scale, height: h * scale, scale }),
|
|
681
|
+
});
|
|
682
|
+
}, []);
|
|
683
|
+
|
|
684
|
+
const handle = useMemo(() => {
|
|
685
|
+
if (!api) return null;
|
|
686
|
+
return {
|
|
687
|
+
api,
|
|
688
|
+
getScene: (opts = {}) => {
|
|
689
|
+
const v = sceneValue(api, paletteRef.current);
|
|
690
|
+
if (!opts.withBackground) return v;
|
|
691
|
+
const bg = api.getSceneElements().filter(isBackground);
|
|
692
|
+
return { ...v, elements: [...bg, ...v.elements], files: { ...v.files, ...filesFor(bg, api.getFiles()) } };
|
|
693
|
+
},
|
|
694
|
+
setScene: (v) => {
|
|
695
|
+
const s = toSketch(v);
|
|
696
|
+
const bg = api.getSceneElementsIncludingDeleted().filter(isBackground);
|
|
697
|
+
if (Object.keys(s.files).length) api.addFiles(Object.values(s.files));
|
|
698
|
+
api.updateScene({ elements: [...bg, ...prepareElements(s.elements, paletteRef.current)], captureUpdate: CaptureUpdateAction.IMMEDIATELY });
|
|
699
|
+
},
|
|
700
|
+
insertElements,
|
|
701
|
+
exportImage,
|
|
702
|
+
zoomToFit: (opts = {}) => api.scrollToContent(undefined, { fitToContent: true, animate: opts.animate !== false }),
|
|
703
|
+
setTool: (id) => {
|
|
704
|
+
const s = stampList.find((x) => x.id === id);
|
|
705
|
+
if (s) return setStamp(s);
|
|
706
|
+
const t = SKETCH_TOOLS.find((x) => x.id === id || x.tool === id);
|
|
707
|
+
setStamp(null);
|
|
708
|
+
if (t) api.setActiveTool({ type: t.tool });
|
|
709
|
+
},
|
|
710
|
+
clear: () => {
|
|
711
|
+
const bg = api.getSceneElementsIncludingDeleted().filter(isBackground);
|
|
712
|
+
api.updateScene({ elements: bg, captureUpdate: CaptureUpdateAction.IMMEDIATELY });
|
|
713
|
+
},
|
|
714
|
+
undo: () => press("z", "KeyZ", { cmd: true }),
|
|
715
|
+
redo: () => press("z", "KeyZ", { cmd: true, shift: true }),
|
|
716
|
+
focus: () => rootRef.current?.querySelector(".excalidraw")?.focus(),
|
|
717
|
+
palette: () => paletteRef.current,
|
|
718
|
+
repaint: refresh,
|
|
719
|
+
save: emit,
|
|
720
|
+
};
|
|
721
|
+
}, [api, insertElements, exportImage, stampList, press, rootRef, refresh, emit]);
|
|
722
|
+
|
|
723
|
+
useEffect(() => {
|
|
724
|
+
if (!canvasRef || !handle) return;
|
|
725
|
+
canvasRef.current = handle;
|
|
726
|
+
return () => {
|
|
727
|
+
if (canvasRef.current === handle) canvasRef.current = null;
|
|
728
|
+
};
|
|
729
|
+
}, [canvasRef, handle]);
|
|
730
|
+
useEffect(() => {
|
|
731
|
+
if (handle) cbs.current.onReady?.(handle);
|
|
732
|
+
}, [handle]);
|
|
733
|
+
|
|
734
|
+
// Open fitted: initialData's scrollToContent only centres the scene, so
|
|
735
|
+
// a board larger than the frame opened on its top-left corner at 100%.
|
|
736
|
+
// Fit once the canvas has its size; never zoom in past 100%.
|
|
737
|
+
useEffect(() => {
|
|
738
|
+
if (!api || !ready) return;
|
|
739
|
+
const t = setTimeout(() => {
|
|
740
|
+
const live = api.getSceneElements().filter((el) => !el.isDeleted);
|
|
741
|
+
if (!live.length) return;
|
|
742
|
+
api.scrollToContent(live, { fitToViewport: true, viewportZoomFactor: 0.9, maxZoom: 1, animate: false });
|
|
743
|
+
}, 150);
|
|
744
|
+
return () => clearTimeout(t);
|
|
745
|
+
}, [api, ready]);
|
|
746
|
+
|
|
747
|
+
// ---- our controls -------------------------------------------------------
|
|
748
|
+
|
|
749
|
+
const pickTool = (t) => {
|
|
750
|
+
setStamp(null);
|
|
751
|
+
apiRef.current?.setActiveTool({ type: t.tool });
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
const placeStamp = (s, at) => {
|
|
755
|
+
insertElements(s.create(), { stamp: s.id, at });
|
|
756
|
+
setStamp(null);
|
|
757
|
+
apiRef.current?.setActiveTool({ type: "selection" });
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
const selectedEls = () => {
|
|
761
|
+
const a = apiRef.current;
|
|
762
|
+
if (!a) return [];
|
|
763
|
+
const ids = a.getAppState().selectedElementIds;
|
|
764
|
+
return a.getSceneElements().filter((el) => ids[el.id]);
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
const setColour = (prop, token) => {
|
|
768
|
+
const a = apiRef.current;
|
|
769
|
+
const pal = paletteRef.current;
|
|
770
|
+
if (!a || !(token in pal)) return;
|
|
771
|
+
const sel = selectedEls();
|
|
772
|
+
const ids = new Set(sel.map((el) => el.id));
|
|
773
|
+
if (prop === "strokeColor") for (const el of sel) for (const b of el.boundElements ?? []) if (b.type === "text") ids.add(b.id);
|
|
774
|
+
const all = a.getSceneElementsIncludingDeleted();
|
|
775
|
+
a.updateScene({
|
|
776
|
+
elements: all.map((el) =>
|
|
777
|
+
ids.has(el.id) ? newElementWith(el, { [prop]: pal[token], customData: { ...(el.customData ?? {}), tokens: { ...(el.customData?.tokens ?? {}), [prop]: token } } }) : el,
|
|
778
|
+
),
|
|
779
|
+
appState: prop === "strokeColor" ? { currentItemStrokeColor: pal[token] } : { currentItemBackgroundColor: pal[token] },
|
|
780
|
+
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
|
781
|
+
});
|
|
782
|
+
current.current = { ...current.current, [prop]: token };
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
const setProp = (patch, only) => {
|
|
786
|
+
const a = apiRef.current;
|
|
787
|
+
if (!a) return;
|
|
788
|
+
const ids = new Set(selectedEls().filter(only ?? (() => true)).map((el) => el.id));
|
|
789
|
+
a.updateScene({ elements: a.getSceneElementsIncludingDeleted().map((el) => (ids.has(el.id) ? newElementWith(el, patch) : el)), captureUpdate: CaptureUpdateAction.IMMEDIATELY });
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
const download = async () => {
|
|
793
|
+
try {
|
|
794
|
+
const blob = await exportImage();
|
|
795
|
+
const url = URL.createObjectURL(blob);
|
|
796
|
+
const link = document.createElement("a");
|
|
797
|
+
link.href = url;
|
|
798
|
+
link.download = "sketch.png";
|
|
799
|
+
link.click();
|
|
800
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
801
|
+
} catch {
|
|
802
|
+
setSave((s) => ({ ...s, exportError: true }));
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
// Keys on their way into the canvas: tools left out, the stamp being
|
|
807
|
+
// placed, Cmd/Ctrl+S.
|
|
808
|
+
const keyDown = (e) => {
|
|
809
|
+
const t = e.target;
|
|
810
|
+
if (t?.tagName === "TEXTAREA" || t?.tagName === "INPUT" || t?.isContentEditable) return;
|
|
811
|
+
const k = e.key?.toLowerCase();
|
|
812
|
+
if ((e.metaKey || e.ctrlKey) && k === "s") {
|
|
813
|
+
e.preventDefault();
|
|
814
|
+
e.stopPropagation();
|
|
815
|
+
if (!readOnly) emit();
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if ((e.metaKey || e.ctrlKey) && k === "f") {
|
|
819
|
+
e.stopPropagation(); // the browser's find, not Excalidraw's (hidden) search
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (stamp) {
|
|
823
|
+
if (e.key === "Escape") {
|
|
824
|
+
e.preventDefault();
|
|
825
|
+
e.stopPropagation();
|
|
826
|
+
setStamp(null);
|
|
827
|
+
} else if (e.key === "Enter") {
|
|
828
|
+
e.preventDefault();
|
|
829
|
+
e.stopPropagation();
|
|
830
|
+
placeStamp(stamp);
|
|
831
|
+
}
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
if (!e.metaKey && !e.ctrlKey && !e.altKey && blockedKeys.has(k)) {
|
|
835
|
+
e.preventDefault();
|
|
836
|
+
e.stopPropagation();
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
const selected = ui.selected.length ? selectedEls() : [];
|
|
841
|
+
const kinds = new Set(selected.map((el) => el.type));
|
|
842
|
+
const hasShape = [...kinds].some((k) => SHAPES.has(k));
|
|
843
|
+
const hasLine = [...kinds].some((k) => SHAPES.has(k) || LINES.has(k));
|
|
844
|
+
const hasText = kinds.has("text") || selected.some((el) => (el.boundElements ?? []).some((b) => b.type === "text"));
|
|
845
|
+
const hasStroke = [...kinds].some((k) => k !== "image" && k !== "frame" && k !== "magicframe");
|
|
846
|
+
const first = selected[0];
|
|
847
|
+
const tokens = first ? elementTokens(first, palette) : {};
|
|
848
|
+
const roughness = first?.roughness ?? 1;
|
|
849
|
+
const width = first?.strokeWidth ?? 2;
|
|
850
|
+
const empty_ = ui.count === 0;
|
|
851
|
+
const showEmpty = empty_ && !readOnly && empty !== false && !stamp && (ui.tool === "selection" || ui.tool === "hand");
|
|
852
|
+
|
|
853
|
+
return (
|
|
854
|
+
// raw: a plain box that hears keys on their way into Excalidraw (capture),
|
|
855
|
+
// to swallow the shortcuts of tools left out; no control of its own.
|
|
856
|
+
<div
|
|
857
|
+
className="absolute inset-0"
|
|
858
|
+
onKeyDownCapture={keyDown}
|
|
859
|
+
style={{ "--sketch-selection": palette.primary ?? "currentColor" }}
|
|
860
|
+
>
|
|
861
|
+
{grid ? (
|
|
862
|
+
<div
|
|
863
|
+
aria-hidden="true"
|
|
864
|
+
className="pointer-events-none absolute inset-0 [background-image:radial-gradient(var(--color-border)_1.2px,transparent_1.3px)] [background-size:var(--sketch-dot-gap,24px)_var(--sketch-dot-gap,24px)] [background-position:var(--sketch-dot-x,0)_var(--sketch-dot-y,0)]"
|
|
865
|
+
/>
|
|
866
|
+
) : null}
|
|
867
|
+
<div className="absolute inset-0">
|
|
868
|
+
<Excalidraw
|
|
869
|
+
excalidrawAPI={(a) => {
|
|
870
|
+
apiRef.current = a;
|
|
871
|
+
setApi(a);
|
|
872
|
+
}}
|
|
873
|
+
initialData={initialData}
|
|
874
|
+
onChange={handleChange}
|
|
875
|
+
onScrollChange={handleScroll}
|
|
876
|
+
onPaste={handlePaste}
|
|
877
|
+
theme={scheme}
|
|
878
|
+
viewModeEnabled={readOnly}
|
|
879
|
+
aiEnabled={false}
|
|
880
|
+
detectScroll
|
|
881
|
+
handleKeyboardGlobally={false}
|
|
882
|
+
UIOptions={{
|
|
883
|
+
canvasActions: {
|
|
884
|
+
changeViewBackgroundColor: false,
|
|
885
|
+
clearCanvas: false,
|
|
886
|
+
export: false,
|
|
887
|
+
loadScene: false,
|
|
888
|
+
saveToActiveFile: false,
|
|
889
|
+
toggleTheme: false,
|
|
890
|
+
saveAsImage: false,
|
|
891
|
+
},
|
|
892
|
+
tools: { image: activeTools.some((t) => t.id === "image") },
|
|
893
|
+
}}
|
|
894
|
+
renderTopRightUI={() => null}
|
|
895
|
+
/>
|
|
896
|
+
</div>
|
|
897
|
+
|
|
898
|
+
{showEmpty ? (
|
|
899
|
+
<div className="pointer-events-none absolute inset-0 z-10 grid place-items-center p-6">
|
|
900
|
+
{empty ?? (
|
|
901
|
+
<div className="flex max-w-xs flex-col items-center gap-2 text-center">
|
|
902
|
+
<span className="grid size-12 place-items-center rounded-full bg-muted text-muted-foreground">
|
|
903
|
+
<Icon name="pencil" size={22} />
|
|
904
|
+
</span>
|
|
905
|
+
<p className="text-base font-medium">Start sketching</p>
|
|
906
|
+
<p className="text-sm text-muted-foreground">
|
|
907
|
+
Pick a tool, or press <Kbd>P</Kbd> to draw, <Kbd>R</Kbd> for a box, <Kbd>T</Kbd> for text.
|
|
908
|
+
</p>
|
|
909
|
+
</div>
|
|
910
|
+
)}
|
|
911
|
+
</div>
|
|
912
|
+
) : null}
|
|
913
|
+
|
|
914
|
+
{stamp ? (
|
|
915
|
+
<>
|
|
916
|
+
{/* raw: a click target laid over the canvas while a stamp waits to
|
|
917
|
+
land — the pointer picks the spot; Enter drops it in the middle. */}
|
|
918
|
+
<div
|
|
919
|
+
aria-hidden="true"
|
|
920
|
+
className="absolute inset-0 z-10 cursor-crosshair"
|
|
921
|
+
onPointerDown={(e) => {
|
|
922
|
+
e.preventDefault();
|
|
923
|
+
const st = apiRef.current?.getAppState();
|
|
924
|
+
if (!st) return;
|
|
925
|
+
placeStamp(stamp, viewportCoordsToSceneCoords({ clientX: e.clientX, clientY: e.clientY }, st));
|
|
926
|
+
}}
|
|
927
|
+
/>
|
|
928
|
+
<div className="pointer-events-none absolute inset-x-0 top-3 z-20 flex justify-center px-3">
|
|
929
|
+
<p role="status" className="rounded-full border border-border bg-popover px-3 py-1 text-xs text-popover-foreground shadow-sm">
|
|
930
|
+
Click to place the {stamp.label.toLowerCase()} · Enter for the middle · Esc to cancel
|
|
931
|
+
</p>
|
|
932
|
+
</div>
|
|
933
|
+
</>
|
|
934
|
+
) : null}
|
|
935
|
+
|
|
936
|
+
{!readOnly ? (
|
|
937
|
+
<Rail tools={activeTools} stamps={stampList} tool={ui.tool} stamp={stamp} onTool={pickTool} onStamp={(s) => setStamp((cur) => (cur?.id === s.id ? null : s))} />
|
|
938
|
+
) : null}
|
|
939
|
+
|
|
940
|
+
{!readOnly && ui.box && selected.length ? (
|
|
941
|
+
<ContextBar
|
|
942
|
+
box={ui.box}
|
|
943
|
+
tokens={tokens}
|
|
944
|
+
hasShape={hasShape}
|
|
945
|
+
hasLine={hasLine}
|
|
946
|
+
hasText={hasText}
|
|
947
|
+
hasStroke={hasStroke}
|
|
948
|
+
roughness={roughness}
|
|
949
|
+
width={width}
|
|
950
|
+
onColour={setColour}
|
|
951
|
+
onWidth={(w) => setProp({ strokeWidth: w }, (el) => el.type !== "text")}
|
|
952
|
+
onRough={(r) => setProp({ roughness: r }, (el) => el.type !== "text")}
|
|
953
|
+
onFont={(bigger) => press(bigger ? ">" : "<", bigger ? "Period" : "Comma", { cmd: true, shift: true })}
|
|
954
|
+
onLayer={layer}
|
|
955
|
+
onDuplicate={() => press("d", "KeyD", { cmd: true })}
|
|
956
|
+
onDelete={() => press("Delete", "Delete")}
|
|
957
|
+
/>
|
|
958
|
+
) : null}
|
|
959
|
+
|
|
960
|
+
{/* top right: the host's actions, the save state, the menu */}
|
|
961
|
+
<div className="absolute right-3 top-3 z-20 flex items-center gap-2">
|
|
962
|
+
{onSave && !readOnly ? <SaveState save={save} onRetry={() => runSave(lastValue.current)} /> : null}
|
|
963
|
+
{actions}
|
|
964
|
+
<div className="rounded-lg border border-border bg-popover shadow-sm">
|
|
965
|
+
<Menu
|
|
966
|
+
label="Canvas menu"
|
|
967
|
+
size="sm"
|
|
968
|
+
items={[
|
|
969
|
+
{ label: "Download PNG", icon: "download", onSelect: download },
|
|
970
|
+
!readOnly && { type: "checkbox", label: "Snap to grid", checked: ui.grid, onCheckedChange: (on) => apiRef.current?.updateScene({ appState: { gridModeEnabled: on } }) },
|
|
971
|
+
{ label: "Fit to screen", icon: "maximize", shortcut: "⇧1", onSelect: () => handle?.zoomToFit() },
|
|
972
|
+
{ label: "Keyboard shortcuts", icon: "keyboard", shortcut: "?", onSelect: () => apiRef.current?.updateScene({ appState: { openDialog: { name: "help" } } }) },
|
|
973
|
+
!readOnly && { type: "separator" },
|
|
974
|
+
!readOnly && { label: "Clear canvas", icon: "trash", tone: "danger", disabled: empty_, onSelect: () => setClearing(true) },
|
|
975
|
+
]}
|
|
976
|
+
/>
|
|
977
|
+
</div>
|
|
978
|
+
</div>
|
|
979
|
+
|
|
980
|
+
{/* bottom left: undo, redo */}
|
|
981
|
+
{!readOnly ? (
|
|
982
|
+
<div className="absolute bottom-3 left-3 z-20 flex items-center gap-0.5 rounded-lg border border-border bg-popover p-0.5 shadow-sm max-md:bottom-[4.25rem]">
|
|
983
|
+
<Tooltip label={`Undo — ${isDarwin() ? "⌘Z" : "Ctrl+Z"}`}>
|
|
984
|
+
<IconButton icon="undo" label="Undo" size="sm" onClick={() => handle?.undo()} />
|
|
985
|
+
</Tooltip>
|
|
986
|
+
<Tooltip label={`Redo — ${isDarwin() ? "⇧⌘Z" : "Ctrl+Shift+Z"}`}>
|
|
987
|
+
<IconButton icon="redo" label="Redo" size="sm" onClick={() => handle?.redo()} />
|
|
988
|
+
</Tooltip>
|
|
989
|
+
</div>
|
|
990
|
+
) : null}
|
|
991
|
+
|
|
992
|
+
{/* bottom right: zoom */}
|
|
993
|
+
<div className="absolute bottom-3 right-3 z-20 flex items-center gap-0.5 rounded-lg border border-border bg-popover p-0.5 shadow-sm max-md:bottom-[4.25rem]">
|
|
994
|
+
<Tooltip label="Zoom out">
|
|
995
|
+
<IconButton icon="minus" label="Zoom out" size="sm" onClick={() => press("-", "Minus", { cmd: true })} />
|
|
996
|
+
</Tooltip>
|
|
997
|
+
<Tooltip label="Reset to 100%">
|
|
998
|
+
<Button variant="quiet" size="sm" className="min-w-14 tabular-nums" onClick={() => press("0", "Digit0", { cmd: true })} aria-label={`Zoom ${Math.round(ui.zoom * 100)}%, reset to 100%`}>
|
|
999
|
+
{Math.round(ui.zoom * 100)}%
|
|
1000
|
+
</Button>
|
|
1001
|
+
</Tooltip>
|
|
1002
|
+
<Tooltip label="Zoom in">
|
|
1003
|
+
<IconButton icon="plus" label="Zoom in" size="sm" onClick={() => press("=", "Equal", { cmd: true })} />
|
|
1004
|
+
</Tooltip>
|
|
1005
|
+
<Tooltip label="Fit to screen — ⇧1">
|
|
1006
|
+
<IconButton icon="maximize" label="Fit to screen" size="sm" onClick={() => handle?.zoomToFit()} />
|
|
1007
|
+
</Tooltip>
|
|
1008
|
+
</div>
|
|
1009
|
+
|
|
1010
|
+
<Confirm
|
|
1011
|
+
open={clearing}
|
|
1012
|
+
onClose={() => setClearing(false)}
|
|
1013
|
+
onConfirm={() => {
|
|
1014
|
+
handle?.clear();
|
|
1015
|
+
setClearing(false);
|
|
1016
|
+
}}
|
|
1017
|
+
title="Clear the canvas?"
|
|
1018
|
+
body="Everything drawn here is removed. Undo brings it back."
|
|
1019
|
+
confirmLabel="Clear"
|
|
1020
|
+
tone="danger"
|
|
1021
|
+
/>
|
|
1022
|
+
</div>
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/** The tool rail: a vertical pill on the left; a bottom bar on a phone. */
|
|
1027
|
+
function Rail({ tools, stamps, tool, stamp, onTool, onStamp }) {
|
|
1028
|
+
return (
|
|
1029
|
+
<div
|
|
1030
|
+
role="toolbar"
|
|
1031
|
+
aria-label="Tools"
|
|
1032
|
+
className={cx(
|
|
1033
|
+
"absolute z-20 flex gap-0.5 rounded-xl border border-border bg-popover p-1 shadow-lg",
|
|
1034
|
+
"bottom-3 left-1/2 max-w-[calc(100%-1.5rem)] -translate-x-1/2 flex-row overflow-x-auto",
|
|
1035
|
+
"md:bottom-auto md:left-3 md:top-1/2 md:max-h-[calc(100%-1.5rem)] md:max-w-none md:-translate-x-0 md:-translate-y-1/2 md:flex-col md:overflow-y-auto md:overflow-x-visible",
|
|
1036
|
+
)}
|
|
1037
|
+
>
|
|
1038
|
+
{tools.map((t) => (
|
|
1039
|
+
<Tooltip key={t.id} label={t.key ? `${t.label} — ${t.key}` : t.label} side="right">
|
|
1040
|
+
<Toggle variant="quiet" size="sm" icon={t.icon} label={t.label} pressed={!stamp && tool === t.tool} onPressedChange={() => onTool(t)} aria-keyshortcuts={t.key ?? undefined} />
|
|
1041
|
+
</Tooltip>
|
|
1042
|
+
))}
|
|
1043
|
+
{stamps.length ? (
|
|
1044
|
+
<>
|
|
1045
|
+
<span aria-hidden="true" className="mx-0.5 w-px shrink-0 self-stretch bg-border md:mx-0.5 md:my-0.5 md:h-px md:w-auto" />
|
|
1046
|
+
{stamps.map((s) => (
|
|
1047
|
+
<Tooltip key={s.id} label={`${s.label} — click the canvas to place`} side="right">
|
|
1048
|
+
<Toggle variant="quiet" size="sm" icon={s.icon ?? "plus"} label={s.label} pressed={stamp?.id === s.id} onPressedChange={() => onStamp(s)} />
|
|
1049
|
+
</Tooltip>
|
|
1050
|
+
))}
|
|
1051
|
+
</>
|
|
1052
|
+
) : null}
|
|
1053
|
+
</div>
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/** The floating toolbar above a selection: colours from the theme, stroke,
|
|
1058
|
+
* text size, order, duplicate, delete. */
|
|
1059
|
+
function ContextBar({ box, tokens, hasShape, hasLine, hasText, hasStroke, roughness, width, onColour, onWidth, onRough, onFont, onLayer, onDuplicate, onDelete }) {
|
|
1060
|
+
const above = box.y > 64;
|
|
1061
|
+
const left = Math.max(8, Math.min(box.vw - 8, box.x + box.w / 2));
|
|
1062
|
+
const top = above ? box.y - 10 : Math.min(box.vh - 56, box.y + box.h + 10);
|
|
1063
|
+
return (
|
|
1064
|
+
<div
|
|
1065
|
+
role="toolbar"
|
|
1066
|
+
aria-label="Selection"
|
|
1067
|
+
className={cx(
|
|
1068
|
+
"absolute z-20 flex max-w-[calc(100%-1rem)] items-center gap-0.5 overflow-x-auto rounded-lg border border-border bg-popover p-0.5 text-popover-foreground shadow-lg",
|
|
1069
|
+
"-translate-x-1/2 transition-opacity duration-150 motion-reduce:transition-none",
|
|
1070
|
+
above && "-translate-y-full",
|
|
1071
|
+
)}
|
|
1072
|
+
style={{ left, top }}
|
|
1073
|
+
>
|
|
1074
|
+
{hasStroke ? <Swatches label="Colour" prop="strokeColor" list={STROKES} value={tokens.strokeColor} ring onPick={onColour} /> : null}
|
|
1075
|
+
{hasShape ? <Swatches label="Fill" prop="backgroundColor" list={FILLS} value={tokens.backgroundColor} onPick={onColour} /> : null}
|
|
1076
|
+
{hasLine ? (
|
|
1077
|
+
<>
|
|
1078
|
+
<Separator orientation="vertical" className="my-1" />
|
|
1079
|
+
<Popover
|
|
1080
|
+
label="Stroke"
|
|
1081
|
+
variant="quiet"
|
|
1082
|
+
size="sm"
|
|
1083
|
+
width="auto"
|
|
1084
|
+
className="p-2"
|
|
1085
|
+
trigger={
|
|
1086
|
+
<span className="inline-flex items-center gap-1" aria-label="Stroke">
|
|
1087
|
+
<Icon name="minus" size={16} />
|
|
1088
|
+
</span>
|
|
1089
|
+
}
|
|
1090
|
+
>
|
|
1091
|
+
<div className="flex flex-col gap-2">
|
|
1092
|
+
<div className="flex gap-1" role="group" aria-label="Stroke width">
|
|
1093
|
+
{WIDTHS.map((w) => (
|
|
1094
|
+
<Toggle key={w.value} size="sm" pressed={width === w.value} onPressedChange={() => onWidth(w.value)}>
|
|
1095
|
+
{w.label}
|
|
1096
|
+
</Toggle>
|
|
1097
|
+
))}
|
|
1098
|
+
</div>
|
|
1099
|
+
<div className="flex gap-1" role="group" aria-label="Line style">
|
|
1100
|
+
<Toggle size="sm" pressed={roughness === 0} onPressedChange={() => onRough(0)}>
|
|
1101
|
+
Clean
|
|
1102
|
+
</Toggle>
|
|
1103
|
+
<Toggle size="sm" pressed={roughness > 0} onPressedChange={() => onRough(1)}>
|
|
1104
|
+
Sketchy
|
|
1105
|
+
</Toggle>
|
|
1106
|
+
</div>
|
|
1107
|
+
</div>
|
|
1108
|
+
</Popover>
|
|
1109
|
+
</>
|
|
1110
|
+
) : null}
|
|
1111
|
+
{hasText ? (
|
|
1112
|
+
<>
|
|
1113
|
+
<Separator orientation="vertical" className="my-1" />
|
|
1114
|
+
<Tooltip label="Smaller text">
|
|
1115
|
+
<IconButton icon="minus" label="Smaller text" size="sm" onClick={() => onFont(false)} />
|
|
1116
|
+
</Tooltip>
|
|
1117
|
+
<Icon name="type" size={16} className="text-muted-foreground" />
|
|
1118
|
+
<Tooltip label="Larger text">
|
|
1119
|
+
<IconButton icon="plus" label="Larger text" size="sm" onClick={() => onFont(true)} />
|
|
1120
|
+
</Tooltip>
|
|
1121
|
+
</>
|
|
1122
|
+
) : null}
|
|
1123
|
+
<Separator orientation="vertical" className="my-1" />
|
|
1124
|
+
<Tooltip label="Bring to front">
|
|
1125
|
+
<IconButton icon="bring-front" label="Bring to front" size="sm" onClick={() => onLayer(true)} />
|
|
1126
|
+
</Tooltip>
|
|
1127
|
+
<Tooltip label="Send to back">
|
|
1128
|
+
<IconButton icon="send-back" label="Send to back" size="sm" onClick={() => onLayer(false)} />
|
|
1129
|
+
</Tooltip>
|
|
1130
|
+
<Tooltip label="Duplicate">
|
|
1131
|
+
<IconButton icon="copy" label="Duplicate" size="sm" onClick={onDuplicate} />
|
|
1132
|
+
</Tooltip>
|
|
1133
|
+
<Tooltip label="Delete">
|
|
1134
|
+
<IconButton icon="trash" label="Delete" size="sm" onClick={onDelete} />
|
|
1135
|
+
</Tooltip>
|
|
1136
|
+
</div>
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function Swatches({ label, prop, list, value, ring, onPick }) {
|
|
1141
|
+
const cur = list.find((s) => s.token === value);
|
|
1142
|
+
return (
|
|
1143
|
+
<Popover
|
|
1144
|
+
label={label}
|
|
1145
|
+
variant="quiet"
|
|
1146
|
+
size="sm"
|
|
1147
|
+
width="auto"
|
|
1148
|
+
className="p-2"
|
|
1149
|
+
trigger={
|
|
1150
|
+
<span className="inline-flex items-center" aria-label={`${label}: ${cur?.label ?? "custom"}`}>
|
|
1151
|
+
<span className={cx("inline-block size-5 rounded-full border border-border", ring && "ring-2 ring-inset ring-background", SWATCH[value] ?? "bg-muted")} />
|
|
1152
|
+
</span>
|
|
1153
|
+
}
|
|
1154
|
+
>
|
|
1155
|
+
<div className="grid grid-cols-6 gap-1" role="group" aria-label={label}>
|
|
1156
|
+
{list.map((s) => (
|
|
1157
|
+
<Tooltip key={s.token} label={s.label}>
|
|
1158
|
+
<Button variant="quiet" size="sm" iconOnly aria-label={s.label} aria-pressed={value === s.token} className="aria-pressed:bg-accent" onClick={() => onPick(prop, s.token)}>
|
|
1159
|
+
<span className={cx("grid size-6 place-items-center rounded-full border border-border", SWATCH[s.token])}>
|
|
1160
|
+
{s.token === "none" ? <Icon name="x" size={14} className="text-muted-foreground" /> : null}
|
|
1161
|
+
</span>
|
|
1162
|
+
</Button>
|
|
1163
|
+
</Tooltip>
|
|
1164
|
+
))}
|
|
1165
|
+
</div>
|
|
1166
|
+
</Popover>
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function SaveState({ save, onRetry }) {
|
|
1171
|
+
if (save.state === "idle") return null;
|
|
1172
|
+
if (save.state === "error") {
|
|
1173
|
+
return (
|
|
1174
|
+
<div role="alert" className="flex items-center gap-1 rounded-lg border border-border bg-popover py-0.5 pl-2.5 pr-0.5 text-xs shadow-sm">
|
|
1175
|
+
<Icon name="warning" size={14} className="text-destructive" />
|
|
1176
|
+
<span>{save.error || "Couldn't save"}</span>
|
|
1177
|
+
<Button variant="quiet" size="sm" onClick={onRetry}>
|
|
1178
|
+
Retry
|
|
1179
|
+
</Button>
|
|
1180
|
+
</div>
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
return (
|
|
1184
|
+
<p role="status" className="rounded-lg border border-border bg-popover px-2.5 py-1.5 text-xs text-muted-foreground shadow-sm">
|
|
1185
|
+
{save.state === "saving" ? "Saving…" : "Saved"}
|
|
1186
|
+
</p>
|
|
1187
|
+
);
|
|
1188
|
+
}
|