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,99 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "sketch-canvas",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "SketchCanvas",
|
|
6
|
+
"description": "A freeform drawing canvas (Excalidraw) in the workspace theme: a Miro-style tool rail (select, pan, draw, shapes, arrows, text, image, eraser, frames), a floating toolbar over the selection with colours from the theme's tokens, zoom and undo controls, a friendly empty state, and stamp tools that place ready-made drawings (UI wireframe pieces built in). Optional locked background image for annotating a screenshot. The sketch is plain JSON; helpers read its words for an agent, and the handle exports a PNG.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.3.1/r/core.json"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"@excalidraw/excalidraw@0.18.1"
|
|
12
|
+
],
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "registry/blocks/sketch-canvas/index.js",
|
|
16
|
+
"type": "registry:component",
|
|
17
|
+
"target": "@components/makersclaw/blocks/sketch-canvas/index.js",
|
|
18
|
+
"content": "// The SketchCanvas block — `makersclaw add sketch-canvas`. Fixed: compose\n// it through its props (README.md beside this file). The helpers read a\n// sketch anywhere, the server included; the blocks built on this one\n// (whiteboard, annotate, sketch-to-app…) import it, never\n// @excalidraw/excalidraw directly.\nexport { SketchCanvas, useSketchCanvas, SKETCH_TOOLS } from \"./sketch-canvas\";\nexport { WIREFRAME_STAMPS } from \"./stamps\";\nexport { STROKES, FILLS } from \"./palette\";\nexport { emptySketch, isSketchEmpty, sketchToText, sketchItems, toSketch } from \"./sketch\";\n"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"path": "registry/blocks/sketch-canvas/sketch-canvas.jsx",
|
|
22
|
+
"type": "registry:component",
|
|
23
|
+
"target": "@components/makersclaw/blocks/sketch-canvas/sketch-canvas.jsx",
|
|
24
|
+
"content": "\"use client\";\n\n// SketchCanvas — Excalidraw, in the workspace theme, behind one set of props.\n//\n// Client only, one static bundle. Excalidraw needs the DOM, and the app's\n// check refuses import() (so no next/dynamic): the modules load with the\n// page (on the server too — see excalidraw.js and ssr-shim.js for how that\n// is made safe), and the canvas itself mounts only after the first effect\n// (the mount guard). Until then — on the server and in the first client\n// paint — the same frame shows, with the sketch's words for a reader.\n//\n// What is ours and what is Excalidraw's: Excalidraw draws, selects, edits\n// text, keeps history and answers its own shortcuts. The tool rail, the\n// context toolbar (colours from the theme), the zoom and undo buttons, the\n// menu, the save state and the empty state are ours, in the kit; theme.js\n// hides the Excalidraw UI they replace. Where ours drives an Excalidraw\n// action that has no API (undo, zoom, layer order, duplicate, delete,\n// font size) it presses that action's own shortcut on the canvas, so the\n// behaviour is Excalidraw's exactly.\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { Button, Confirm, Icon, IconButton, Kbd, Menu, Popover, Separator, Toggle, Tooltip, cx } from \"../../ui\";\nimport {\n CaptureUpdateAction,\n Excalidraw,\n convertToExcalidrawElements,\n exportToBlob,\n getCommonBounds,\n getSceneVersion,\n newElementWith,\n restoreElements,\n sceneCoordsToViewportCoords,\n viewportCoordsToSceneCoords,\n} from \"./excalidraw\";\nimport { FILLS, STROKES, elementTokens, paletteKey, repaint, resolveColours, resolvePalette, tagTokens } from \"./palette\";\nimport { BACKGROUND_ROLE, SKETCH_VERSION, SYSTEM_FONT, filesFor, isBackground, pickAppState, sketchToText, toSketch } from \"./sketch\";\nimport { ROOT_VARS, THEME, TRIM_CSS } from \"./theme\";\n\n/** A ref for the canvas's handle: `const sketch = useSketchCanvas()`, then\n * `<SketchCanvas canvasRef={sketch} />` and `await sketch.current?.exportImage()`. */\nexport function useSketchCanvas() {\n return useRef(null);\n}\n\n/** The built-in tools, in rail order. `tools` picks a subset by id. */\nexport const SKETCH_TOOLS = [\n { id: \"select\", tool: \"selection\", label: \"Select\", icon: \"mouse-pointer\", key: \"V\" },\n { id: \"hand\", tool: \"hand\", label: \"Pan\", icon: \"hand\", key: \"H\" },\n { id: \"draw\", tool: \"freedraw\", label: \"Draw\", icon: \"pencil\", key: \"P\" },\n { id: \"rectangle\", tool: \"rectangle\", label: \"Rectangle\", icon: \"square\", key: \"R\" },\n { id: \"ellipse\", tool: \"ellipse\", label: \"Ellipse\", icon: \"circle\", key: \"O\" },\n { id: \"diamond\", tool: \"diamond\", label: \"Diamond\", icon: \"diamond\", key: \"D\" },\n { id: \"arrow\", tool: \"arrow\", label: \"Arrow\", icon: \"arrow-right\", key: \"A\" },\n { id: \"line\", tool: \"line\", label: \"Line\", icon: \"minus\", key: \"L\" },\n { id: \"text\", tool: \"text\", label: \"Text\", icon: \"type\", key: \"T\" },\n { id: \"image\", tool: \"image\", label: \"Image\", icon: \"image\", key: null },\n { id: \"eraser\", tool: \"eraser\", label: \"Eraser\", icon: \"eraser\", key: \"E\" },\n { id: \"frame\", tool: \"frame\", label: \"Frame\", icon: \"frame\", key: \"F\" },\n];\n\n// Excalidraw's own single-key tool shortcuts; a tool left out of `tools`\n// has its keys swallowed so it cannot be reached by keyboard either.\nconst TOOL_KEYS = {\n selection: [\"v\", \"1\"],\n rectangle: [\"r\", \"2\"],\n diamond: [\"d\", \"3\"],\n ellipse: [\"o\", \"4\"],\n arrow: [\"a\", \"5\"],\n line: [\"l\", \"6\"],\n freedraw: [\"p\", \"x\", \"7\"],\n text: [\"t\", \"8\"],\n image: [\"9\"],\n eraser: [\"e\", \"0\"],\n hand: [\"h\"],\n frame: [\"f\"],\n laser: [\"k\"],\n};\n\n// Tailwind needs whole class names in the source: one per token.\nconst SWATCH = {\n foreground: \"bg-foreground\",\n \"muted-foreground\": \"bg-muted-foreground\",\n primary: \"bg-primary\",\n destructive: \"bg-destructive\",\n warning: \"bg-warning\",\n success: \"bg-success\",\n info: \"bg-info\",\n \"chart-2\": \"bg-chart-2\",\n \"chart-3\": \"bg-chart-3\",\n \"chart-4\": \"bg-chart-4\",\n \"chart-5\": \"bg-chart-5\",\n none: \"bg-background\",\n muted: \"bg-muted\",\n \"primary:soft\": \"bg-primary/25\",\n \"destructive:soft\": \"bg-destructive/25\",\n \"warning:soft\": \"bg-warning/25\",\n \"success:soft\": \"bg-success/25\",\n \"info:soft\": \"bg-info/25\",\n \"chart-2:soft\": \"bg-chart-2/25\",\n \"chart-3:soft\": \"bg-chart-3/25\",\n \"chart-4:soft\": \"bg-chart-4/25\",\n \"chart-5:soft\": \"bg-chart-5/25\",\n};\n\nconst WIDTHS = [\n { value: 1, label: \"Thin\" },\n { value: 2, label: \"Medium\" },\n { value: 4, label: \"Bold\" },\n];\n\nconst BG_ID = \"sketch-background\";\nconst SHAPES = new Set([\"rectangle\", \"ellipse\", \"diamond\"]);\nconst LINES = new Set([\"arrow\", \"line\", \"freedraw\"]);\nconst isDarwin = () => typeof navigator !== \"undefined\" && /Mac|iPod|iPhone|iPad/.test(navigator.platform);\nconst nonEmpty = (v) => (v && (Array.isArray(v) ? v.length : Array.isArray(v.elements) ? v.elements.length : false) ? v : null);\n\n/** A short stable hash (for a background file id). */\nfunction hash(s) {\n let h = 5381;\n for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) >>> 0;\n return h.toString(36);\n}\n\nfunction mimeOf(src) {\n const m = /^data:([^;,]+)/.exec(src);\n if (m) return m[1];\n const ext = /\\.(png|jpe?g|webp|gif|svg)(?:[?#]|$)/i.exec(src)?.[1]?.toLowerCase();\n return { png: \"image/png\", jpg: \"image/jpeg\", jpeg: \"image/jpeg\", webp: \"image/webp\", gif: \"image/gif\", svg: \"image/svg+xml\" }[ext] ?? \"image/png\";\n}\n\n/** The workspace's light or dark where the canvas sits. */\nfunction schemeOf(root) {\n const scoped = root?.closest?.(\"[data-theme]\")?.getAttribute(\"data-theme\");\n if (scoped === \"light\" || scoped === \"dark\") return scoped;\n const cs = root ? getComputedStyle(root).colorScheme || \"\" : \"\";\n if (/\\bdark\\b/.test(cs) && !/\\blight\\b/.test(cs)) return \"dark\";\n if (/\\blight\\b/.test(cs) && !/\\bdark\\b/.test(cs)) return \"light\";\n return typeof matchMedia !== \"undefined\" && matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n}\n\n/** What is saved of the scene: live elements with their tokens, the\n * files they show, the appState a sketch keeps. */\nfunction sceneValue(api, palette) {\n return valueOf(api.getSceneElements(), api.getFiles(), api.getAppState(), palette);\n}\n\nfunction valueOf(elements, files, appState, palette) {\n const live = tagTokens(\n elements.filter((el) => !el.isDeleted && !isBackground(el)),\n palette,\n );\n return { type: \"sketch\", version: SKETCH_VERSION, elements: live, files: filesFor(live, files), appState: pickAppState(appState) };\n}\n\n/** A cheap fingerprint of a value's elements and files. */\nfunction signature(value) {\n const s = toSketch(value);\n return `${s.elements.map((e) => `${e.id}:${e.version}`).join(\",\")}|${Object.keys(s.files).sort().join(\",\")}`;\n}\n\n/** A box never shorter than the text bound inside it. Excalidraw re-wraps\n * and re-measures text as a scene loads (the system font, a narrower box)\n * but grows a container only while someone types, so text that wrapped to\n * an extra line hung out of its box (\"Annual billing toggle\" on the\n * whiteboard demo, seen in the pane 2026-09-24). Grows the box and centres\n * the text again; ellipses and diamonds need more room for the same text. */\nconst BOUND_PAD = 5;\nfunction fitContainers(elements) {\n const byId = new Map(elements.map((el) => [el.id, el]));\n const grown = new Map();\n for (const t of elements) {\n if (t?.type !== \"text\" || !t.containerId || t.isDeleted) continue;\n const box = grown.get(t.containerId) ?? byId.get(t.containerId);\n if (!box || box.type === \"arrow\" || box.type === \"line\") continue;\n // A rectangle needs the text plus its padding; an ellipse or diamond\n // fits text of about 1/√2 of its height (a numbered pin: 18px of text\n // in a 28px circle is fine, and must stay a circle).\n const round = box.type === \"ellipse\" || box.type === \"diamond\";\n const need = Math.ceil(round ? t.height * Math.SQRT2 : t.height + BOUND_PAD * 2);\n if (box.height + 1 >= need) continue;\n grown.set(box.id, { ...box, height: need, version: (box.version ?? 1) + 1 });\n }\n if (!grown.size) return elements;\n return elements.map((el) => {\n if (grown.has(el.id)) return grown.get(el.id);\n if (el?.type === \"text\" && grown.has(el.containerId)) {\n const box = grown.get(el.containerId);\n return { ...el, y: box.y + (box.height - el.height) / 2, version: (el.version ?? 1) + 1 };\n }\n return el;\n });\n}\n\n/** Elements as they should enter the scene: every text in the system\n * font (re-measured), tokened colours resolved in this theme. */\nfunction prepareElements(elements, palette) {\n const fonted = (elements ?? []).map((el) => (el?.type === \"text\" && el.fontFamily !== SYSTEM_FONT ? { ...el, fontFamily: SYSTEM_FONT } : el));\n const restored = fitContainers(restoreElements(fonted, null, { refreshDimensions: true, repairBindings: true }));\n const patches = new Map(repaint(restored, palette, null).map((p) => [p.id, p.patch]));\n return restored.map((el) => (patches.has(el.id) ? { ...el, ...patches.get(el.id) } : el));\n}\n\n/** A skeleton (convertToExcalidrawElements' input) ready to convert:\n * token colours resolved, texts in the system font. */\nfunction prepareSkeleton(sk, palette) {\n const out = resolveColours(sk, palette);\n if (out.type === \"text\" && !out.fontFamily) out.fontFamily = SYSTEM_FONT;\n if (out.label && !out.label.fontFamily) out.label = { ...out.label, fontFamily: SYSTEM_FONT };\n if (out.type !== \"text\" && !out.strokeColor && palette.foreground) out.strokeColor = palette.foreground;\n if (out.type === \"text\" && !out.strokeColor && palette.foreground) out.strokeColor = palette.foreground;\n return out;\n}\n\nfunction boundsOfSkeletons(sks) {\n const boxes = sks.map((s) => {\n const w = s.width ?? (s.type === \"text\" ? String(s.text ?? \"\").length * (s.fontSize ?? 20) * 0.55 : 0);\n const h = s.height ?? (s.type === \"text\" ? (s.fontSize ?? 20) * 1.25 : 0);\n return { x1: s.x ?? 0, y1: s.y ?? 0, x2: (s.x ?? 0) + w, y2: (s.y ?? 0) + h };\n });\n const x1 = Math.min(...boxes.map((b) => b.x1));\n const y1 = Math.min(...boxes.map((b) => b.y1));\n return { x: x1, y: y1, width: Math.max(...boxes.map((b) => b.x2)) - x1, height: Math.max(...boxes.map((b) => b.y2)) - y1 };\n}\n\nexport function SketchCanvas(props) {\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n return <Frame {...props} mounted={mounted} />;\n}\n\n/** The frame, the same before and after the canvas mounts: server HTML\n * already has its size, border and text alternative; nothing jumps. */\nfunction Frame({ mounted, className, label = \"Sketch\", ...props }) {\n const rootRef = useRef(null);\n const descId = useId();\n const [text, setText] = useState(() => sketchToText(nonEmpty(props.value) ?? nonEmpty(props.defaultValue)));\n const sized = /(^|\\s)(h-|min-h-|size-|flex-1|grow)/.test(className ?? \"\");\n return (\n <div\n ref={rootRef}\n data-sketch-canvas=\"\"\n role=\"group\"\n aria-label={label}\n aria-describedby={descId}\n className={cx(\n \"relative isolate overflow-hidden rounded-lg border border-border bg-background text-foreground\",\n !sized && \"h-[32rem]\",\n ROOT_VARS,\n THEME,\n className,\n )}\n >\n <style>{TRIM_CSS}</style>\n <p id={descId} className=\"sr-only\">\n {text ? `The sketch says: ${text.replace(/\\n+/g, \". \")}` : \"The sketch is empty.\"}\n </p>\n {mounted ? (\n <LiveSketch {...props} rootRef={rootRef} onText={setText} />\n ) : (\n <div className=\"grid h-full place-items-center text-sm text-muted-foreground\" aria-busy=\"true\">\n Loading the canvas…\n </div>\n )}\n </div>\n );\n}\n\nfunction LiveSketch({\n rootRef,\n onText,\n value,\n defaultValue,\n onChange,\n onSave,\n changeDelay = 600,\n readOnly = false,\n background,\n tools,\n stamps,\n grid = true,\n empty,\n actions,\n canvasRef,\n onReady,\n}) {\n const apiRef = useRef(null);\n const [api, setApi] = useState(null);\n // Excalidraw hands over its API before it has loaded initialData, then\n // loads it asynchronously and replaces the scene. Anything laid before\n // that is wiped: annotate's screenshot vanished that way (its file stayed,\n // its image element went; seen in the pane 2026-09-24). `ready` turns true\n // once the first load is done, and the effects that write the scene wait\n // for it.\n const [ready, setReady] = useState(false);\n useEffect(() => {\n if (!api) return;\n if (!api.getAppState().isLoading) return setReady(true);\n const off = api.onChange((_els, appState) => {\n if (!appState.isLoading) setReady(true);\n });\n return () => off?.();\n }, [api]);\n const [palette, setPalette] = useState(() => resolvePalette(rootRef.current));\n const paletteRef = useRef(palette);\n const [scheme, setScheme] = useState(() => schemeOf(rootRef.current));\n const [ui, setUi] = useState({ tool: \"selection\", zoom: 1, selected: [], box: null, count: 0, grid: false });\n const uiKey = useRef(\"\");\n const [stamp, setStamp] = useState(null);\n const [save, setSave] = useState({ state: \"idle\" });\n const [clearing, setClearing] = useState(false);\n const current = useRef({ strokeColor: \"foreground\", backgroundColor: \"none\" });\n\n // What was last sent out / taken in, so a controlled `value` that is\n // only our own echo never reloads the scene.\n const initial = useRef(null);\n if (!initial.current) initial.current = toSketch(nonEmpty(value) ?? nonEmpty(defaultValue));\n const lastSig = useRef(null);\n if (lastSig.current === null) lastSig.current = signature(initial.current);\n const lastVersion = useRef(-1);\n const timer = useRef(0);\n const pending = useRef(false);\n const latest = useRef(null);\n const saveSeq = useRef(0);\n const lastValue = useRef(initial.current);\n const cbs = useRef({});\n cbs.current = { onChange, onSave, onText, onReady };\n\n const activeTools = useMemo(() => {\n const want = Array.isArray(tools) && tools.length ? new Set(tools) : null;\n return SKETCH_TOOLS.filter((t) => !want || want.has(t.id));\n }, [tools]);\n const blockedKeys = useMemo(() => {\n const on = new Set(activeTools.map((t) => t.tool));\n const keys = new Set();\n for (const [tool, ks] of Object.entries(TOOL_KEYS)) if (!on.has(tool)) ks.forEach((k) => keys.add(k));\n return keys;\n }, [activeTools]);\n const stampList = useMemo(() => (Array.isArray(stamps) ? stamps.filter((s) => s && s.id && typeof s.create === \"function\") : []), [stamps]);\n\n const initialData = useMemo(() => {\n const els = prepareElements(initial.current.elements, palette);\n return {\n elements: els,\n files: initial.current.files,\n appState: {\n viewBackgroundColor: \"transparent\",\n currentItemFontFamily: SYSTEM_FONT,\n currentItemStrokeColor: palette.foreground,\n currentItemBackgroundColor: \"transparent\",\n currentItemFillStyle: \"solid\",\n currentItemRoundness: \"round\",\n gridModeEnabled: !!initial.current.appState.gridModeEnabled,\n },\n scrollToContent: true,\n };\n // Excalidraw reads initialData once.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // ---- output -------------------------------------------------------------\n\n const runSave = useCallback(async (v) => {\n const fn = cbs.current.onSave;\n if (!fn) return;\n const seq = ++saveSeq.current;\n setSave({ state: \"saving\" });\n let res;\n try {\n res = await fn(v);\n } catch (e) {\n res = { ok: false, error: e?.message };\n }\n if (seq !== saveSeq.current) return;\n setSave(res?.ok ? { state: \"saved\" } : { state: \"error\", error: res?.error });\n }, []);\n\n const emit = useCallback(() => {\n clearTimeout(timer.current);\n pending.current = false;\n // From what the last onChange handed us (so a flush while unmounting\n // never reads a scene Excalidraw is tearing down), else the API.\n const seen = latest.current;\n const a = apiRef.current;\n if (!seen && !a) return;\n const v = seen ? valueOf(seen.elements, seen.files, seen.appState, paletteRef.current) : sceneValue(a, paletteRef.current);\n lastValue.current = v;\n lastSig.current = signature(v);\n cbs.current.onText?.(sketchToText(v));\n cbs.current.onChange?.(v);\n runSave(v);\n }, [runSave]);\n\n const schedule = useCallback(() => {\n clearTimeout(timer.current);\n pending.current = true;\n timer.current = setTimeout(emit, Math.max(0, changeDelay));\n }, [emit, changeDelay]);\n\n // Leaving with an edit still in its debounce: send it now, not never.\n const flush = useRef(emit);\n flush.current = emit;\n useEffect(\n () => () => {\n clearTimeout(timer.current);\n if (!pending.current) return;\n try {\n flush.current();\n } catch {\n // the canvas is already gone; nothing left to read\n }\n },\n [],\n );\n\n // ---- the scene ----------------------------------------------------------\n\n const quietUpdate = useCallback((elements, appState) => {\n const a = apiRef.current;\n if (!a) return;\n a.updateScene({ elements, appState, captureUpdate: CaptureUpdateAction.NEVER });\n lastVersion.current = getSceneVersion(a.getSceneElementsIncludingDeleted());\n }, []);\n\n /** Press one of Excalidraw's own shortcuts on the canvas. */\n const press = useCallback((key, code, mods = {}) => {\n const target = rootRef.current?.querySelector(\".excalidraw\");\n if (!target) return;\n const mac = isDarwin();\n target.dispatchEvent(\n new KeyboardEvent(\"keydown\", {\n key,\n code,\n bubbles: true,\n cancelable: true,\n metaKey: !!mods.cmd && mac,\n ctrlKey: !!mods.cmd && !mac,\n shiftKey: !!mods.shift,\n altKey: !!mods.alt,\n }),\n );\n }, [rootRef]);\n\n const layer = useCallback(\n (front) => {\n const mac = isDarwin();\n press(front ? \"]\" : \"[\", front ? \"BracketRight\" : \"BracketLeft\", mac ? { cmd: true, alt: true } : { cmd: true, shift: true });\n },\n [press],\n );\n\n // The locked background image, laid from the prop at the back.\n const bgKey = background?.src ? `${background.src.length}:${hash(background.src)}:${background.width ?? \"\"}x${background.height ?? \"\"}` : \"\";\n useEffect(() => {\n const a = api;\n if (!a || !ready) return;\n const others = () => a.getSceneElementsIncludingDeleted().filter((el) => !isBackground(el));\n if (!background?.src) {\n if (a.getSceneElementsIncludingDeleted().some(isBackground)) quietUpdate(others());\n return;\n }\n let cancelled = false;\n const place = (width, height) => {\n if (cancelled || !width || !height) return;\n const fileId = `bg-${hash(background.src)}`;\n a.addFiles([{ id: fileId, dataURL: background.src, mimeType: mimeOf(background.src), created: Date.now() }]);\n const [bg] = convertToExcalidrawElements(\n [{ type: \"image\", id: BG_ID, fileId, x: 0, y: 0, width, height, locked: true, status: \"saved\", customData: { role: BACKGROUND_ROLE } }],\n { regenerateIds: false },\n );\n const rest = others();\n quietUpdate([bg, ...rest]);\n if (!rest.some((el) => !el.isDeleted)) a.scrollToContent(undefined, { fitToContent: true });\n };\n if (background.width && background.height) place(background.width, background.height);\n else {\n const img = new window.Image();\n img.onload = () => place(img.naturalWidth, img.naturalHeight);\n img.src = background.src;\n }\n return () => {\n cancelled = true;\n };\n // bgKey stands for background's fields.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [api, ready, bgKey, quietUpdate]);\n\n // Controlled: a `value` that is not what we last sent replaces the scene.\n useEffect(() => {\n const a = api;\n if (!a || !ready || value === undefined) return;\n const sig = signature(value);\n if (sig === lastSig.current) return;\n lastSig.current = sig;\n const s = toSketch(value);\n lastValue.current = s;\n const bg = a.getSceneElementsIncludingDeleted().filter(isBackground);\n const files = Object.values(s.files);\n if (files.length) a.addFiles(files);\n quietUpdate([...bg, ...prepareElements(s.elements, paletteRef.current)], { gridModeEnabled: !!s.appState.gridModeEnabled });\n a.history.clear();\n cbs.current.onText?.(sketchToText(s));\n }, [api, ready, value, quietUpdate]);\n\n // Theme and scheme: re-read the tokens when anything that sets them\n // changes, and repaint what was drawn in them.\n const refresh = useCallback(() => {\n const root = rootRef.current;\n if (!root) return;\n setScheme(schemeOf(root));\n const next = resolvePalette(root);\n const prev = paletteRef.current;\n if (paletteKey(next) === paletteKey(prev)) return;\n paletteRef.current = next;\n setPalette(next);\n const a = apiRef.current;\n if (!a) return;\n const all = a.getSceneElementsIncludingDeleted();\n const patches = new Map(repaint(all, next, prev).map((p) => [p.id, p.patch]));\n quietUpdate(\n patches.size ? all.map((el) => (patches.has(el.id) ? newElementWith(el, patches.get(el.id)) : el)) : undefined,\n {\n currentItemStrokeColor: next[current.current.strokeColor] ?? next.foreground,\n currentItemBackgroundColor: next[current.current.backgroundColor] ?? \"transparent\",\n },\n );\n }, [rootRef, quietUpdate]);\n\n useEffect(() => {\n let t = 0;\n const soon = () => {\n clearTimeout(t);\n t = setTimeout(refresh, 30);\n };\n const obs = new MutationObserver((records) => {\n for (const r of records) for (const n of r.addedNodes ?? []) if (n.tagName === \"LINK\") n.addEventListener(\"load\", soon, { once: true });\n soon();\n });\n obs.observe(document.documentElement, { attributes: true, attributeFilter: [\"data-theme\", \"class\", \"style\"] });\n obs.observe(document.head, { childList: true });\n const scoped = rootRef.current?.closest(\"[data-theme]\");\n if (scoped && scoped !== document.documentElement) obs.observe(scoped, { attributes: true, attributeFilter: [\"data-theme\", \"class\", \"style\"] });\n const mq = matchMedia(\"(prefers-color-scheme: dark)\");\n mq.addEventListener(\"change\", soon);\n soon();\n return () => {\n clearTimeout(t);\n obs.disconnect();\n mq.removeEventListener(\"change\", soon);\n };\n }, [refresh, rootRef]);\n\n // ---- Excalidraw's events -----------------------------------------------\n\n const fontFix = useRef(0);\n const handleChange = useCallback(\n (elements, appState, files) => {\n latest.current = { elements, appState, files };\n // What our overlays show.\n const selected = elements.filter((el) => appState.selectedElementIds[el.id] && !el.isDeleted);\n const busy =\n appState.newElement || appState.selectionElement || appState.selectedElementsAreBeingDragged || appState.resizingElement || appState.isResizing || appState.isRotating || appState.editingTextElement || appState.multiElement || appState.cursorButton === \"down\";\n let box = null;\n if (selected.length && !busy) {\n const [x1, y1, x2, y2] = getCommonBounds(selected);\n const a = sceneCoordsToViewportCoords({ sceneX: x1, sceneY: y1 }, appState);\n const b = sceneCoordsToViewportCoords({ sceneX: x2, sceneY: y2 }, appState);\n 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 };\n }\n const count = elements.reduce((n, el) => n + (!el.isDeleted && !isBackground(el) ? 1 : 0), 0);\n const next = { tool: appState.activeTool.type, zoom: appState.zoom.value, selected: selected.map((el) => el.id), box, count, grid: !!appState.gridModeEnabled };\n const key = JSON.stringify(next);\n if (key !== uiKey.current) {\n uiKey.current = key;\n setUi(next);\n }\n // Output: only when the drawing changed, not on pan, zoom or select.\n const v = getSceneVersion(elements);\n if (lastVersion.current === -1) lastVersion.current = v;\n else if (v !== lastVersion.current) {\n lastVersion.current = v;\n schedule();\n }\n // Text that arrived in another font (a paste, a dropped file): into\n // the system font, once nothing is being edited.\n if (!busy && elements.some((el) => el.type === \"text\" && !el.isDeleted && el.fontFamily !== SYSTEM_FONT)) {\n clearTimeout(fontFix.current);\n fontFix.current = setTimeout(() => {\n const a = apiRef.current;\n if (!a) return;\n // The whole scene through restoreElements, as Excalidraw does on\n // load: it re-measures text in its container (a subset would lose\n // the bindings it cannot see).\n const all = a.getSceneElementsIncludingDeleted();\n const need = new Set(all.filter((el) => el.type === \"text\" && el.fontFamily !== SYSTEM_FONT).map((el) => el.id));\n if (!need.size) return;\n const fixed = restoreElements(\n all.map((el) => (need.has(el.id) ? { ...el, fontFamily: SYSTEM_FONT } : el)),\n null,\n { refreshDimensions: true, repairBindings: true },\n ).map((el) => (need.has(el.id) ? { ...el, version: el.version + 1, versionNonce: Math.floor(Math.random() * 2 ** 31) } : el));\n a.updateScene({ elements: fixed, captureUpdate: CaptureUpdateAction.NEVER });\n }, 0);\n }\n },\n [schedule],\n );\n\n const handleScroll = useCallback(\n (scrollX, scrollY, zoom) => {\n const root = rootRef.current;\n if (!root) return;\n let gap = 24 * zoom.value;\n while (gap < 10) gap *= 4;\n root.style.setProperty(\"--sketch-dot-gap\", `${gap}px`);\n root.style.setProperty(\"--sketch-dot-x\", `${(scrollX * zoom.value) % gap}px`);\n root.style.setProperty(\"--sketch-dot-y\", `${(scrollY * zoom.value) % gap}px`);\n },\n [rootRef],\n );\n\n // Pasted Excalidraw content keeps its own fonts: into the system font\n // before it lands (no font is ever fetched from a CDN).\n const handlePaste = useCallback((data) => {\n for (const el of data?.elements ?? []) if (el?.type === \"text\") el.fontFamily = SYSTEM_FONT;\n return true;\n }, []);\n\n // ---- the handle ---------------------------------------------------------\n\n const viewCentre = useCallback(() => {\n const st = apiRef.current.getAppState();\n return viewportCoordsToSceneCoords({ clientX: st.offsetLeft + st.width / 2, clientY: st.offsetTop + st.height / 2 }, st);\n }, []);\n\n const insertElements = useCallback(\n (skeletons, opts = {}) => {\n const a = apiRef.current;\n if (!a || !Array.isArray(skeletons) || !skeletons.length) return [];\n const pal = paletteRef.current;\n let sks = skeletons.map((sk) => prepareSkeleton(sk, pal));\n if (opts.at !== false) {\n const b = boundsOfSkeletons(sks);\n const at = opts.at && typeof opts.at === \"object\" ? opts.at : null;\n const c = at ? { x: at.x, y: at.y } : (() => { const v = viewCentre(); return { x: v.x, y: v.y }; })();\n const dx = (opts.anchor === \"top-left\" && at ? c.x : c.x - b.width / 2) - b.x;\n const dy = (opts.anchor === \"top-left\" && at ? c.y : c.y - b.height / 2) - b.y;\n sks = sks.map((sk) => ({ ...sk, x: (sk.x ?? 0) + dx, y: (sk.y ?? 0) + dy }));\n }\n let els = convertToExcalidrawElements(sks, { regenerateIds: true });\n const gid = opts.group || opts.stamp ? `g${Math.random().toString(36).slice(2, 10)}` : null;\n els = els.map((el) => ({\n ...el,\n groupIds: gid ? [...(el.groupIds ?? []), gid] : el.groupIds,\n customData: { ...(el.customData ?? {}), ...(opts.customData ?? {}), ...(opts.stamp ? { stamp: opts.stamp, stampId: gid } : {}) },\n }));\n if (opts.files?.length) a.addFiles(opts.files);\n const select = opts.select !== false;\n a.updateScene({\n elements: [...a.getSceneElementsIncludingDeleted(), ...els],\n appState: select ? { selectedElementIds: Object.fromEntries(els.filter((e) => !e.containerId).map((e) => [e.id, true])), selectedGroupIds: gid ? { [gid]: true } : {} } : undefined,\n captureUpdate: CaptureUpdateAction.IMMEDIATELY,\n });\n return els.map((e) => e.id);\n },\n [viewCentre],\n );\n\n const exportImage = useCallback(async (opts = {}) => {\n const a = apiRef.current;\n if (!a) throw new Error(\"The canvas is not ready yet.\");\n const st = a.getAppState();\n const all = a.getSceneElements();\n const els = opts.onlySelected ? all.filter((el) => st.selectedElementIds[el.id]) : opts.withBackground === false ? all.filter((el) => !isBackground(el)) : all;\n const scale = opts.scale ?? 2;\n return exportToBlob({\n elements: els,\n files: a.getFiles(),\n mimeType: \"image/png\",\n exportPadding: opts.padding ?? 16,\n appState: { ...st, exportBackground: opts.transparent !== true, viewBackgroundColor: paletteRef.current.background ?? \"transparent\", exportWithDarkMode: false, exportScale: scale },\n getDimensions: (w, h) => ({ width: w * scale, height: h * scale, scale }),\n });\n }, []);\n\n const handle = useMemo(() => {\n if (!api) return null;\n return {\n api,\n getScene: (opts = {}) => {\n const v = sceneValue(api, paletteRef.current);\n if (!opts.withBackground) return v;\n const bg = api.getSceneElements().filter(isBackground);\n return { ...v, elements: [...bg, ...v.elements], files: { ...v.files, ...filesFor(bg, api.getFiles()) } };\n },\n setScene: (v) => {\n const s = toSketch(v);\n const bg = api.getSceneElementsIncludingDeleted().filter(isBackground);\n if (Object.keys(s.files).length) api.addFiles(Object.values(s.files));\n api.updateScene({ elements: [...bg, ...prepareElements(s.elements, paletteRef.current)], captureUpdate: CaptureUpdateAction.IMMEDIATELY });\n },\n insertElements,\n exportImage,\n zoomToFit: (opts = {}) => api.scrollToContent(undefined, { fitToContent: true, animate: opts.animate !== false }),\n setTool: (id) => {\n const s = stampList.find((x) => x.id === id);\n if (s) return setStamp(s);\n const t = SKETCH_TOOLS.find((x) => x.id === id || x.tool === id);\n setStamp(null);\n if (t) api.setActiveTool({ type: t.tool });\n },\n clear: () => {\n const bg = api.getSceneElementsIncludingDeleted().filter(isBackground);\n api.updateScene({ elements: bg, captureUpdate: CaptureUpdateAction.IMMEDIATELY });\n },\n undo: () => press(\"z\", \"KeyZ\", { cmd: true }),\n redo: () => press(\"z\", \"KeyZ\", { cmd: true, shift: true }),\n focus: () => rootRef.current?.querySelector(\".excalidraw\")?.focus(),\n palette: () => paletteRef.current,\n repaint: refresh,\n save: emit,\n };\n }, [api, insertElements, exportImage, stampList, press, rootRef, refresh, emit]);\n\n useEffect(() => {\n if (!canvasRef || !handle) return;\n canvasRef.current = handle;\n return () => {\n if (canvasRef.current === handle) canvasRef.current = null;\n };\n }, [canvasRef, handle]);\n useEffect(() => {\n if (handle) cbs.current.onReady?.(handle);\n }, [handle]);\n\n // Open fitted: initialData's scrollToContent only centres the scene, so\n // a board larger than the frame opened on its top-left corner at 100%.\n // Fit once the canvas has its size; never zoom in past 100%.\n useEffect(() => {\n if (!api || !ready) return;\n const t = setTimeout(() => {\n const live = api.getSceneElements().filter((el) => !el.isDeleted);\n if (!live.length) return;\n api.scrollToContent(live, { fitToViewport: true, viewportZoomFactor: 0.9, maxZoom: 1, animate: false });\n }, 150);\n return () => clearTimeout(t);\n }, [api, ready]);\n\n // ---- our controls -------------------------------------------------------\n\n const pickTool = (t) => {\n setStamp(null);\n apiRef.current?.setActiveTool({ type: t.tool });\n };\n\n const placeStamp = (s, at) => {\n insertElements(s.create(), { stamp: s.id, at });\n setStamp(null);\n apiRef.current?.setActiveTool({ type: \"selection\" });\n };\n\n const selectedEls = () => {\n const a = apiRef.current;\n if (!a) return [];\n const ids = a.getAppState().selectedElementIds;\n return a.getSceneElements().filter((el) => ids[el.id]);\n };\n\n const setColour = (prop, token) => {\n const a = apiRef.current;\n const pal = paletteRef.current;\n if (!a || !(token in pal)) return;\n const sel = selectedEls();\n const ids = new Set(sel.map((el) => el.id));\n if (prop === \"strokeColor\") for (const el of sel) for (const b of el.boundElements ?? []) if (b.type === \"text\") ids.add(b.id);\n const all = a.getSceneElementsIncludingDeleted();\n a.updateScene({\n elements: all.map((el) =>\n ids.has(el.id) ? newElementWith(el, { [prop]: pal[token], customData: { ...(el.customData ?? {}), tokens: { ...(el.customData?.tokens ?? {}), [prop]: token } } }) : el,\n ),\n appState: prop === \"strokeColor\" ? { currentItemStrokeColor: pal[token] } : { currentItemBackgroundColor: pal[token] },\n captureUpdate: CaptureUpdateAction.IMMEDIATELY,\n });\n current.current = { ...current.current, [prop]: token };\n };\n\n const setProp = (patch, only) => {\n const a = apiRef.current;\n if (!a) return;\n const ids = new Set(selectedEls().filter(only ?? (() => true)).map((el) => el.id));\n a.updateScene({ elements: a.getSceneElementsIncludingDeleted().map((el) => (ids.has(el.id) ? newElementWith(el, patch) : el)), captureUpdate: CaptureUpdateAction.IMMEDIATELY });\n };\n\n const download = async () => {\n try {\n const blob = await exportImage();\n const url = URL.createObjectURL(blob);\n const link = document.createElement(\"a\");\n link.href = url;\n link.download = \"sketch.png\";\n link.click();\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n } catch {\n setSave((s) => ({ ...s, exportError: true }));\n }\n };\n\n // Keys on their way into the canvas: tools left out, the stamp being\n // placed, Cmd/Ctrl+S.\n const keyDown = (e) => {\n const t = e.target;\n if (t?.tagName === \"TEXTAREA\" || t?.tagName === \"INPUT\" || t?.isContentEditable) return;\n const k = e.key?.toLowerCase();\n if ((e.metaKey || e.ctrlKey) && k === \"s\") {\n e.preventDefault();\n e.stopPropagation();\n if (!readOnly) emit();\n return;\n }\n if ((e.metaKey || e.ctrlKey) && k === \"f\") {\n e.stopPropagation(); // the browser's find, not Excalidraw's (hidden) search\n return;\n }\n if (stamp) {\n if (e.key === \"Escape\") {\n e.preventDefault();\n e.stopPropagation();\n setStamp(null);\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n e.stopPropagation();\n placeStamp(stamp);\n }\n return;\n }\n if (!e.metaKey && !e.ctrlKey && !e.altKey && blockedKeys.has(k)) {\n e.preventDefault();\n e.stopPropagation();\n }\n };\n\n const selected = ui.selected.length ? selectedEls() : [];\n const kinds = new Set(selected.map((el) => el.type));\n const hasShape = [...kinds].some((k) => SHAPES.has(k));\n const hasLine = [...kinds].some((k) => SHAPES.has(k) || LINES.has(k));\n const hasText = kinds.has(\"text\") || selected.some((el) => (el.boundElements ?? []).some((b) => b.type === \"text\"));\n const hasStroke = [...kinds].some((k) => k !== \"image\" && k !== \"frame\" && k !== \"magicframe\");\n const first = selected[0];\n const tokens = first ? elementTokens(first, palette) : {};\n const roughness = first?.roughness ?? 1;\n const width = first?.strokeWidth ?? 2;\n const empty_ = ui.count === 0;\n const showEmpty = empty_ && !readOnly && empty !== false && !stamp && (ui.tool === \"selection\" || ui.tool === \"hand\");\n\n return (\n // raw: a plain box that hears keys on their way into Excalidraw (capture),\n // to swallow the shortcuts of tools left out; no control of its own.\n <div\n className=\"absolute inset-0\"\n onKeyDownCapture={keyDown}\n style={{ \"--sketch-selection\": palette.primary ?? \"currentColor\" }}\n >\n {grid ? (\n <div\n aria-hidden=\"true\"\n 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)]\"\n />\n ) : null}\n <div className=\"absolute inset-0\">\n <Excalidraw\n excalidrawAPI={(a) => {\n apiRef.current = a;\n setApi(a);\n }}\n initialData={initialData}\n onChange={handleChange}\n onScrollChange={handleScroll}\n onPaste={handlePaste}\n theme={scheme}\n viewModeEnabled={readOnly}\n aiEnabled={false}\n detectScroll\n handleKeyboardGlobally={false}\n UIOptions={{\n canvasActions: {\n changeViewBackgroundColor: false,\n clearCanvas: false,\n export: false,\n loadScene: false,\n saveToActiveFile: false,\n toggleTheme: false,\n saveAsImage: false,\n },\n tools: { image: activeTools.some((t) => t.id === \"image\") },\n }}\n renderTopRightUI={() => null}\n />\n </div>\n\n {showEmpty ? (\n <div className=\"pointer-events-none absolute inset-0 z-10 grid place-items-center p-6\">\n {empty ?? (\n <div className=\"flex max-w-xs flex-col items-center gap-2 text-center\">\n <span className=\"grid size-12 place-items-center rounded-full bg-muted text-muted-foreground\">\n <Icon name=\"pencil\" size={22} />\n </span>\n <p className=\"text-base font-medium\">Start sketching</p>\n <p className=\"text-sm text-muted-foreground\">\n Pick a tool, or press <Kbd>P</Kbd> to draw, <Kbd>R</Kbd> for a box, <Kbd>T</Kbd> for text.\n </p>\n </div>\n )}\n </div>\n ) : null}\n\n {stamp ? (\n <>\n {/* raw: a click target laid over the canvas while a stamp waits to\n land — the pointer picks the spot; Enter drops it in the middle. */}\n <div\n aria-hidden=\"true\"\n className=\"absolute inset-0 z-10 cursor-crosshair\"\n onPointerDown={(e) => {\n e.preventDefault();\n const st = apiRef.current?.getAppState();\n if (!st) return;\n placeStamp(stamp, viewportCoordsToSceneCoords({ clientX: e.clientX, clientY: e.clientY }, st));\n }}\n />\n <div className=\"pointer-events-none absolute inset-x-0 top-3 z-20 flex justify-center px-3\">\n <p role=\"status\" className=\"rounded-full border border-border bg-popover px-3 py-1 text-xs text-popover-foreground shadow-sm\">\n Click to place the {stamp.label.toLowerCase()} · Enter for the middle · Esc to cancel\n </p>\n </div>\n </>\n ) : null}\n\n {!readOnly ? (\n <Rail tools={activeTools} stamps={stampList} tool={ui.tool} stamp={stamp} onTool={pickTool} onStamp={(s) => setStamp((cur) => (cur?.id === s.id ? null : s))} />\n ) : null}\n\n {!readOnly && ui.box && selected.length ? (\n <ContextBar\n box={ui.box}\n tokens={tokens}\n hasShape={hasShape}\n hasLine={hasLine}\n hasText={hasText}\n hasStroke={hasStroke}\n roughness={roughness}\n width={width}\n onColour={setColour}\n onWidth={(w) => setProp({ strokeWidth: w }, (el) => el.type !== \"text\")}\n onRough={(r) => setProp({ roughness: r }, (el) => el.type !== \"text\")}\n onFont={(bigger) => press(bigger ? \">\" : \"<\", bigger ? \"Period\" : \"Comma\", { cmd: true, shift: true })}\n onLayer={layer}\n onDuplicate={() => press(\"d\", \"KeyD\", { cmd: true })}\n onDelete={() => press(\"Delete\", \"Delete\")}\n />\n ) : null}\n\n {/* top right: the host's actions, the save state, the menu */}\n <div className=\"absolute right-3 top-3 z-20 flex items-center gap-2\">\n {onSave && !readOnly ? <SaveState save={save} onRetry={() => runSave(lastValue.current)} /> : null}\n {actions}\n <div className=\"rounded-lg border border-border bg-popover shadow-sm\">\n <Menu\n label=\"Canvas menu\"\n size=\"sm\"\n items={[\n { label: \"Download PNG\", icon: \"download\", onSelect: download },\n !readOnly && { type: \"checkbox\", label: \"Snap to grid\", checked: ui.grid, onCheckedChange: (on) => apiRef.current?.updateScene({ appState: { gridModeEnabled: on } }) },\n { label: \"Fit to screen\", icon: \"maximize\", shortcut: \"⇧1\", onSelect: () => handle?.zoomToFit() },\n { label: \"Keyboard shortcuts\", icon: \"keyboard\", shortcut: \"?\", onSelect: () => apiRef.current?.updateScene({ appState: { openDialog: { name: \"help\" } } }) },\n !readOnly && { type: \"separator\" },\n !readOnly && { label: \"Clear canvas\", icon: \"trash\", tone: \"danger\", disabled: empty_, onSelect: () => setClearing(true) },\n ]}\n />\n </div>\n </div>\n\n {/* bottom left: undo, redo */}\n {!readOnly ? (\n <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]\">\n <Tooltip label={`Undo — ${isDarwin() ? \"⌘Z\" : \"Ctrl+Z\"}`}>\n <IconButton icon=\"undo\" label=\"Undo\" size=\"sm\" onClick={() => handle?.undo()} />\n </Tooltip>\n <Tooltip label={`Redo — ${isDarwin() ? \"⇧⌘Z\" : \"Ctrl+Shift+Z\"}`}>\n <IconButton icon=\"redo\" label=\"Redo\" size=\"sm\" onClick={() => handle?.redo()} />\n </Tooltip>\n </div>\n ) : null}\n\n {/* bottom right: zoom */}\n <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]\">\n <Tooltip label=\"Zoom out\">\n <IconButton icon=\"minus\" label=\"Zoom out\" size=\"sm\" onClick={() => press(\"-\", \"Minus\", { cmd: true })} />\n </Tooltip>\n <Tooltip label=\"Reset to 100%\">\n <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%`}>\n {Math.round(ui.zoom * 100)}%\n </Button>\n </Tooltip>\n <Tooltip label=\"Zoom in\">\n <IconButton icon=\"plus\" label=\"Zoom in\" size=\"sm\" onClick={() => press(\"=\", \"Equal\", { cmd: true })} />\n </Tooltip>\n <Tooltip label=\"Fit to screen — ⇧1\">\n <IconButton icon=\"maximize\" label=\"Fit to screen\" size=\"sm\" onClick={() => handle?.zoomToFit()} />\n </Tooltip>\n </div>\n\n <Confirm\n open={clearing}\n onClose={() => setClearing(false)}\n onConfirm={() => {\n handle?.clear();\n setClearing(false);\n }}\n title=\"Clear the canvas?\"\n body=\"Everything drawn here is removed. Undo brings it back.\"\n confirmLabel=\"Clear\"\n tone=\"danger\"\n />\n </div>\n );\n}\n\n/** The tool rail: a vertical pill on the left; a bottom bar on a phone. */\nfunction Rail({ tools, stamps, tool, stamp, onTool, onStamp }) {\n return (\n <div\n role=\"toolbar\"\n aria-label=\"Tools\"\n className={cx(\n \"absolute z-20 flex gap-0.5 rounded-xl border border-border bg-popover p-1 shadow-lg\",\n \"bottom-3 left-1/2 max-w-[calc(100%-1.5rem)] -translate-x-1/2 flex-row overflow-x-auto\",\n \"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\",\n )}\n >\n {tools.map((t) => (\n <Tooltip key={t.id} label={t.key ? `${t.label} — ${t.key}` : t.label} side=\"right\">\n <Toggle variant=\"quiet\" size=\"sm\" icon={t.icon} label={t.label} pressed={!stamp && tool === t.tool} onPressedChange={() => onTool(t)} aria-keyshortcuts={t.key ?? undefined} />\n </Tooltip>\n ))}\n {stamps.length ? (\n <>\n <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\" />\n {stamps.map((s) => (\n <Tooltip key={s.id} label={`${s.label} — click the canvas to place`} side=\"right\">\n <Toggle variant=\"quiet\" size=\"sm\" icon={s.icon ?? \"plus\"} label={s.label} pressed={stamp?.id === s.id} onPressedChange={() => onStamp(s)} />\n </Tooltip>\n ))}\n </>\n ) : null}\n </div>\n );\n}\n\n/** The floating toolbar above a selection: colours from the theme, stroke,\n * text size, order, duplicate, delete. */\nfunction ContextBar({ box, tokens, hasShape, hasLine, hasText, hasStroke, roughness, width, onColour, onWidth, onRough, onFont, onLayer, onDuplicate, onDelete }) {\n const above = box.y > 64;\n const left = Math.max(8, Math.min(box.vw - 8, box.x + box.w / 2));\n const top = above ? box.y - 10 : Math.min(box.vh - 56, box.y + box.h + 10);\n return (\n <div\n role=\"toolbar\"\n aria-label=\"Selection\"\n className={cx(\n \"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\",\n \"-translate-x-1/2 transition-opacity duration-150 motion-reduce:transition-none\",\n above && \"-translate-y-full\",\n )}\n style={{ left, top }}\n >\n {hasStroke ? <Swatches label=\"Colour\" prop=\"strokeColor\" list={STROKES} value={tokens.strokeColor} ring onPick={onColour} /> : null}\n {hasShape ? <Swatches label=\"Fill\" prop=\"backgroundColor\" list={FILLS} value={tokens.backgroundColor} onPick={onColour} /> : null}\n {hasLine ? (\n <>\n <Separator orientation=\"vertical\" className=\"my-1\" />\n <Popover\n label=\"Stroke\"\n variant=\"quiet\"\n size=\"sm\"\n width=\"auto\"\n className=\"p-2\"\n trigger={\n <span className=\"inline-flex items-center gap-1\" aria-label=\"Stroke\">\n <Icon name=\"minus\" size={16} />\n </span>\n }\n >\n <div className=\"flex flex-col gap-2\">\n <div className=\"flex gap-1\" role=\"group\" aria-label=\"Stroke width\">\n {WIDTHS.map((w) => (\n <Toggle key={w.value} size=\"sm\" pressed={width === w.value} onPressedChange={() => onWidth(w.value)}>\n {w.label}\n </Toggle>\n ))}\n </div>\n <div className=\"flex gap-1\" role=\"group\" aria-label=\"Line style\">\n <Toggle size=\"sm\" pressed={roughness === 0} onPressedChange={() => onRough(0)}>\n Clean\n </Toggle>\n <Toggle size=\"sm\" pressed={roughness > 0} onPressedChange={() => onRough(1)}>\n Sketchy\n </Toggle>\n </div>\n </div>\n </Popover>\n </>\n ) : null}\n {hasText ? (\n <>\n <Separator orientation=\"vertical\" className=\"my-1\" />\n <Tooltip label=\"Smaller text\">\n <IconButton icon=\"minus\" label=\"Smaller text\" size=\"sm\" onClick={() => onFont(false)} />\n </Tooltip>\n <Icon name=\"type\" size={16} className=\"text-muted-foreground\" />\n <Tooltip label=\"Larger text\">\n <IconButton icon=\"plus\" label=\"Larger text\" size=\"sm\" onClick={() => onFont(true)} />\n </Tooltip>\n </>\n ) : null}\n <Separator orientation=\"vertical\" className=\"my-1\" />\n <Tooltip label=\"Bring to front\">\n <IconButton icon=\"bring-front\" label=\"Bring to front\" size=\"sm\" onClick={() => onLayer(true)} />\n </Tooltip>\n <Tooltip label=\"Send to back\">\n <IconButton icon=\"send-back\" label=\"Send to back\" size=\"sm\" onClick={() => onLayer(false)} />\n </Tooltip>\n <Tooltip label=\"Duplicate\">\n <IconButton icon=\"copy\" label=\"Duplicate\" size=\"sm\" onClick={onDuplicate} />\n </Tooltip>\n <Tooltip label=\"Delete\">\n <IconButton icon=\"trash\" label=\"Delete\" size=\"sm\" onClick={onDelete} />\n </Tooltip>\n </div>\n );\n}\n\nfunction Swatches({ label, prop, list, value, ring, onPick }) {\n const cur = list.find((s) => s.token === value);\n return (\n <Popover\n label={label}\n variant=\"quiet\"\n size=\"sm\"\n width=\"auto\"\n className=\"p-2\"\n trigger={\n <span className=\"inline-flex items-center\" aria-label={`${label}: ${cur?.label ?? \"custom\"}`}>\n <span className={cx(\"inline-block size-5 rounded-full border border-border\", ring && \"ring-2 ring-inset ring-background\", SWATCH[value] ?? \"bg-muted\")} />\n </span>\n }\n >\n <div className=\"grid grid-cols-6 gap-1\" role=\"group\" aria-label={label}>\n {list.map((s) => (\n <Tooltip key={s.token} label={s.label}>\n <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)}>\n <span className={cx(\"grid size-6 place-items-center rounded-full border border-border\", SWATCH[s.token])}>\n {s.token === \"none\" ? <Icon name=\"x\" size={14} className=\"text-muted-foreground\" /> : null}\n </span>\n </Button>\n </Tooltip>\n ))}\n </div>\n </Popover>\n );\n}\n\nfunction SaveState({ save, onRetry }) {\n if (save.state === \"idle\") return null;\n if (save.state === \"error\") {\n return (\n <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\">\n <Icon name=\"warning\" size={14} className=\"text-destructive\" />\n <span>{save.error || \"Couldn't save\"}</span>\n <Button variant=\"quiet\" size=\"sm\" onClick={onRetry}>\n Retry\n </Button>\n </div>\n );\n }\n return (\n <p role=\"status\" className=\"rounded-lg border border-border bg-popover px-2.5 py-1.5 text-xs text-muted-foreground shadow-sm\">\n {save.state === \"saving\" ? \"Saving…\" : \"Saved\"}\n </p>\n );\n}\n"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"path": "registry/blocks/sketch-canvas/excalidraw.js",
|
|
28
|
+
"type": "registry:component",
|
|
29
|
+
"target": "@components/makersclaw/blocks/sketch-canvas/excalidraw.js",
|
|
30
|
+
"content": "// The one door to Excalidraw (@excalidraw/excalidraw, MIT). Every file in\n// this block reaches it through here, and the blocks built on this one\n// reach it through SketchCanvas — never the package directly.\n//\n// The order of the imports below is the point: ssr-shim.js lays stand-ins\n// for the browser globals Excalidraw reads while its modules load (only\n// on the server, where they are missing), the package loads, and\n// ssr-shim-end.js takes them away again. Keep the three together and in\n// this order. Its stylesheet ships the same way doc-editor's does: an\n// import of the package's CSS, which the app's bundler picks up.\nimport \"@excalidraw/excalidraw/index.css\";\nimport \"./ssr-shim\";\nimport {\n CaptureUpdateAction,\n Excalidraw,\n FONT_FAMILY,\n convertToExcalidrawElements,\n exportToBlob,\n getCommonBounds,\n getSceneVersion,\n newElementWith,\n restoreElements,\n sceneCoordsToViewportCoords,\n viewportCoordsToSceneCoords,\n} from \"@excalidraw/excalidraw\";\nimport \"./ssr-shim-end\";\n\nexport {\n CaptureUpdateAction,\n Excalidraw,\n FONT_FAMILY,\n convertToExcalidrawElements,\n exportToBlob,\n getCommonBounds,\n getSceneVersion,\n newElementWith,\n restoreElements,\n sceneCoordsToViewportCoords,\n viewportCoordsToSceneCoords,\n};\n"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"path": "registry/blocks/sketch-canvas/ssr-shim.js",
|
|
34
|
+
"type": "registry:component",
|
|
35
|
+
"target": "@components/makersclaw/blocks/sketch-canvas/ssr-shim.js",
|
|
36
|
+
"content": "// Server only: a stand-in for the few browser globals Excalidraw's modules\n// read WHILE THEY LOAD. See excalidraw.js for why this exists.\n//\n// Excalidraw 0.18 runs browser code at module level (window.location,\n// devicePixelRatio, document.createElement(\"img\"), an Element.prototype\n// polyfill), so evaluating it on the server throws \"window is not defined\"\n// and the page fails to render — even though the component itself only\n// ever renders in the browser (the mount guard). The app is one static\n// bundle (no import(), no next/dynamic), so the module cannot be kept off\n// the server; instead it is let load against these stand-ins.\n//\n// The stand-ins live only for that load: excalidraw.js imports this file,\n// then @excalidraw/excalidraw, then ssr-shim-end.js, which removes them —\n// module evaluation is synchronous, so no other code runs in between. If\n// the load throws, the microtask below removes them anyway, so a global\n// `window` can never outlive it and confuse the rest of the server. In\n// the browser `window` exists and this file does nothing.\nconst G = globalThis;\nconst SET = [];\n\nfunction stand(name, value) {\n if (typeof G[name] !== \"undefined\") return;\n G[name] = value;\n SET.push(name);\n}\n\nif (typeof window === \"undefined\") {\n const element = () => ({ style: {}, dataset: {}, setAttribute() {}, appendChild() {}, getContext: () => ({}) });\n stand(\"window\", { location: { origin: \"\", href: \"\" }, devicePixelRatio: 1 });\n stand(\"document\", { createElement: element, documentElement: element(), head: element(), body: element() });\n stand(\"devicePixelRatio\", 1);\n stand(\"Element\", function Element() {});\n stand(\"navigator\", { platform: \"\", userAgent: \"\" });\n G.__sketchCanvasShim = SET;\n queueMicrotask(() => {\n if (G.__sketchCanvasShim === SET) removeShim();\n });\n}\n\nexport function removeShim() {\n const set = G.__sketchCanvasShim;\n if (!set) return;\n for (const name of set) delete G[name];\n delete G.__sketchCanvasShim;\n}\n"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"path": "registry/blocks/sketch-canvas/ssr-shim-end.js",
|
|
40
|
+
"type": "registry:component",
|
|
41
|
+
"target": "@components/makersclaw/blocks/sketch-canvas/ssr-shim-end.js",
|
|
42
|
+
"content": "// Server only: removes the stand-ins ssr-shim.js laid, right after\n// Excalidraw's modules have loaded (see ssr-shim.js). Does nothing in the\n// browser.\nimport { removeShim } from \"./ssr-shim\";\n\nremoveShim();\n"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"path": "registry/blocks/sketch-canvas/theme.js",
|
|
46
|
+
"type": "registry:component",
|
|
47
|
+
"target": "@components/makersclaw/blocks/sketch-canvas/theme.js",
|
|
48
|
+
"content": "// Excalidraw in the workspace theme, and trimmed — the one place its look\n// is mapped. Like doc-editor's theme.js for BlockNote.\n//\n// Excalidraw paints its chrome (menus, dialogs, the context menu, the\n// text editor) from CSS variables its stylesheet sets on `.excalidraw`\n// with its own violet and greys. Each one is pointed at a token here, as\n// Tailwind arbitrary properties aimed at that element from our wrapper.\n// Every class ends in `!`: Excalidraw's stylesheet is unlayered, and an\n// unlayered rule beats any Tailwind utility (those live in @layer\n// utilities) whatever its specificity; `!important` inside a layer is the\n// one thing that outranks it.\n//\n// Two of these carry more weight than the rest:\n// --theme-filter: none Excalidraw's dark mode draws the canvas light\n// and flips it with invert(93%) hue-rotate(180deg).\n// Our colours are already the dark scheme's (they\n// are resolved from the tokens in effect), so the\n// filter would invert them back. Off.\n// --color-selection read by Excalidraw's renderer for the selection\n// box; the component sets --sketch-selection to\n// the resolved primary (a canvas needs a concrete\n// colour, not a var()).\n//\n// Name clash: Excalidraw ALSO names its own variables --color-primary and\n// --color-muted, on `.excalidraw`, so var(--color-primary) read there is\n// its violet (and setting it to itself is a cycle). ROOT_VARS copies our\n// two onto the wrapper as --sketch-primary / --sketch-muted, and the\n// mapping reads those. The other tokens it uses have no Excalidraw twin.\n//\n// The TRIM list hides the parts of Excalidraw's UI the block replaces\n// or does not want: its tool bar (the rail is ours), its properties panel\n// (the context toolbar is ours, with the theme's palette), its main menu,\n// its library / share / collaboration corner, its zoom and undo footer\n// (ours), its mobile bars, and every sidebar (library, search). The hint\n// line under the old tool bar stays (\"Click and drag…\").\nexport const ROOT_VARS = \"[--sketch-primary:var(--color-primary)] [--sketch-muted:var(--color-muted)]\";\n\nexport const THEME = [\n // surfaces and text\n \"[&_.excalidraw]:[--theme-filter:none]!\",\n \"[&_.excalidraw]:[--color-selection:var(--sketch-selection)]!\",\n \"[&_.excalidraw]:[--ui-font:var(--theme-font-sans)]!\",\n \"[&_.excalidraw]:[--default-bg-color:var(--color-background)]!\",\n \"[&_.excalidraw]:[--island-bg-color:var(--color-popover)]!\",\n \"[&_.excalidraw]:[--popup-bg-color:var(--color-popover)]!\",\n \"[&_.excalidraw]:[--popup-secondary-bg-color:var(--sketch-muted)]!\",\n \"[&_.excalidraw]:[--popup-text-color:var(--color-popover-foreground)]!\",\n \"[&_.excalidraw]:[--popup-text-inverted-color:var(--color-background)]!\",\n \"[&_.excalidraw]:[--sidebar-bg-color:var(--color-popover)]!\",\n \"[&_.excalidraw]:[--sidebar-border-color:var(--color-border)]!\",\n \"[&_.excalidraw]:[--text-primary-color:var(--color-foreground)]!\",\n \"[&_.excalidraw]:[--icon-fill-color:var(--color-foreground)]!\",\n \"[&_.excalidraw]:[--keybinding-color:var(--color-muted-foreground)]!\",\n \"[&_.excalidraw]:[--color-on-surface:var(--color-foreground)]!\",\n \"[&_.excalidraw]:[--color-surface-lowest:var(--color-background)]!\",\n \"[&_.excalidraw]:[--color-surface-low:var(--sketch-muted)]!\",\n \"[&_.excalidraw]:[--color-surface-mid:var(--sketch-muted)]!\",\n \"[&_.excalidraw]:[--color-surface-high:var(--color-accent)]!\",\n \"[&_.excalidraw]:[--color-surface-primary-container:var(--color-accent)]!\",\n \"[&_.excalidraw]:[--color-on-primary-container:var(--color-accent-foreground)]!\",\n \"[&_.excalidraw]:[--color-muted:var(--sketch-muted)]!\",\n \"[&_.excalidraw]:[--color-muted-darker:var(--color-muted-foreground)]!\",\n \"[&_.excalidraw]:[--color-muted-darkest:var(--color-foreground)]!\",\n \"[&_.excalidraw]:[--color-disabled:var(--color-muted-foreground)]!\",\n // the brand violet → primary\n \"[&_.excalidraw]:[--color-primary:var(--sketch-primary)]!\",\n \"[&_.excalidraw]:[--color-primary-darker:var(--sketch-primary)]!\",\n \"[&_.excalidraw]:[--color-primary-darkest:var(--sketch-primary)]!\",\n \"[&_.excalidraw]:[--color-primary-hover:var(--sketch-primary)]!\",\n \"[&_.excalidraw]:[--color-primary-light:var(--color-accent)]!\",\n \"[&_.excalidraw]:[--color-primary-light-darker:var(--color-accent)]!\",\n \"[&_.excalidraw]:[--color-brand-hover:var(--sketch-primary)]!\",\n \"[&_.excalidraw]:[--color-brand-active:var(--sketch-primary)]!\",\n \"[&_.excalidraw]:[--color-promo:var(--sketch-primary)]!\",\n \"[&_.excalidraw]:[--link-color:var(--sketch-primary)]!\",\n \"[&_.excalidraw]:[--select-highlight-color:var(--sketch-primary)]!\",\n \"[&_.excalidraw]:[--focus-highlight-color:var(--color-ring)]!\",\n // lines, inputs, buttons\n \"[&_.excalidraw]:[--default-border-color:var(--color-border)]!\",\n \"[&_.excalidraw]:[--dialog-border-color:var(--color-border)]!\",\n \"[&_.excalidraw]:[--color-border-outline:var(--color-border)]!\",\n \"[&_.excalidraw]:[--color-border-outline-variant:var(--color-border)]!\",\n \"[&_.excalidraw]:[--input-bg-color:var(--color-background)]!\",\n \"[&_.excalidraw]:[--input-border-color:var(--color-input)]!\",\n \"[&_.excalidraw]:[--input-hover-bg-color:var(--color-accent)]!\",\n \"[&_.excalidraw]:[--input-label-color:var(--color-muted-foreground)]!\",\n \"[&_.excalidraw]:[--button-hover-bg:var(--color-accent)]!\",\n \"[&_.excalidraw]:[--button-active-bg:var(--color-accent)]!\",\n \"[&_.excalidraw]:[--button-active-border:var(--sketch-primary)]!\",\n \"[&_.excalidraw]:[--scrollbar-thumb:var(--color-border)]!\",\n \"[&_.excalidraw]:[--scrollbar-thumb-hover:var(--color-muted-foreground)]!\",\n // status\n \"[&_.excalidraw]:[--color-danger:var(--color-destructive)]!\",\n \"[&_.excalidraw]:[--color-danger-darker:var(--color-destructive)]!\",\n \"[&_.excalidraw]:[--color-danger-text:var(--color-destructive)]!\",\n \"[&_.excalidraw]:[--button-destructive-color:var(--color-destructive)]!\",\n \"[&_.excalidraw]:[--button-destructive-bg-color:var(--sketch-muted)]!\",\n // radius\n \"[&_.excalidraw]:[--border-radius-md:var(--theme-radius)]!\",\n \"[&_.excalidraw]:[--border-radius-lg:var(--theme-radius)]!\",\n].join(\" \");\n\n// Plain CSS, not Tailwind classes: Tailwind reads every `_` inside an\n// arbitrary variant as a space, and Excalidraw's class names are full of\n// them (layer-ui__wrapper__top-right, App-menu_top__left), so the class\n// form silently matched nothing — its library button, zoom footer and a\n// squeezed hint line all showed (seen in the pane, 2026-09-24). Rendered\n// in a <style> by the block, scoped to its own wrapper.\nconst HIDE = [\n // desktop: main menu + properties panel, the tool bar's own buttons, the\n // library / share corner, the zoom + undo + help footer\n \".App-menu_top__left\",\n \".App-toolbar > .Stack\",\n \".App-toolbar__extra-tools-trigger\",\n \".layer-ui__wrapper__top-right\",\n \".layer-ui__wrapper__footer-left\",\n \".layer-ui__wrapper__footer-right\",\n // its hint line: the tool bar it sat under is gone (our empty state and\n // rail tooltips say the same)\n \".HintViewer\",\n // phone layout: its top and bottom bars\n \".App-top-bar\",\n \".App-bottom-bar\",\n // library, search and every other sidebar\n \".sidebar\",\n \".default-sidebar\",\n];\nexport const TRIM_CSS =\n HIDE.map((sel) => `[data-sketch-canvas] ${sel}`).join(\",\\n\") +\n \" { display: none !important; }\\n\" +\n \"[data-sketch-canvas] .App-toolbar { background: transparent !important; box-shadow: none !important; border: 0 !important; }\";\n"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"path": "registry/blocks/sketch-canvas/palette.js",
|
|
52
|
+
"type": "registry:component",
|
|
53
|
+
"target": "@components/makersclaw/blocks/sketch-canvas/palette.js",
|
|
54
|
+
"content": "// The sketch's colours are the workspace's tokens.\n//\n// Excalidraw draws with concrete colour strings, and a saved sketch holds\n// them. So a colour is picked as a TOKEN (\"primary\", \"chart-2:soft\"),\n// resolved to a concrete colour at run time from the CSS variable the\n// workspace theme sets (--color-primary…), and the token is kept beside\n// it on the element (customData.tokens.strokeColor = \"primary\"). When the\n// theme or the light/dark scheme changes, every tokened colour is resolved\n// again and the sketch repaints; a colour with no token (pasted from\n// elsewhere) is left as it is.\n//\n// A token is a slot name, \"<slot>:soft\" (the slot mixed into the page\n// background — a sticky-note tint), or \"none\" (no fill). No colour value\n// is written in this file: every one is read from the page.\n//\n// Everything but resolvePalette() is a plain function of plain objects,\n// so it runs in node and on the server.\n\n/** The props of an element that hold a colour. */\nexport const COLOR_PROPS = [\"strokeColor\", \"backgroundColor\"];\n\n/** Ink: strokes, lines, text. */\nexport const STROKES = [\n { token: \"foreground\", label: \"Ink\" },\n { token: \"muted-foreground\", label: \"Grey\" },\n { token: \"primary\", label: \"Primary\" },\n { token: \"destructive\", label: \"Red\" },\n { token: \"warning\", label: \"Amber\" },\n { token: \"success\", label: \"Green\" },\n { token: \"info\", label: \"Blue\" },\n { token: \"chart-2\", label: \"Accent 2\" },\n { token: \"chart-3\", label: \"Accent 3\" },\n { token: \"chart-4\", label: \"Accent 4\" },\n { token: \"chart-5\", label: \"Accent 5\" },\n];\n\n/** Fills: none, a quiet grey, soft tints of the hues, and solid primary. */\nexport const FILLS = [\n { token: \"none\", label: \"No fill\" },\n { token: \"muted\", label: \"Grey\" },\n { token: \"primary:soft\", label: \"Primary, soft\" },\n { token: \"destructive:soft\", label: \"Red, soft\" },\n { token: \"warning:soft\", label: \"Amber, soft\" },\n { token: \"success:soft\", label: \"Green, soft\" },\n { token: \"info:soft\", label: \"Blue, soft\" },\n { token: \"chart-2:soft\", label: \"Accent 2, soft\" },\n { token: \"chart-3:soft\", label: \"Accent 3, soft\" },\n { token: \"chart-4:soft\", label: \"Accent 4, soft\" },\n { token: \"chart-5:soft\", label: \"Accent 5, soft\" },\n { token: \"primary\", label: \"Primary\" },\n];\n\n/** How much of the hue a soft tint keeps (the rest is the page background). */\nexport const SOFT_MIX = 0.26;\n\n/** The slots read from the page: every token's slot, plus the background. */\nexport const SLOTS = [...new Set([\"background\", \"foreground\", ...STROKES.map((s) => s.token), ...FILLS.map((f) => f.token.split(\":\")[0]).filter((s) => s !== \"none\")])];\n\nconst HEXISH = /^#?([0-9a-f]{6})([0-9a-f]{2})?$/i;\n\n/** \"#rrggbb\" or \"#rrggbbaa\" → [r, g, b, a(0-255)], or null. */\nexport function parseHex(value) {\n const m = HEXISH.exec(String(value ?? \"\").trim());\n if (!m) return null;\n const n = parseInt(m[1], 16);\n return [(n >> 16) & 255, (n >> 8) & 255, n & 255, m[2] ? parseInt(m[2], 16) : 255];\n}\n\nconst byte = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, \"0\");\n\n/** [r, g, b, a] → \"#rrggbb\" (or \"#rrggbbaa\" when not opaque). */\nexport function toHex([r, g, b, a = 255]) {\n return \"#\" + byte(r) + byte(g) + byte(b) + (a >= 255 ? \"\" : byte(a));\n}\n\n/** `t` of colour a over (1 - t) of colour b, both hex; opaque result. */\nexport function mixHex(a, b, t) {\n const x = parseHex(a);\n const y = parseHex(b);\n if (!x || !y) return a ?? b ?? null;\n const k = Math.max(0, Math.min(1, t));\n return toHex([0, 1, 2].map((i) => x[i] * k + y[i] * (1 - k)));\n}\n\n/** slot → hex (as read from the page) → token → concrete colour. */\nexport function buildPalette(slots) {\n const out = { none: \"transparent\" };\n for (const [slot, hex] of Object.entries(slots ?? {})) if (hex) out[slot] = hex;\n for (const f of FILLS) {\n const [slot, soft] = f.token.split(\":\");\n if (soft && out[slot] && out.background) out[f.token] = mixHex(out[slot], out.background, SOFT_MIX);\n }\n return out;\n}\n\n/** Is this string a token the palette knows? */\nexport const isToken = (palette, value) => typeof value === \"string\" && Object.hasOwn(palette ?? {}, value);\n\n/** The token a concrete colour came from, by value; null when none matches.\n * A plain slot wins over a soft tint that happens to match. */\nexport function tokenFor(palette, colour) {\n if (typeof colour !== \"string\") return null;\n const c = colour.trim().toLowerCase();\n if (c === \"transparent\") return \"none\";\n let soft = null;\n for (const [token, value] of Object.entries(palette ?? {})) {\n if (String(value).toLowerCase() !== c) continue;\n if (!token.includes(\":\")) return token;\n soft ??= token;\n }\n return soft;\n}\n\n/** The tokens stored on an element: { strokeColor?: \"primary\", … }. */\nexport const tokensOf = (el) => (el?.customData && typeof el.customData.tokens === \"object\" && el.customData.tokens) || {};\n\n/** The token each colour prop of `el` stands for: the stored one, else a\n * match against `palette`. Only props with a token are listed. */\nexport function elementTokens(el, palette) {\n const stored = tokensOf(el);\n const out = {};\n for (const prop of COLOR_PROPS) {\n const t = isToken(palette, stored[prop]) ? stored[prop] : tokenFor(palette, el?.[prop]);\n if (t) out[prop] = t;\n }\n return out;\n}\n\nconst sameTokens = (a, b) => COLOR_PROPS.every((p) => (a?.[p] ?? null) === (b?.[p] ?? null));\n\n/** Elements with their tokens written into customData — what a saved\n * sketch holds. An element that already says the same is returned as is. */\nexport function tagTokens(elements, palette) {\n return (elements ?? []).map((el) => {\n const tokens = elementTokens(el, palette);\n if (sameTokens(tokens, tokensOf(el))) return el;\n const { tokens: _old, ...rest } = el.customData ?? {};\n const customData = Object.keys(tokens).length ? { ...rest, tokens } : Object.keys(rest).length ? rest : undefined;\n return { ...el, customData };\n });\n}\n\n/** What changes when the palette changes: for each element whose tokened\n * colours resolve to something new, { id, patch } with the new colours\n * and the tokens. Tokens come from customData, else from matching the\n * colour against `previous` (the palette it was drawn with). */\nexport function repaint(elements, palette, previous) {\n const out = [];\n for (const el of elements ?? []) {\n const stored = tokensOf(el);\n const patch = {};\n const tokens = {};\n for (const prop of COLOR_PROPS) {\n const token = isToken(palette, stored[prop]) ? stored[prop] : previous ? tokenFor(previous, el[prop]) : null;\n if (!token || !isToken(palette, token)) continue;\n tokens[prop] = token;\n if (palette[token] !== el[prop]) patch[prop] = palette[token];\n }\n if (Object.keys(patch).length) out.push({ id: el.id, patch: { ...patch, customData: { ...(el.customData ?? {}), tokens: { ...stored, ...tokens } } } });\n }\n return out;\n}\n\n/** Resolve a skeleton's (or an element's) token-named colours to concrete\n * ones, keeping the tokens in customData. Other values pass through. */\nexport function resolveColours(obj, palette) {\n const tokens = { ...tokensOf(obj) };\n const out = { ...obj };\n for (const prop of COLOR_PROPS) {\n if (isToken(palette, obj[prop])) {\n tokens[prop] = obj[prop];\n out[prop] = palette[obj[prop]];\n }\n }\n if (obj.label && isToken(palette, obj.label.strokeColor)) out.label = { ...obj.label, strokeColor: palette[obj.label.strokeColor] };\n if (Object.keys(tokens).length) out.customData = { ...(obj.customData ?? {}), tokens };\n return out;\n}\n\n/** A stable fingerprint of a palette: equal strings, nothing to repaint. */\nexport const paletteKey = (palette) =>\n Object.keys(palette ?? {})\n .sort()\n .map((k) => `${k}=${palette[k]}`)\n .join(\";\");\n\n// ---- the browser half -------------------------------------------------------\n\n/** Read every slot from the CSS variables in effect at `root` (so a\n * data-theme or a theme stylesheet scoped to an ancestor counts) and\n * build the palette. A probe element takes `color: var(--color-<slot>)`;\n * the browser resolves var(), light-dark(), color-mix() and relative\n * colours into its computed colour, and a 1×1 canvas turns that into\n * bytes whatever syntax it came back in. */\nexport function resolvePalette(root) {\n if (typeof document === \"undefined\" || !root) return buildPalette({});\n const probe = document.createElement(\"span\");\n probe.setAttribute(\"aria-hidden\", \"true\");\n probe.style.position = \"absolute\";\n probe.style.visibility = \"hidden\";\n probe.style.pointerEvents = \"none\";\n root.appendChild(probe);\n const canvas = document.createElement(\"canvas\");\n canvas.width = 1;\n canvas.height = 1;\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n const slots = {};\n try {\n for (const slot of SLOTS) {\n probe.style.color = \"\";\n probe.style.color = `var(--color-${slot})`;\n const computed = getComputedStyle(probe).color;\n if (!computed || !ctx) continue;\n ctx.clearRect(0, 0, 1, 1);\n ctx.fillStyle = \"transparent\";\n ctx.fillStyle = computed;\n ctx.fillRect(0, 0, 1, 1);\n const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;\n if (a === 0) continue;\n slots[slot] = toHex([r, g, b, a]);\n }\n } finally {\n probe.remove();\n }\n return buildPalette(slots);\n}\n"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"path": "registry/blocks/sketch-canvas/sketch.js",
|
|
58
|
+
"type": "registry:component",
|
|
59
|
+
"target": "@components/makersclaw/blocks/sketch-canvas/sketch.js",
|
|
60
|
+
"content": "// The sketch as data: what SketchCanvas takes and gives back, and the\n// helpers that read it. Plain functions of plain JSON — they never import\n// Excalidraw, so they run anywhere, the server included.\n//\n// A sketch is\n// { type: \"sketch\", version: 1, elements: [...], files: {...}, appState: {...} }\n// where `elements` are Excalidraw's own element objects (non-deleted),\n// `files` holds the images they show (id → { id, mimeType, dataURL,\n// created }), and `appState` keeps only what a sketch should remember\n// ({ gridModeEnabled }). The locked background image (the `background`\n// prop) is never in it: the block lays that from the prop.\n\nexport const SKETCH_VERSION = 1;\n\n/** Excalidraw's system-font family (\"Helvetica\" in its list): drawn with\n * the device's own sans (Helvetica, Arial…), never fetched. Every text the\n * block makes uses it; see README \"Fonts\". */\nexport const SYSTEM_FONT = 2;\n\n/** The role the background image carries in customData. */\nexport const BACKGROUND_ROLE = \"background\";\n\nexport function emptySketch() {\n return { type: \"sketch\", version: SKETCH_VERSION, elements: [], files: {}, appState: {} };\n}\n\n/** Anything a caller may hand us → a sketch: a sketch, a bare element\n * array, Excalidraw's own { elements, files, appState } export, or\n * nothing. */\nexport function toSketch(value) {\n if (!value) return emptySketch();\n if (Array.isArray(value)) return { ...emptySketch(), elements: value };\n if (typeof value !== \"object\") return emptySketch();\n return {\n type: \"sketch\",\n version: SKETCH_VERSION,\n elements: Array.isArray(value.elements) ? value.elements : [],\n files: value.files && typeof value.files === \"object\" ? value.files : {},\n appState: value.appState && typeof value.appState === \"object\" ? pickAppState(value.appState) : {},\n };\n}\n\n/** The part of Excalidraw's appState a sketch keeps. */\nexport function pickAppState(appState) {\n const out = {};\n if (typeof appState?.gridModeEnabled === \"boolean\") out.gridModeEnabled = appState.gridModeEnabled;\n return out;\n}\n\nexport const isBackground = (el) => el?.customData?.role === BACKGROUND_ROLE;\n\n/** The elements a reader cares about: not deleted, not the background. */\nexport function liveElements(sketch) {\n return toSketch(sketch).elements.filter((el) => el && !el.isDeleted && !isBackground(el));\n}\n\n/** Nothing drawn (the background alone counts as empty). */\nexport function isSketchEmpty(sketch) {\n return liveElements(sketch).length === 0;\n}\n\n/** The files the given elements show, and no others. */\nexport function filesFor(elements, files) {\n const out = {};\n for (const el of elements ?? []) {\n if (el?.type === \"image\" && el.fileId && files?.[el.fileId]) out[el.fileId] = files[el.fileId];\n }\n return out;\n}\n\nconst centre = (b) => ({ x: b.x + b.width / 2, y: b.y + b.height / 2 });\nconst union = (boxes) => {\n const x1 = Math.min(...boxes.map((b) => b.x));\n const y1 = Math.min(...boxes.map((b) => b.y));\n const x2 = Math.max(...boxes.map((b) => b.x + b.width));\n const y2 = Math.max(...boxes.map((b) => b.y + b.height));\n return { x: x1, y: y1, width: x2 - x1, height: y2 - y1 };\n};\nconst boxOf = (el) => ({ x: Math.min(el.x, el.x + el.width), y: Math.min(el.y, el.y + el.height), width: Math.abs(el.width), height: Math.abs(el.height) });\nconst textOf = (el) => String(el.originalText ?? el.text ?? \"\").replace(/\\s+/g, \" \").trim();\n\n/** Reading order: rows top to bottom (items whose tops are within `band`\n * px share a row), then left to right. */\nexport function readingOrder(items, band = 16) {\n const sorted = [...items].sort((a, b) => a.y - b.y || a.x - b.x);\n const rows = [];\n for (const it of sorted) {\n const row = rows.at(-1);\n if (row && Math.abs(it.y - row[0].y) <= band) row.push(it);\n else rows.push([it]);\n }\n return rows.flatMap((row) => row.sort((a, b) => a.x - b.x));\n}\n\n/**\n * The sketch as a flat list of what it says, in reading order:\n * { kind: \"frame\", id, name, x, y, width, height }\n * { kind: \"stamp\", id, stamp: \"button\", text, frame, x, y, width, height }\n * { kind: \"text\", id, text, container: \"rectangle\" | null, frame, x, y, width, height }\n * { kind: \"image\", id, frame, x, y, width, height }\n * A stamp is one item however many elements it is made of; its text is\n * the words inside it. A text bound to a shape takes the shape's box.\n * Shapes and lines with no words are left out; `frame` is the id of the\n * frame the item sits in (or null). Frames come first, then the rest.\n */\nexport function sketchItems(sketch) {\n const els = liveElements(sketch);\n const byId = new Map(els.map((el) => [el.id, el]));\n const frameOf = (el) => el.frameId ?? (el.containerId ? byId.get(el.containerId)?.frameId : null) ?? null;\n const stamps = new Map();\n const loose = [];\n for (const el of els) {\n const cd = el.customData ?? {};\n const container = el.containerId ? byId.get(el.containerId) : null;\n const stampId = cd.stampId ?? container?.customData?.stampId;\n if (stampId) {\n const s = stamps.get(stampId) ?? { id: stampId, stamp: cd.stamp ?? container?.customData?.stamp ?? \"stamp\", els: [] };\n s.els.push(el);\n stamps.set(stampId, s);\n continue;\n }\n if (el.type === \"frame\" || el.type === \"magicframe\") {\n loose.push({ kind: \"frame\", id: el.id, name: String(el.name ?? \"\").trim() || \"Frame\", ...boxOf(el) });\n } else if (el.type === \"text\") {\n const t = textOf(el);\n if (!t) continue;\n const box = container ? boxOf(container) : boxOf(el);\n loose.push({ kind: \"text\", id: el.id, text: t, container: container?.type ?? null, frame: frameOf(el), ...box });\n } else if (el.type === \"image\") {\n loose.push({ kind: \"image\", id: el.id, frame: frameOf(el), ...boxOf(el) });\n }\n }\n const stampItems = [...stamps.values()].map((s) => {\n const box = union(s.els.map(boxOf));\n const words = readingOrder(s.els.filter((e) => e.type === \"text\").map((e) => ({ ...boxOf(e), t: textOf(e) })))\n .map((w) => w.t)\n .filter(Boolean);\n return { kind: \"stamp\", id: s.id, stamp: s.stamp, text: words.join(\" \"), frame: s.els.map(frameOf).find(Boolean) ?? null, ...box };\n });\n const frames = readingOrder(loose.filter((i) => i.kind === \"frame\"));\n const rest = readingOrder([...loose.filter((i) => i.kind !== \"frame\"), ...stampItems]);\n // An item inside a frame's box with no frameId (dropped in before the\n // frame was drawn round it) still belongs to it.\n for (const it of rest) {\n if (it.frame) continue;\n const c = centre(it);\n const f = frames.find((fr) => c.x >= fr.x && c.x <= fr.x + fr.width && c.y >= fr.y && c.y <= fr.y + fr.height);\n it.frame = f ? f.id : null;\n }\n return [...frames, ...rest];\n}\n\n/**\n * The sketch's words as plain text, for an agent or a screen reader: one\n * line per item in reading order, grouped under \"# <frame name>\" for each\n * frame; a stamp reads \"[button] Sign up\", an image \"[image]\". Items\n * outside every frame come last (under \"# Outside frames\" when there are\n * frames). An empty sketch gives \"\".\n *\n * stamps true (default): name the stamp kind in brackets\n */\nexport function sketchToText(sketch, { stamps = true } = {}) {\n const items = sketchItems(sketch);\n const frames = items.filter((i) => i.kind === \"frame\");\n const rest = items.filter((i) => i.kind !== \"frame\");\n const line = (it) => {\n if (it.kind === \"stamp\") return stamps ? `[${it.stamp}]${it.text ? ` ${it.text}` : \"\"}` : it.text;\n if (it.kind === \"image\") return stamps ? \"[image]\" : \"\";\n return it.text;\n };\n const lines = [];\n for (const f of frames) {\n const inside = rest.filter((i) => i.frame === f.id).map(line).filter(Boolean);\n lines.push(`# ${f.name}`, ...inside, \"\");\n }\n const outside = rest.filter((i) => !i.frame || !frames.some((f) => f.id === i.frame)).map(line).filter(Boolean);\n if (outside.length) lines.push(...(frames.length ? [\"# Outside frames\"] : []), ...outside);\n return lines.join(\"\\n\").trim();\n}\n"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"path": "registry/blocks/sketch-canvas/stamps.js",
|
|
64
|
+
"type": "registry:component",
|
|
65
|
+
"target": "@components/makersclaw/blocks/sketch-canvas/stamps.js",
|
|
66
|
+
"content": "// Stamps: tools that place a small ready-made drawing with one click. A\n// stamp is { id, label, icon, create() }, where create() returns Excalidraw\n// element skeletons (the input of its convertToExcalidrawElements) laid\n// out from (0, 0). Colours are TOKEN names (\"foreground\", \"primary:soft\"),\n// resolved when the stamp lands; text is the system font. SketchCanvas\n// groups what a stamp places, so it moves as one, and marks every element\n// with customData { stamp: id, stampId } — sketchToText reads a stamp as\n// \"[button] Sign up\".\n//\n// WIREFRAME_STAMPS are the UI pieces a screen sketch is made of: button,\n// input, card, nav bar, table and image placeholder.\nimport { SYSTEM_FONT } from \"./sketch\";\n\nconst T = (text, x, y, more = {}) => ({ type: \"text\", x, y, text, fontSize: 16, fontFamily: SYSTEM_FONT, strokeColor: \"foreground\", ...more });\nconst rect = (x, y, width, height, more = {}) => ({\n type: \"rectangle\",\n x,\n y,\n width,\n height,\n strokeColor: \"foreground\",\n backgroundColor: \"none\",\n fillStyle: \"solid\",\n roundness: { type: 3 },\n ...more,\n});\nconst line = (x, y, dx, dy, more = {}) => ({\n type: \"line\",\n x,\n y,\n width: Math.abs(dx),\n height: Math.abs(dy),\n points: [\n [0, 0],\n [dx, dy],\n ],\n strokeColor: \"muted-foreground\",\n ...more,\n});\nconst label = (text, more = {}) => ({ text, fontSize: 16, fontFamily: SYSTEM_FONT, ...more });\n\nexport const WIREFRAME_STAMPS = [\n {\n id: \"button\",\n label: \"Button\",\n icon: \"zap\",\n create: () => [rect(0, 0, 132, 44, { backgroundColor: \"primary:soft\", label: label(\"Button\") })],\n },\n {\n id: \"input\",\n label: \"Input\",\n icon: \"type\",\n create: () => [\n T(\"Label\", 0, 0, { fontSize: 14, strokeColor: \"muted-foreground\" }),\n rect(0, 24, 260, 44, { strokeColor: \"muted-foreground\", label: label(\"Placeholder\", { textAlign: \"left\", strokeColor: \"muted-foreground\" }) }),\n ],\n },\n {\n id: \"card\",\n label: \"Card\",\n icon: \"file\",\n create: () => [\n rect(0, 0, 280, 190),\n T(\"Card title\", 20, 18, { fontSize: 20 }),\n line(20, 64, 220, 0),\n line(20, 90, 170, 0),\n rect(20, 128, 104, 40, { backgroundColor: \"primary:soft\", label: label(\"Action\") }),\n ],\n },\n {\n id: \"nav\",\n label: \"Nav bar\",\n icon: \"menu\",\n create: () => [\n rect(0, 0, 680, 60, { backgroundColor: \"muted\", roundness: null }),\n T(\"Logo\", 24, 17, { fontSize: 20 }),\n T(\"Home\", 400, 20),\n T(\"Pricing\", 478, 20),\n T(\"About\", 568, 20),\n ],\n },\n {\n id: \"table\",\n label: \"Table\",\n icon: \"grid\",\n create: () => [\n rect(0, 0, 450, 176, { roundness: null }),\n rect(0, 0, 450, 44, { backgroundColor: \"muted\", roundness: null }),\n T(\"Name\", 14, 12),\n T(\"Status\", 164, 12),\n T(\"Amount\", 314, 12),\n line(0, 88, 450, 0),\n line(0, 132, 450, 0),\n line(150, 0, 0, 176),\n line(300, 0, 0, 176),\n ],\n },\n {\n id: \"image\",\n label: \"Image\",\n icon: \"image\",\n create: () => [rect(0, 0, 240, 160, { strokeColor: \"muted-foreground\", roundness: null }), line(0, 0, 240, 160), line(0, 160, 240, -160)],\n },\n];\n"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"path": "registry/blocks/sketch-canvas/README.md",
|
|
70
|
+
"type": "registry:file",
|
|
71
|
+
"target": "@components/makersclaw/blocks/sketch-canvas/README.md",
|
|
72
|
+
"content": "# SketchCanvas\n\nA freeform drawing canvas in the workspace theme, friendly like Miro. A\ntool rail on the left (a bar along the bottom on a phone): select, pan,\ndraw, rectangle, ellipse, diamond, arrow, line, text, image, eraser, frame\n— each with its one-key shortcut. Select something and a small toolbar\nfloats above it: colour and fill from the theme's palette, stroke, text\nsize, bring to front / send to back, duplicate, delete. Undo and redo sit\nbottom left, zoom and fit-to-screen bottom right, a menu top right\n(download PNG, snap to grid, shortcuts, clear). An empty canvas says how\nto start. Stamp tools place ready-made drawings with one click — the six\nUI wireframe pieces come built in.\n\nIt is [Excalidraw](https://github.com/excalidraw/excalidraw) (MIT)\nunderneath: its canvas, selection, text editing, history and shortcuts;\nour rail, toolbars, palette and empty state, in the kit. Excalidraw's own\ntool bar, properties panel, main menu, library, share / collaboration\nbuttons, welcome screen and sidebars are hidden. The blocks built on this\none — `whiteboard`, `annotate`, `sketch-to-app` — import it, never\n`@excalidraw/excalidraw`.\n\n```jsx\nimport { SketchCanvas, sketchToText } from \"../components/blocks/sketch-canvas\"; // a Makersclaw app (\"./blocks/sketch-canvas\" from components/)\nimport { SketchCanvas, sketchToText } from \"@/components/makersclaw/blocks/sketch-canvas\"; // a shadcn project\n```\n\n`SketchCanvas` is a client component: render it from a `\"use client\"`\ncomponent. The helpers (`sketchToText`, `sketchItems`, `emptySketch`,\n`isSketchEmpty`, `toSketch`) are plain functions and work on the server.\n\n`makersclaw add sketch-canvas` adds `@excalidraw/excalidraw` (pinned,\nexact) to package.json: run `npm install` after. It loads with the page\n(no `import()`); the canvas mounts after the first render, and until then\n— on the server — the frame shows \"Loading the canvas…\", with the\nsketch's words for a screen reader.\n\n## The data\n\nA sketch is plain JSON. Store it as it comes out of `onChange` / `onSave`\n(a `jsonb` column).\n\n```js\n{\n type: \"sketch\",\n version: 1,\n elements: [ /* Excalidraw elements, non-deleted, in paint order (first = back) */ ],\n files: { \"<fileId>\": { id, mimeType: \"image/png\", dataURL: \"data:image/png;base64,…\", created } },\n appState: { gridModeEnabled: false },\n}\n```\n\n- `elements` are Excalidraw's own element objects: `type` is `rectangle`,\n `ellipse`, `diamond`, `arrow`, `line`, `freedraw`, `text`, `image` or\n `frame`; each has `id`, `x`, `y`, `width`, `height`, `strokeColor`,\n `backgroundColor`, `strokeWidth`, `roughness`, `groupIds`, `frameId`…\n A `text` has `text`, `fontSize`, and `containerId` when it is the label\n of a shape; a shape lists its label and arrows in `boundElements`.\n- **Colours are tokens.** Every colour picked in the canvas is a theme\n token, and the element keeps it in `customData.tokens`:\n `{ strokeColor: \"primary\", backgroundColor: \"chart-2:soft\" }`. The\n concrete colour beside it (`strokeColor`) is what the token resolved to\n when it was saved; on load, and whenever the theme or light/dark\n changes, the canvas resolves every token again and repaints. A colour\n with no token (pasted from elsewhere) stays as it is.\n Tokens: `foreground`, `muted-foreground`, `primary`, `destructive`,\n `warning`, `success`, `info`, `chart-2` … `chart-5` (ink), and for\n fills also `none`, `muted` and `<slot>:soft` (the slot mixed into the\n page background — a sticky-note tint). `STROKES` and `FILLS` list them\n with labels.\n- **Stamps** mark what they place: every element of one stamp shares a\n group and carries `customData: { stamp: \"button\", stampId }`.\n- `files` holds the images the elements show (only those). Images are\n data URLs inside the JSON — keep them small, or store the file\n elsewhere and keep the sketch light.\n- The `background` image is never in the value; it comes from the prop.\n- Written by hand (a starter template, a seed), an element may leave out\n what Excalidraw can default (`version`, `seed`, `strokeWidth`,\n `roughness`…) and give its colours only as tokens:\n `{ id: \"a\", type: \"rectangle\", x: 0, y: 0, width: 160, height: 72, customData: { tokens: { strokeColor: \"foreground\", backgroundColor: \"chart-2:soft\" } } }`.\n A text needs `text`, `fontSize` and a rough `width`/`height` (it is\n re-measured). Or skip hand-written elements: insert skeletons through\n the handle (below).\n- `sketchToText(sketch)` reads the words out for an agent; see Helpers.\n\n## Props\n\n- `value` — the sketch, controlled. A new `value` that differs from what\n the canvas last sent replaces the drawing (loading another record) and\n clears undo. Pass back exactly what `onChange` gave you and nothing\n resets. Also takes a bare element array or Excalidraw's own export\n (`{ elements, files, appState }`).\n- `defaultValue` — the starting sketch, uncontrolled. With neither, it\n starts empty.\n- `onChange(sketch)` — the whole sketch, after an edit settles\n (debounced by `changeDelay`). Not called for pan, zoom or selection.\n- `onSave(sketch)` — `async`, returns `{ ok: true }` or\n `{ ok: false, error? }`. Called with the same sketch, right after\n `onChange`, and on Cmd/Ctrl+S. The canvas shows \"Saving…\", \"Saved\",\n or the error with a Retry. A throw counts as a failure.\n- `changeDelay` — ms of quiet before `onChange` / `onSave` (default 600).\n- `readOnly` — `false` (default). `true` shows the sketch to look at: pan\n and zoom, no rail, no toolbar, no edits.\n- `background` — `{ src, width?, height? }`: an image laid at the back,\n locked (it cannot be selected, moved, erased or deleted), top-left at\n (0, 0). For marking up a screenshot (annotate). `src` is a data URL, or\n a same-origin URL (a cross-origin one draws, but spoils `exportImage`).\n Without `width`/`height` the image's own size is used. When it first\n lands on an empty sketch, the view fits it.\n- `tools` — ids of the built-in tools to offer, in rail order:\n `[\"select\", \"hand\", \"draw\", \"rectangle\", \"ellipse\", \"diamond\", \"arrow\",\n \"line\", \"text\", \"image\", \"eraser\", \"frame\"]` (all, the default). A tool\n left out is also unreachable by its shortcut. `SKETCH_TOOLS` lists them\n with labels, icons and keys.\n- `stamps` — stamp tools after the built-ins:\n `[{ id, label, icon, create() }]`. `create()` returns element\n skeletons (below) laid out from (0, 0); a click on the canvas places\n them centred on the pointer (Enter: the middle of the view; Esc\n cancels), grouped, selected. `WIREFRAME_STAMPS` are button, input, card,\n nav bar, table and image placeholder.\n- `grid` — `true` (default): a dotted background that pans and zooms with\n the drawing. `false`: plain.\n- `empty` — what an empty, editable canvas shows in its middle. Omit for\n the built-in hint (\"Start sketching…\"); `false` for nothing; or a node\n (starter templates: give its buttons `pointer-events-auto`, the layer\n lets clicks through to the canvas). Shown while nothing is drawn and the\n select or pan tool is active.\n- `actions` — nodes in the top-right corner, before the menu (a Share\n button, a title chip).\n- `canvasRef` — a ref (`useSketchCanvas()`) that receives the handle.\n- `onReady(handle)` — once, when the canvas has mounted.\n- `label` — the accessible name of the canvas region (default \"Sketch\").\n- `className` — layout: width, margin, height. The default height is\n `h-[32rem]`; a height class of yours (`h-dvh`, `h-full`, `min-h-…`,\n `flex-1`) replaces it. Never colour.\n\n### The handle\n\n`canvasRef.current` (and `onReady`'s argument):\n\n- `getScene({ withBackground? })` — the sketch now (what `onChange`\n sends), without waiting for the debounce.\n- `setScene(sketch)` — replace the drawing (undoable).\n- `insertElements(skeletons, opts?)` → the new element ids. Skeletons are\n Excalidraw's `convertToExcalidrawElements` input:\n `{ type: \"rectangle\" | \"ellipse\" | \"diamond\", x, y, width, height, label?: { text, fontSize?, textAlign? } }`,\n `{ type: \"text\", x, y, text, fontSize? }`,\n `{ type: \"arrow\" | \"line\", x, y, points: [[0, 0], [dx, dy]], start?: { id }, end?: { id } }`,\n `{ type: \"frame\", children: [ids], name }`, `{ type: \"image\", fileId, x, y, width, height }`.\n Colours may be token names (`strokeColor: \"primary\"`,\n `backgroundColor: \"warning:soft\"`); a shape with no stroke gets\n `foreground`; text is the system font. Options:\n - `at` — `{ x, y }` in canvas coordinates; the skeletons' box is\n centred there (or its top-left, with `anchor: \"top-left\"`). Default:\n the middle of the view. `false`: keep the skeletons' own x/y.\n - `group` — group them (move as one). `stamp: \"<kind>\"` groups too and\n marks them for `sketchToText`.\n - `customData` — merged into every new element's customData (annotate:\n `{ note: 3 }`).\n - `files` — `[{ id, dataURL, mimeType, created }]` to add first, for\n image skeletons.\n - `select` — `true` (default): select what was inserted.\n- `exportImage(opts?)` → `Promise<Blob>`, a PNG made by Excalidraw's own\n export, in the theme's colours now, on the theme's background. Options:\n `scale` (default 2), `padding` (px, default 16), `transparent: true`\n (no background), `withBackground: false` (leave the background image\n out), `onlySelected: true`.\n- `zoomToFit({ animate? })` — fit everything in view (Shift+1).\n- `setTool(id)` — a built-in tool id or a stamp id.\n- `undo()`, `redo()`, `clear()`, `focus()`.\n- `save()` — send `onChange` / `onSave` now.\n- `palette()` — token → the concrete colour it resolves to now.\n- `repaint()` — re-read the theme and repaint (the canvas already does\n this when `data-theme` changes or a theme stylesheet loads).\n- `api` — Excalidraw's own imperative API, for anything else. Reaching for\n it ties you to Excalidraw 0.18; prefer the methods above.\n\n### Helpers\n\nPlain functions; they take a sketch (or an element array) and never throw\non a partial one:\n\n- `emptySketch()` — `{ type: \"sketch\", version: 1, elements: [], files: {}, appState: {} }`.\n- `isSketchEmpty(sketch)` — nothing drawn (deleted elements and the\n background image do not count).\n- `toSketch(value)` — any accepted input → a sketch.\n- `sketchToText(sketch, { stamps = true })` — the words, for an agent or a\n reader: one line per text or stamp in reading order (top to bottom,\n then left to right), grouped under `# <frame name>` per frame (then\n `# Outside frames`); a shape's label reads as its text; a stamp reads\n `[button] Sign up`, an image `[image]`. `stamps: false` drops the\n brackets. An empty sketch gives `\"\"`.\n- `sketchItems(sketch)` — the same, structured: `[{ kind: \"frame\" | \"stamp\" | \"text\" | \"image\", id, text?, name?, stamp?, container?, frame, x, y, width, height }]`.\n- `STROKES`, `FILLS` — `[{ token, label }]`, the palette the toolbar offers.\n- `SKETCH_TOOLS`, `WIREFRAME_STAMPS` — as above.\n\n## Theming, colours and fonts\n\n- **Chrome.** Excalidraw's own CSS variables (its menus, dialogs, context\n menu, hints) point at the workspace tokens (`theme.js`). Light or dark\n follows the nearest `data-theme` (else `color-scheme`, else the OS).\n- **Canvas colours.** Excalidraw needs concrete colours; they are read at\n run time from the tokens' CSS variables and never written in source.\n Excalidraw's dark mode (which inverts the whole canvas with a filter) is\n switched off: the tokens are already the dark scheme's.\n- **Fonts.** Every text is drawn in the device's own sans-serif\n (Excalidraw's \"Helvetica\" family — Helvetica, Arial…), so no font is ever\n fetched: Excalidraw's hand-drawn fonts would otherwise load from a\n third-party CDN. Text pasted from another Excalidraw is moved into it.\n The strokes keep their hand-drawn look (Sketchy / Clean in the toolbar).\n- **Stylesheet.** The block imports `@excalidraw/excalidraw/index.css`\n from the package; the app's bundler ships it like any CSS.\n\n## Example\n\nA whiteboard that saves itself:\n\n```jsx\n\"use client\";\nimport { SketchCanvas } from \"./blocks/sketch-canvas\";\nimport { updateRow } from \"../lib/actions\";\n\nexport default function Board({ board }) {\n return (\n <SketchCanvas\n label={board.title}\n defaultValue={board.sketch} // jsonb, may be null\n onSave={(sketch) => updateRow({ source: \"public.boards\", id: board.id, patch: { sketch } })}\n className=\"h-dvh rounded-none border-0\"\n />\n );\n}\n```\n\nMarking up a screenshot, with numbered notes:\n\n```jsx\nconst canvas = useSketchCanvas();\nconst addNote = (n) =>\n canvas.current?.insertElements(\n [{ type: \"ellipse\", x: 0, y: 0, width: 32, height: 32, strokeColor: \"destructive\", backgroundColor: \"destructive:soft\", label: { text: String(n) } }],\n { group: true, customData: { note: n } },\n );\n\n<SketchCanvas canvasRef={canvas} background={{ src: screenshotDataUrl }} tools={[\"select\", \"hand\", \"draw\", \"rectangle\", \"arrow\", \"text\", \"eraser\"]} onChange={setSketch} />\nconst image = await canvas.current.exportImage(); // the screenshot with the marks, as a PNG\n```\n\nA wireframe for an agent:\n\n```jsx\nimport { SketchCanvas, WIREFRAME_STAMPS, sketchToText, useSketchCanvas } from \"./blocks/sketch-canvas\";\n\nconst canvas = useSketchCanvas();\n<SketchCanvas canvasRef={canvas} stamps={WIREFRAME_STAMPS} tools={[\"select\", \"hand\", \"draw\", \"rectangle\", \"arrow\", \"text\", \"eraser\", \"frame\"]} onChange={setSketch} />\n\nconst build = async () => onBuild({ image: await canvas.current.exportImage(), sketch, text: sketchToText(sketch), prompt });\n// sketchToText → \"# Login\\nWelcome back\\n[input] Email\\n[button] Sign in\"\n```\n\n## What it will not do\n\n- Collaborate live (several cursors): Excalidraw's collaboration is a\n server of its own, not wired here. Saves are last-write-wins.\n- Offer Excalidraw's library, share links, \"text to diagram\" / AI, or its\n hand-drawn fonts. Its command palette (Cmd+/) and help dialog (?) still\n open, themed; the palette may name actions that do nothing here.\n- Show a minimap.\n- Keep images out of the JSON: an image lives as a data URL in `files`.\n- Render on the server: the server shows the frame and the sketch's\n words; the drawing appears when the page runs.\n- Pick an arbitrary colour: only the theme's tokens.\n\nA canvas 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 sketch-canvas` restores them.\n\n## Credits\n\nExcalidraw (https://github.com/excalidraw/excalidraw), MIT — installed from\nnpm as a dependency, not copied here. See THIRD-PARTY-NOTICES.md.\n"
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
"categories": [
|
|
76
|
+
"sketch",
|
|
77
|
+
"draw",
|
|
78
|
+
"drawing",
|
|
79
|
+
"whiteboard",
|
|
80
|
+
"canvas",
|
|
81
|
+
"excalidraw",
|
|
82
|
+
"diagram",
|
|
83
|
+
"annotate",
|
|
84
|
+
"markup",
|
|
85
|
+
"wireframe",
|
|
86
|
+
"freehand",
|
|
87
|
+
"shapes",
|
|
88
|
+
"arrows",
|
|
89
|
+
"miro",
|
|
90
|
+
"png"
|
|
91
|
+
],
|
|
92
|
+
"docs": "Installed to components/makersclaw/blocks/sketch-canvas. Import: import { SketchCanvas, useSketchCanvas, SKETCH_TOOLS, WIREFRAME_STAMPS, STROKES, FILLS, emptySketch, isSketchEmpty, sketchToText, sketchItems, toSketch } from \"@/components/makersclaw/blocks/sketch-canvas\". Props and an example: components/makersclaw/blocks/sketch-canvas/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
93
|
+
"meta": {
|
|
94
|
+
"makersclaw": {
|
|
95
|
+
"version": "0.3.1",
|
|
96
|
+
"useWhen": "A screen needs freehand drawing: a whiteboard, marking up a screenshot, sketching a UI wireframe, a diagram drawn by hand. The whiteboard, annotate and sketch-to-app blocks are built on it. Not for structured boards of cards and connectors (flow-canvas), charts (chart) or a kanban (board)."
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|