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,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "transaction-list",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "TransactionList",
|
|
6
|
+
"description": "Money records as a plain list, in two variants: history (payments in and out, grouped by month, a received / paid / pending / failed mark and a signed amount) and invoices (number, date, a Paid / Due in N days / N days overdue badge, amount, download). Optional date-range filter, Export and per-row download. Empty, loading and error states built in.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.3.1/r/core.json"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "registry/blocks/transaction-list/index.js",
|
|
14
|
+
"type": "registry:component",
|
|
15
|
+
"target": "@components/makersclaw/blocks/transaction-list/index.js",
|
|
16
|
+
"content": "// The TransactionList block — `makersclaw add transaction-list`. Fixed:\n// compose it through its props (README.md beside this file); a list it\n// cannot be is your own component in components/.\nexport { TransactionList } from \"./transaction-list\";\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "registry/blocks/transaction-list/transaction-list.jsx",
|
|
20
|
+
"type": "registry:component",
|
|
21
|
+
"target": "@components/makersclaw/blocks/transaction-list/transaction-list.jsx",
|
|
22
|
+
"content": "\"use client\";\n\nimport { useEffect, useId, useState } from \"react\";\nimport { Badge, Button, DatePicker, Empty, Icon, IconButton, Notice, Skeleton, cx, todayString } from \"../../ui\";\n\n// Money records as a plain list: one row per charge, payment or invoice,\n// optionally grouped by month, with a date-range filter, an export and a\n// per-row download. A list, not a table: no columns, no sorting — rows\n// come in in the order they should show.\n\n// Server and browser may differ in locale, time zone and even the day:\n// the first render uses fixed ones (en-US, UTC) so it matches the server's\n// HTML, then the reader's own take over.\nfunction useHydrated() {\n const [hydrated, setHydrated] = useState(false);\n useEffect(() => setHydrated(true), []);\n return hydrated;\n}\n\nconst formats = new Map();\nfunction money(amount, currency = \"USD\", locale) {\n const key = `${locale ?? \"\"}|${currency}`;\n let f = formats.get(key);\n if (!f) {\n try {\n f = new Intl.NumberFormat(locale, { style: \"currency\", currency });\n } catch {\n f = new Intl.NumberFormat(locale, { style: \"currency\", currency: \"USD\" });\n }\n formats.set(key, f);\n }\n return f.format(Math.abs(Number(amount) || 0));\n}\n\nconst DAY = /^\\d{4}-\\d{2}-\\d{2}$/;\nconst dayOf = (value) => (typeof value === \"string\" ? value.slice(0, 10) : \"\");\n\n// \"Sep 21, 2026\", or \"Sep 21, 2026, 12:30 PM\" when the row has a time.\nfunction when(value, locale, timeZone) {\n if (!value) return \"\";\n if (typeof value === \"string\" && DAY.test(value)) {\n const [y, m, d] = value.split(\"-\").map(Number);\n return new Intl.DateTimeFormat(locale, { dateStyle: \"medium\", timeZone: \"UTC\" }).format(Date.UTC(y, m - 1, d));\n }\n const d = new Date(value);\n if (Number.isNaN(d.getTime())) return String(value);\n return new Intl.DateTimeFormat(locale, { dateStyle: \"medium\", timeStyle: \"short\", timeZone }).format(d);\n}\n\nfunction monthLabel(key, locale) {\n const [y, m] = key.split(\"-\").map(Number);\n if (!y || !m) return \"Undated\";\n return new Intl.DateTimeFormat(locale, { month: \"long\", year: \"numeric\", timeZone: \"UTC\" }).format(Date.UTC(y, m - 1, 1));\n}\n\nconst daysBetween = (from, to) => {\n const a = Date.parse(`${from}T00:00:00Z`);\n const b = Date.parse(`${to}T00:00:00Z`);\n return Math.round((b - a) / 86400000);\n};\n\n// An invoice's badge: paid, void, or how long until (or since) it is due.\nfunction invoiceBadge(row, today) {\n const status = String(row.status ?? \"\").toLowerCase();\n if (status === \"paid\") return { tone: \"success\", text: \"Paid\" };\n if (status === \"void\" || status === \"cancelled\") return { tone: \"neutral\", text: status === \"void\" ? \"Void\" : \"Cancelled\" };\n if (status === \"draft\") return { tone: \"neutral\", text: \"Draft\" };\n const due = dayOf(row.dueDate);\n if (!DAY.test(due)) return status === \"overdue\" ? { tone: \"danger\", text: \"Overdue\" } : { tone: \"info\", text: \"Open\" };\n const left = daysBetween(today, due);\n if (left > 0) return { tone: \"info\", text: `Due in ${left} ${left === 1 ? \"day\" : \"days\"}` };\n if (left === 0) return { tone: \"warning\", text: \"Due today\" };\n return { tone: \"danger\", text: `${-left} ${left === -1 ? \"day\" : \"days\"} overdue` };\n}\n\n// A history row's direction and state.\nfunction flowOf(row) {\n const status = String(row.status ?? \"\").toLowerCase();\n const incoming = row.direction ? row.direction === \"in\" : Number(row.amount) >= 0;\n if (status === \"pending\") return { key: \"pending\", icon: \"clock\", word: \"Pending\", incoming };\n if (status === \"failed\" || status === \"declined\") return { key: \"failed\", icon: \"x\", word: \"Failed\", incoming };\n if (status === \"refunded\") return { key: \"refunded\", icon: \"refresh\", word: \"Refunded\", incoming };\n return { key: incoming ? \"in\" : \"out\", icon: incoming ? \"arrow-down\" : \"arrow-up\", word: incoming ? \"Received\" : \"Paid\", incoming };\n}\n\nconst MARK = {\n in: \"text-success\",\n out: \"text-foreground\",\n pending: \"text-muted-foreground\",\n failed: \"text-destructive\",\n refunded: \"text-muted-foreground\",\n};\n\nasync function attempt(fn) {\n try {\n const res = await fn();\n return res && res.ok === false ? res.error || \"That did not work.\" : null;\n } catch (e) {\n return e?.message || \"That did not work.\";\n }\n}\n\nfunction RowAction({ row, onDownload, downloadHref, label, onError }) {\n const [busy, setBusy] = useState(false);\n const href = downloadHref?.(row);\n if (!href && !onDownload) return null;\n async function run() {\n setBusy(true);\n onError(null);\n onError(await attempt(() => onDownload(row)));\n setBusy(false);\n }\n return href ? (\n <Button iconOnly variant=\"quiet\" href={href} aria-label={label} title={label} className=\"shrink-0\">\n <Icon name=\"download\" size={18} />\n </Button>\n ) : (\n <IconButton icon=\"download\" label={label} onClick={run} loading={busy} />\n );\n}\n\nfunction RowError({ error }) {\n return error ? (\n <p role=\"alert\" className=\"basis-full text-xs text-destructive\">\n {error}\n </p>\n ) : null;\n}\n\nfunction HistoryRow({ row, currency, locale, timeZone, onOpen, onDownload, downloadHref }) {\n const [error, setError] = useState(null);\n const flow = flowOf(row);\n const amount = money(row.amount, row.currency ?? currency, locale);\n const sign = flow.key === \"in\" ? \"+\" : flow.key === \"out\" ? \"−\" : \"\";\n const title = onOpen ? (\n <Button variant=\"link\" size=\"inherit\" onClick={() => onOpen(row)} className=\"text-left font-medium\">\n {row.name}\n </Button>\n ) : (\n <span className=\"font-medium\">{row.name}</span>\n );\n return (\n <li className=\"flex flex-wrap items-center gap-3 rounded-lg border border-border p-3 sm:gap-4 sm:p-4\">\n <span className={cx(\"grid size-10 shrink-0 place-items-center rounded-md border border-border bg-card\", MARK[flow.key])}>\n <Icon name={flow.icon} size={18} />\n <span className=\"sr-only\">{flow.word}</span>\n </span>\n <div className=\"grid min-w-0 flex-1 gap-0.5\">\n <p className=\"truncate\">{title}</p>\n <p className=\"truncate text-sm text-muted-foreground\">\n {row.description ? `${row.description} · ` : \"\"}\n {when(row.date, locale, timeZone)}\n </p>\n </div>\n <div className=\"grid shrink-0 justify-items-end gap-0.5 text-right\">\n <p\n className={cx(\n \"text-sm font-semibold tabular-nums\",\n MARK[flow.key],\n flow.key === \"failed\" && \"line-through\",\n )}\n >\n {sign}\n {amount}\n </p>\n {flow.key === \"pending\" || flow.key === \"failed\" || flow.key === \"refunded\" ? (\n <p className=\"text-xs text-muted-foreground\">{flow.word}</p>\n ) : null}\n </div>\n <RowAction row={row} onDownload={onDownload} downloadHref={downloadHref} label={`Download ${row.name}`} onError={setError} />\n <RowError error={error} />\n </li>\n );\n}\n\nfunction InvoiceRow({ row, currency, locale, timeZone, today, onOpen, onDownload, downloadHref }) {\n const [error, setError] = useState(null);\n const badge = invoiceBadge(row, today);\n const name = row.number ?? row.name;\n return (\n <li className=\"flex flex-wrap items-center gap-x-4 gap-y-2 py-4\">\n <div className=\"grid min-w-0 basis-full gap-0.5 sm:flex-1 sm:basis-auto\">\n {onOpen ? (\n <Button variant=\"link\" size=\"inherit\" onClick={() => onOpen(row)} className=\"justify-self-start text-left font-semibold\">\n {name}\n </Button>\n ) : (\n <p className=\"truncate font-semibold\">{name}</p>\n )}\n <p className=\"truncate text-sm text-muted-foreground\">\n {row.number && row.name ? `${row.name} · ` : \"\"}\n {when(row.date, locale, timeZone)}\n </p>\n </div>\n <Badge tone={badge.tone} variant=\"soft\">\n {badge.text}\n </Badge>\n <p className=\"ml-auto text-sm font-semibold tabular-nums\">{money(row.amount, row.currency ?? currency, locale)}</p>\n <RowAction row={row} onDownload={onDownload} downloadHref={downloadHref} label={`Download ${name}`} onError={setError} />\n <RowError error={error} />\n </li>\n );\n}\n\nfunction Loading({ variant }) {\n return (\n <ul aria-busy=\"true\" aria-label=\"Loading\" className={cx(\"grid\", variant === \"history\" ? \"gap-2\" : \"divide-y divide-border\")}>\n {[0, 1, 2].map((i) => (\n <li key={i} className={cx(\"flex items-center gap-3\", variant === \"history\" ? \"rounded-lg border border-border p-4\" : \"py-4\")}>\n {variant === \"history\" ? <Skeleton shape=\"rect\" className=\"size-10\" /> : null}\n <div className=\"grid flex-1 gap-2\">\n <Skeleton className=\"w-1/3\" />\n <Skeleton className=\"w-1/4\" />\n </div>\n <Skeleton className=\"w-16\" />\n </li>\n ))}\n </ul>\n );\n}\n\nexport function TransactionList({\n rows,\n variant = \"history\",\n currency = \"USD\",\n locale: localeProp,\n groupBy,\n title,\n description,\n loading = false,\n error,\n dateFilter = false,\n range: rangeProp,\n onRangeChange,\n onExport,\n exportLabel = \"Export\",\n onOpen,\n onDownload,\n downloadHref,\n emptyTitle,\n emptyBody,\n today,\n className,\n}) {\n const hydrated = useHydrated();\n const locale = localeProp ?? (hydrated ? undefined : \"en-US\");\n const timeZone = hydrated ? undefined : \"UTC\";\n const invoices = variant === \"invoices\";\n const headingId = useId();\n const [ownRange, setOwnRange] = useState(null);\n const [exporting, setExporting] = useState(false);\n const [exportError, setExportError] = useState(null);\n const range = rangeProp !== undefined ? rangeProp : ownRange;\n const setRange = (next) => {\n if (rangeProp === undefined) setOwnRange(next);\n onRangeChange?.(next);\n };\n const now = today ?? (hydrated ? todayString() : new Date().toISOString().slice(0, 10));\n const group = (groupBy ?? (invoices ? \"none\" : \"month\")) === \"month\";\n const heading = title ?? (invoices ? \"Invoices\" : \"Transactions\");\n\n const all = (rows ?? []).filter(Boolean);\n const shown = range?.from\n ? all.filter((r) => {\n const d = dayOf(r.date);\n return d >= range.from && d <= (range.to ?? range.from);\n })\n : all;\n\n const groups = [];\n if (group) {\n const at = new Map();\n for (const r of shown) {\n const key = dayOf(r.date).slice(0, 7) || \"none\";\n if (!at.has(key)) {\n at.set(key, groups.length);\n groups.push({ key, rows: [] });\n }\n groups[at.get(key)].rows.push(r);\n }\n } else groups.push({ key: \"all\", rows: shown });\n\n async function runExport() {\n setExporting(true);\n setExportError(null);\n setExportError(await attempt(() => onExport(shown, range)));\n setExporting(false);\n }\n\n const rowProps = { currency, locale, timeZone, today: now, onOpen, onDownload, downloadHref };\n const Row = invoices ? InvoiceRow : HistoryRow;\n const listClass = invoices ? \"divide-y divide-border\" : \"grid gap-2\";\n\n let body;\n if (loading) body = <Loading variant={variant} />;\n else if (!all.length)\n body = (\n <Empty\n icon={invoices ? \"receipt\" : \"dollar\"}\n title={emptyTitle ?? (invoices ? \"No invoices yet\" : \"No transactions yet\")}\n body={emptyBody ?? (invoices ? \"Invoices appear here once they are issued.\" : \"Payments in and out appear here.\")}\n />\n );\n else if (!shown.length)\n body = (\n <Empty\n icon=\"calendar\"\n title=\"Nothing in these dates\"\n body=\"Pick a wider range, or clear it to see everything.\"\n action={\n <Button variant=\"secondary\" onClick={() => setRange(null)}>\n Clear dates\n </Button>\n }\n />\n );\n else\n body = (\n <div className=\"grid gap-5\">\n {groups.map((g) =>\n group ? (\n <section key={g.key} aria-label={g.key === \"none\" ? \"Undated\" : monthLabel(g.key, locale)} className=\"grid gap-2\">\n <h3 className=\"px-1 text-sm font-semibold text-muted-foreground\">\n {g.key === \"none\" ? \"Undated\" : monthLabel(g.key, locale)}\n </h3>\n <ul className={listClass}>\n {g.rows.map((r) => (\n <Row key={r.id} row={r} {...rowProps} />\n ))}\n </ul>\n </section>\n ) : (\n <ul key={g.key} aria-labelledby={headingId} className={listClass}>\n {g.rows.map((r) => (\n <Row key={r.id} row={r} {...rowProps} />\n ))}\n </ul>\n ),\n )}\n </div>\n );\n\n return (\n <section aria-labelledby={headingId} className={cx(\"grid gap-4 rounded-lg border border-border bg-card p-4 text-card-foreground sm:p-6\", className)}>\n <header className=\"flex flex-wrap items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <h2 id={headingId} className=\"font-semibold\">\n {heading}\n </h2>\n {description ? <p className=\"mt-0.5 text-sm text-muted-foreground\">{description}</p> : null}\n </div>\n {dateFilter || onExport ? (\n <div className=\"flex w-full flex-wrap items-center gap-2 sm:w-auto\">\n {dateFilter ? (\n <DatePicker\n mode=\"range\"\n value={range}\n onChange={setRange}\n placeholder=\"All dates\"\n presets\n size=\"sm\"\n label=\"Show dates\"\n className=\"min-w-0 flex-1 sm:w-56 sm:flex-none\"\n />\n ) : null}\n {onExport ? (\n <Button size=\"sm\" icon=\"download\" onClick={runExport} loading={exporting} disabled={loading || !shown.length}>\n {exportLabel}\n </Button>\n ) : null}\n </div>\n ) : null}\n </header>\n {error ? <Notice tone=\"danger\">{error}</Notice> : null}\n {exportError ? (\n <Notice tone=\"danger\" onDismiss={() => setExportError(null)}>\n {exportError}\n </Notice>\n ) : null}\n {body}\n </section>\n );\n}\n"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "registry/blocks/transaction-list/README.md",
|
|
26
|
+
"type": "registry:file",
|
|
27
|
+
"target": "@components/makersclaw/blocks/transaction-list/README.md",
|
|
28
|
+
"content": "# TransactionList\n\nMoney records as a plain list in a card. There are two variants:\n\n- `history` (the default) — payments in and out, grouped by month. Each\n row has a mark (received, paid, pending, failed, refunded), the name,\n the date and a signed amount.\n- `invoices` — one row per invoice: its number and date, a badge (Paid,\n Due in 12 days, Due today, 3 days overdue, Void), the amount and a\n download button.\n\nThe header can hold a date-range filter and an Export button. It is a\nlist, not a table: there are no columns and no sorting, so the rows show\nin the order they come in (newest first, usually).\n\n```jsx\nimport { TransactionList } from \"../components/blocks/transaction-list\"; // a Makersclaw app (\"./blocks/transaction-list\" from components/)\nimport { TransactionList } from \"@/components/makersclaw/blocks/transaction-list\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component, and\nload the rows in the server surface (README §4).\n\n## The data\n\nA history row:\n\n```js\n{ id: \"tx_1\", name: \"Northwind Ltd\", date: \"2026-09-21T12:30:00Z\", amount: 2500, direction: \"in\", status: \"completed\", description: \"Invoice #1042\" }\n```\n\n- `date` — `\"YYYY-MM-DD\"`, or an ISO timestamp (then the time shows too).\n Months group by its first seven characters.\n- `amount` — a number in the currency's major unit. The sign is drawn\n from `direction` (`\"in\"` · `\"out\"`); without one, a negative amount is\n money out.\n- `status` — `completed` (the default), `pending`, `failed` (struck\n through), `refunded`.\n- `description` — a short line before the date. `currency` — per row,\n over the `currency` prop.\n\nAn invoice row:\n\n```js\n{ id: \"inv_24\", number: \"INV-0024\", date: \"2026-09-15\", dueDate: \"2026-10-15\", amount: 2500, status: \"open\" }\n```\n\n- `status` — `paid`, `open` (then the badge counts the days to or past\n `dueDate`), `overdue` (when there is no due date), `void`, `draft`,\n `cancelled`.\n- `name` — who it is for; shown before the date when `number` is set.\n\n## Props\n\n- `rows` — the records, in the order to show.\n- `variant` — `\"history\"` (default) or `\"invoices\"`.\n- `currency` — default `\"USD\"`. `locale` — money and dates, default the\n reader's.\n- `groupBy` — `\"month\"` (the default for history) or `\"none\"` (the\n default for invoices).\n- `title` · `description` — the card's heading; default \"Transactions\" or\n \"Invoices\".\n- `dateFilter` — `true` shows a date-range picker in the header (with\n presets). The block hides rows outside the range itself; `range` +\n `onRangeChange(range)` control it (`{ from, to }` or `null`) when the\n surface loads by date instead.\n- `onExport(rows, range)` — an Export button; it gets the rows on screen.\n Return `{ ok: false, error }` to say why it failed.\n- `onDownload(row)` — a download button on each row (make the PDF, open\n it); same return, the reason shows under the row. Or\n `downloadHref(row)` — a link instead (return `null` for a row with none).\n- `onOpen(row)` — the row's name becomes a button (open your `Sheet`).\n- `loading` — skeleton rows. `error` — a line above the list.\n- `emptyTitle` · `emptyBody` — the empty state's words.\n- `today` — `\"YYYY-MM-DD\"`, the day an invoice's due date is counted\n from; default today.\n- `className` — layout around it (width, margin). Never colour.\n\n## Example\n\n```jsx\n// components/Billing.jsx — client\n\"use client\";\nimport { TransactionList } from \"./blocks/transaction-list\";\n\nexport default function Billing({ invoices, payments, error }) {\n return (\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <TransactionList\n variant=\"invoices\"\n rows={invoices}\n error={error}\n description=\"Every invoice we have sent you.\"\n downloadHref={(inv) => inv.pdfUrl}\n />\n <TransactionList\n rows={payments}\n dateFilter\n description=\"Money in and out of the account.\"\n onExport={async (rows) => {\n const csv = rows.map((r) => [r.date, r.name, r.amount].join(\",\")).join(\"\\n\");\n const url = URL.createObjectURL(new Blob([csv], { type: \"text/csv\" }));\n Object.assign(document.createElement(\"a\"), { href: url, download: \"payments.csv\" }).click();\n return { ok: true };\n }}\n />\n </div>\n );\n}\n```\n\n## What it will not do\n\n- Sort, page, or filter by column: rows come in ordered, and a record set\n that needs those is a `data-table`.\n- Total the rows: put a `Stat` above it if a sum matters.\n- Make the invoice PDF or the export file: `onDownload` and `onExport`\n are the app's.\n\nA list it cannot be is your own component in `components/`, built from\nthe kit — not an edit to this block. Its files are fixed: `npm run check`\nrefuses a change, and `makersclaw add transaction-list` restores them.\n\n## Credits\n\nShape and behaviour from Creative Tim UI (https://github.com/creativetimofficial/ui), MIT; this is a rewrite on the Makersclaw kit.\n"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"categories": [
|
|
32
|
+
"transactions",
|
|
33
|
+
"payments",
|
|
34
|
+
"history",
|
|
35
|
+
"invoices",
|
|
36
|
+
"billing",
|
|
37
|
+
"receipts",
|
|
38
|
+
"money",
|
|
39
|
+
"ledger",
|
|
40
|
+
"activity",
|
|
41
|
+
"statement",
|
|
42
|
+
"download",
|
|
43
|
+
"export",
|
|
44
|
+
"finance"
|
|
45
|
+
],
|
|
46
|
+
"docs": "Installed to components/makersclaw/blocks/transaction-list. Import: import { TransactionList } from \"@/components/makersclaw/blocks/transaction-list\". Props and an example: components/makersclaw/blocks/transaction-list/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
47
|
+
"meta": {
|
|
48
|
+
"makersclaw": {
|
|
49
|
+
"version": "0.3.1",
|
|
50
|
+
"useWhen": "A billing page's invoices, an account's payment history, a wallet's recent activity: records read top to bottom, newest first. Not for records the founder sorts, filters by column or totals (data-table), or for one order (order-detail)."
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "whiteboard",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Whiteboard",
|
|
6
|
+
"description": "A full-page freeform whiteboard on sketch-canvas: a top bar with the board's title (click to rename), the save state (Saving…, Saved, or the error with Retry), a Board / Text view switch, Export PNG and an optional Share button; under it the Miro-style canvas (tool rail, floating toolbar, zoom, undo). An empty board offers starter templates — blank, a grid of titled areas, a 2×2 matrix — and the Text view lists every word on the board for a screen reader or an agent. With onCreateTask, a selected note or text becomes a task from the top bar (\"Create task\", editable title; several notes make one task each), and a small badge marks it on the board from then on. The sketch is plain JSON; saves are async callbacks answering { ok }.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.3.1/r/core.json",
|
|
9
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.3.1/r/sketch-canvas.json"
|
|
10
|
+
],
|
|
11
|
+
"dependencies": [],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "registry/blocks/whiteboard/index.js",
|
|
15
|
+
"type": "registry:component",
|
|
16
|
+
"target": "@components/makersclaw/blocks/whiteboard/index.js",
|
|
17
|
+
"content": "// The Whiteboard block — `makersclaw add whiteboard`. Fixed: compose it\n// through its props (README.md beside this file). Built on sketch-canvas\n// (the canvas, its tools and its helpers); the templates are plain\n// functions that run anywhere, the server included.\nexport { Whiteboard } from \"./whiteboard\";\nexport { WHITEBOARD_TEMPLATES, gridSketch, matrixSketch } from \"./templates\";\n"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"path": "registry/blocks/whiteboard/whiteboard.jsx",
|
|
21
|
+
"type": "registry:component",
|
|
22
|
+
"target": "@components/makersclaw/blocks/whiteboard/whiteboard.jsx",
|
|
23
|
+
"content": "\"use client\";\n\n// Whiteboard — a full-page freeform board on sketch-canvas: a top bar with\n// the board's title (click to rename), the save state, a Board / Text view\n// switch, Export PNG and Share; under it the canvas with sketch-canvas's\n// rail, context toolbar, zoom and undo. An empty board offers starter\n// templates (blank, grid, 2×2 matrix). The Text view is the board's words\n// as a list — the accessible alternative to the drawing, and what an agent\n// reads.\n//\n// With `onCreateTask`, a selected note or text gets a \"Create task\" button\n// in the top bar; the element keeps a mark (customData.task, drawn as a\n// small badge over it) once the task exists.\n//\n// Saves are the page's: `onSave(sketch)` and `onRename(title)` are async\n// and answer { ok }; the top bar shows \"Saving…\", \"Saved\", or the error\n// with a Retry. The canvas stays mounted under the Text view, so switching\n// views never loses an edit or the undo history.\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { Button, Field, Icon, IconButton, Input, Kbd, Popover, ToggleGroup, Tooltip, cx, toast } from \"../../ui\";\nimport { SketchCanvas, isSketchEmpty, sketchItems, sketchToText, useSketchCanvas } from \"../sketch-canvas\";\nimport { WHITEBOARD_TEMPLATES, fileNameFor, templateSketch } from \"./templates\";\n\nconst IDLE = { state: \"idle\" };\n\nasync function settle(fn, ...args) {\n try {\n const res = await fn(...args);\n return res?.ok ? { state: \"saved\" } : { state: \"error\", error: res?.error };\n } catch (e) {\n return { state: \"error\", error: e?.message };\n }\n}\n\nexport function Whiteboard({\n title,\n defaultTitle = \"Untitled board\",\n onRename,\n value,\n defaultValue,\n onChange,\n onSave,\n changeDelay,\n templates = WHITEBOARD_TEMPLATES,\n onShare,\n shareLabel = \"Share\",\n onExport,\n onBack,\n backLabel = \"Back\",\n meta,\n readOnly = false,\n defaultView = \"board\",\n canvasRef,\n onCreateTask,\n onOpenTask,\n taskLabel = \"Create task\",\n className,\n}) {\n const own = useSketchCanvas();\n const ref = canvasRef ?? own;\n\n // ---- title --------------------------------------------------------------\n const [ownTitle, setOwnTitle] = useState(defaultTitle);\n const [pendingTitle, setPendingTitle] = useState(null); // typed, not yet confirmed by onRename\n const shownTitle = pendingTitle ?? (title !== undefined ? title : ownTitle);\n const [titleSave, setTitleSave] = useState(IDLE);\n const titleSeq = useRef(0);\n\n const rename = useCallback(\n async (next) => {\n if (title === undefined) setOwnTitle(next);\n if (!onRename) return;\n const seq = ++titleSeq.current;\n setPendingTitle(next);\n setTitleSave({ state: \"saving\" });\n const res = await settle(onRename, next);\n if (seq !== titleSeq.current) return;\n setTitleSave(res);\n if (res.state === \"saved\") setPendingTitle(null);\n },\n [title, onRename],\n );\n\n // ---- the sketch ---------------------------------------------------------\n const [sketch, setSketch] = useState(() => value ?? defaultValue ?? null);\n useEffect(() => {\n if (value !== undefined) setSketch(value);\n }, [value]);\n const empty = isSketchEmpty(sketch);\n const [blank, setBlank] = useState(false); // \"Blank\" chosen: the plain hint, not the templates\n const [sketchSave, setSketchSave] = useState(IDLE);\n const sketchSeq = useRef(0);\n const lastSketch = useRef(null);\n const cbs = useRef({});\n cbs.current = { onChange, onSave };\n\n const saveSketch = useCallback(async (s) => {\n const fn = cbs.current.onSave;\n if (!fn) return;\n const seq = ++sketchSeq.current;\n setSketchSave({ state: \"saving\" });\n const res = await settle(fn, s);\n if (seq !== sketchSeq.current) return;\n setSketchSave(res);\n }, []);\n\n const handleChange = useCallback(\n (s) => {\n lastSketch.current = s;\n setSketch(s);\n if (isSketchEmpty(s)) setBlank(false);\n cbs.current.onChange?.(s);\n saveSketch(s);\n },\n [saveSketch],\n );\n\n const applyTemplate = (t) => {\n const h = ref.current;\n if (!h) return;\n const s = templateSketch(t);\n if (!s) {\n setBlank(true);\n h.focus();\n return;\n }\n h.setScene(s);\n h.setTool(\"select\");\n requestAnimationFrame(() => h.zoomToFit({ animate: false }));\n h.focus();\n };\n\n // ---- export -------------------------------------------------------------\n const [exporting, setExporting] = useState(false);\n const [exportError, setExportError] = useState(null);\n const exportPng = async () => {\n const h = ref.current;\n if (!h) return;\n setExporting(true);\n setExportError(null);\n try {\n const blob = await h.exportImage();\n if (onExport) {\n const res = await settle(onExport, blob, { title: shownTitle, fileName: fileNameFor(shownTitle) });\n if (res.state === \"error\") setExportError(res.error || \"Couldn't export\");\n } else {\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = fileNameFor(shownTitle);\n document.body.appendChild(a);\n a.click();\n a.remove();\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n }\n } catch (e) {\n setExportError(e?.message || \"Couldn't export\");\n } finally {\n setExporting(false);\n }\n };\n\n // ---- tasks from what is on the board -----------------------------------\n // Excalidraw tells us every change (selection, scroll, zoom); from it we\n // read what could become a task (selected notes and texts that are not\n // one yet) and where the badges of those that are sit on screen.\n const [handle, setHandle] = useState(null);\n const [picked, setPicked] = useState([]); // [{ key, ids, text }]\n const [marks, setMarks] = useState([]); // [{ key, x, y, title, id }]\n const [pending, setPending] = useState(() => new Map()); // key → title\n const pendingRef = useRef(pending);\n pendingRef.current = pending;\n const keys = useRef({ picked: \"\", marks: \"\" });\n const scene = useRef(null);\n const canTask = Boolean(onCreateTask) && !readOnly;\n\n const readScene = useCallback((elements, appState) => {\n scene.current = { elements, appState };\n const live = (elements ?? []).filter((el) => !el.isDeleted);\n const byId = new Map(live.map((el) => [el.id, el]));\n const sel = appState?.selectedElementIds ?? {};\n const found = new Map();\n for (const el of live) {\n if (!sel[el.id]) continue;\n const found1 = taskSource(el, byId);\n if (!found1 || found.has(found1.key) || isTask(found1, byId) || pendingRef.current.has(found1.key)) continue;\n found.set(found1.key, found1);\n }\n const nextPicked = [...found.values()].sort((a, b) => a.y - b.y || a.x - b.x);\n const pk = nextPicked.map((p) => `${p.key}:${p.text}`).join(\"|\");\n if (pk !== keys.current.picked) {\n keys.current.picked = pk;\n setPicked(nextPicked);\n }\n // Badges: at the top-right corner of each converted note, in screen px.\n const z = appState?.zoom?.value ?? 1;\n const sx = appState?.scrollX ?? 0;\n const sy = appState?.scrollY ?? 0;\n const w = appState?.width ?? Infinity;\n const h = appState?.height ?? Infinity;\n const nextMarks = [];\n for (const el of live) {\n if (el.type === \"text\" && el.containerId && byId.has(el.containerId)) continue;\n const task = el.customData?.task;\n const title = task?.title ?? pendingRef.current.get(el.id);\n if (!task && !pendingRef.current.has(el.id)) continue;\n const x = Math.round((el.x + el.width + sx) * z);\n const y = Math.round((el.y + sy) * z);\n if (x < -40 || y < -20 || x > w + 40 || y > h + 20) continue;\n nextMarks.push({ key: el.id, x, y, title, id: task?.id ?? null, pending: !task });\n }\n const mk = JSON.stringify(nextMarks);\n if (mk !== keys.current.marks) {\n keys.current.marks = mk;\n setMarks(nextMarks);\n }\n }, []);\n\n useEffect(() => {\n const api = handle?.api;\n if (!api?.onChange) return;\n readScene(api.getSceneElements(), api.getAppState());\n return api.onChange((els, st) => readScene(els, st));\n }, [handle, readScene]);\n useEffect(() => {\n // A pending mark came or went: redraw the badges.\n keys.current.marks = \"\";\n if (scene.current) readScene(scene.current.elements, scene.current.appState);\n }, [pending, readScene]);\n\n /** Write (or clear) the task mark on the elements a task came from. */\n const markElements = (ids, task) => {\n const api = (handle ?? ref.current)?.api;\n if (!api) return;\n const set = new Set(ids);\n const els = api.getSceneElementsIncludingDeleted().map((el) => {\n if (!set.has(el.id)) return el;\n const { task: _old, ...rest } = el.customData ?? {};\n return {\n ...el,\n customData: task ? { ...rest, task } : rest,\n version: (el.version ?? 1) + 1,\n versionNonce: Math.floor(Math.random() * 2 ** 31),\n updated: Date.now(),\n };\n });\n api.updateScene({ elements: els });\n };\n\n const [taskOpen, setTaskOpen] = useState(false);\n const [taskDraft, setTaskDraft] = useState(\"\");\n const createTasks = async (items, override) => {\n setTaskOpen(false);\n const list = items.map((it) => ({ ...it, title: clipTitle(items.length === 1 && override?.trim() ? override : it.text) }));\n setPending((m) => {\n const next = new Map(m);\n for (const it of list) next.set(it.key, it.title);\n return next;\n });\n const results = await Promise.all(\n list.map(async (it) => {\n let res;\n try {\n res = await onCreateTask({ title: it.title, sourceIds: it.ids });\n } catch (e) {\n res = { ok: false, error: e?.message };\n }\n const ok = Boolean(res) && res.ok !== false;\n if (ok) markElements(it.ids, { id: res?.id ?? null, title: it.title });\n return { it, ok, id: res?.id ?? null, error: res?.error };\n }),\n );\n setPending((m) => {\n const next = new Map(m);\n for (const it of list) next.delete(it.key);\n return next;\n });\n const done = results.filter((r) => r.ok);\n const failed = results.filter((r) => !r.ok);\n if (done.length) {\n const one = done.length === 1 ? done[0] : null;\n toast.success(one ? `Task created: ${one.it.title}` : `${done.length} tasks created`, {\n action: one && onOpenTask ? { label: \"Open\", onClick: () => onOpenTask({ id: one.id, title: one.it.title, sourceIds: one.it.ids }) } : undefined,\n });\n }\n if (failed.length) {\n toast.error(failed.length === 1 ? `Couldn't create “${failed[0].it.title}”` : `Couldn't create ${failed.length} tasks`, {\n description: failed[0].error || \"Nothing was created. Try again.\",\n });\n }\n };\n\n // ---- view ---------------------------------------------------------------\n const [view, setView] = useState(defaultView === \"text\" ? \"text\" : \"board\");\n const textView = view === \"text\";\n\n const retry = () => {\n if (sketchSave.state === \"error\") saveSketch(lastSketch.current ?? ref.current?.getScene() ?? sketch);\n if (titleSave.state === \"error\") rename(shownTitle);\n };\n\n const sized = /(^|\\s)(h-|min-h-|size-|flex-1|grow)/.test(className ?? \"\");\n const hasTemplates = Array.isArray(templates) && templates.length > 0;\n\n return (\n <section\n aria-label={shownTitle}\n className={cx(\"flex flex-col overflow-hidden rounded-lg border border-border bg-background text-foreground\", !sized && \"h-[40rem]\", className)}\n >\n <header className=\"flex min-h-14 shrink-0 items-center gap-1 border-b border-border bg-card px-2 py-1.5 text-card-foreground sm:gap-2 sm:px-3\">\n {onBack ? <IconButton icon=\"arrow-left\" label={backLabel} size=\"sm\" onClick={onBack} /> : null}\n <span aria-hidden=\"true\" className=\"hidden size-8 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground sm:grid\">\n <Icon name=\"pencil\" size={16} />\n </span>\n <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n <TitleField value={shownTitle} readOnly={readOnly} onCommit={rename} />\n {meta ? <span className=\"hidden min-w-0 truncate text-xs text-muted-foreground lg:inline\">{meta}</span> : null}\n </div>\n <SaveStatus states={[sketchSave, titleSave]} onRetry={retry} />\n <ToggleGroup\n size=\"sm\"\n aria-label=\"View\"\n value={view}\n onChange={setView}\n items={[\n { value: \"board\", label: \"Board view\", icon: \"pencil\", iconOnly: true },\n { value: \"text\", label: \"Text view\", icon: \"list\", iconOnly: true },\n ]}\n />\n {canTask && picked.length ? (\n <Popover\n open={taskOpen}\n onOpenChange={(o) => {\n if (o) setTaskDraft(picked[0]?.text ?? \"\");\n setTaskOpen(o);\n }}\n label={picked.length === 1 ? taskLabel : `Create ${picked.length} tasks`}\n align=\"end\"\n width=\"md\"\n trigger={(p) => (\n <Button {...p} size=\"sm\" icon=\"check-circle\" aria-label={picked.length === 1 ? taskLabel : `Create ${picked.length} tasks`}>\n <span className=\"max-sm:hidden\">{picked.length === 1 ? taskLabel : `Create ${picked.length} tasks`}</span>\n </Button>\n )}\n >\n {picked.length === 1 ? (\n <form\n className=\"grid gap-3\"\n onSubmit={(e) => {\n e.preventDefault();\n if (taskDraft.trim()) createTasks(picked, taskDraft);\n }}\n >\n <Field id=\"wb-task-title\" label=\"Task\">\n <Input id=\"wb-task-title\" value={taskDraft} maxLength={200} onChange={(e) => setTaskDraft(e.target.value)} onFocus={(e) => e.target.select()} />\n </Field>\n <p className=\"text-xs text-muted-foreground\">The note stays on the board, with a mark that it's now a task.</p>\n <div className=\"flex justify-end gap-2\">\n <Button size=\"sm\" variant=\"quiet\" onClick={() => setTaskOpen(false)}>\n Cancel\n </Button>\n <Button size=\"sm\" variant=\"primary\" type=\"submit\" disabled={!taskDraft.trim()}>\n {taskLabel}\n </Button>\n </div>\n </form>\n ) : (\n <div className=\"grid gap-3\">\n <p className=\"text-sm font-medium\">One task for each note</p>\n <ul className=\"grid max-h-48 gap-1 overflow-y-auto\">\n {picked.map((it) => (\n <li key={it.key} className=\"flex items-start gap-2 text-sm\">\n <Icon name=\"check-circle\" size={14} className=\"mt-0.5 shrink-0 text-muted-foreground\" />\n <span className=\"min-w-0 break-words\">{clipTitle(it.text)}</span>\n </li>\n ))}\n </ul>\n <div className=\"flex justify-end gap-2\">\n <Button size=\"sm\" variant=\"quiet\" onClick={() => setTaskOpen(false)}>\n Cancel\n </Button>\n <Button size=\"sm\" variant=\"primary\" onClick={() => createTasks(picked)}>\n Create {picked.length} tasks\n </Button>\n </div>\n </div>\n )}\n </Popover>\n ) : null}\n <Tooltip label={exportError ? `Export failed: ${exportError}` : \"Export as PNG\"}>\n <Button size=\"sm\" icon={exportError ? \"warning\" : \"download\"} loading={exporting} disabled={empty} onClick={exportPng} aria-label={exportError ? `Export PNG — failed: ${exportError}. Try again` : \"Export PNG\"}>\n <span className=\"max-sm:hidden\">Export</span>\n </Button>\n </Tooltip>\n {onShare ? (\n <Button size=\"sm\" variant=\"primary\" icon=\"share\" onClick={onShare} aria-label={shareLabel}>\n <span className=\"max-sm:hidden\">{shareLabel}</span>\n </Button>\n ) : null}\n </header>\n\n <div className=\"relative min-h-0 flex-1\">\n <div className=\"absolute inset-0\" inert={textView || undefined} aria-hidden={textView || undefined}>\n <SketchCanvas\n label={shownTitle}\n value={value}\n defaultValue={defaultValue}\n onChange={handleChange}\n changeDelay={changeDelay}\n readOnly={readOnly}\n canvasRef={ref}\n onReady={setHandle}\n empty={hasTemplates && !blank ? <Starter templates={templates} onPick={applyTemplate} /> : undefined}\n className=\"h-full rounded-none border-0\"\n />\n {marks.length ? <TaskMarks marks={marks} onOpen={onOpenTask} /> : null}\n </div>\n {textView ? <TextView sketch={sketch} onBoard={() => setView(\"board\")} readOnly={readOnly} /> : null}\n </div>\n </section>\n );\n}\n\n/** The title: a button that becomes a field; Enter or leaving saves, Esc\n * puts it back. */\nfunction TitleField({ value, readOnly, onCommit }) {\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(value);\n const done = useRef(false);\n if (readOnly) return <h2 className=\"min-w-0 truncate px-1 text-sm font-semibold\">{value}</h2>;\n if (!editing) {\n return (\n <Tooltip label=\"Rename\">\n <Button\n variant=\"quiet\"\n size=\"sm\"\n align=\"start\"\n className=\"min-w-0 max-w-full\"\n aria-label={`${value}. Rename board`}\n onClick={() => {\n setDraft(value);\n done.current = false;\n setEditing(true);\n }}\n >\n <span className=\"truncate font-semibold\">{value}</span>\n </Button>\n </Tooltip>\n );\n }\n const finish = (save) => {\n if (done.current) return;\n done.current = true;\n setEditing(false);\n const next = draft.trim();\n if (save && next && next !== value) onCommit(next);\n };\n return (\n <Input\n autoFocus\n aria-label=\"Board title\"\n value={draft}\n maxLength={120}\n className=\"min-w-0 max-w-sm font-semibold\"\n onChange={(e) => setDraft(e.target.value)}\n onFocus={(e) => e.target.select()}\n onBlur={() => finish(true)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n finish(true);\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n finish(false);\n }\n }}\n />\n );\n}\n\n/** One line for every save the board makes (the drawing, the title): the\n * worst of them. */\nfunction SaveStatus({ states, onRetry }) {\n const failed = states.find((s) => s.state === \"error\");\n if (failed) {\n return (\n <div role=\"alert\" className=\"flex min-w-0 items-center gap-1 text-xs\">\n <Icon name=\"warning\" size={14} className=\"shrink-0 text-destructive\" />\n <span className=\"truncate max-md:sr-only\">{failed.error || \"Couldn't save\"}</span>\n <Button variant=\"quiet\" size=\"sm\" onClick={onRetry}>\n Retry\n </Button>\n </div>\n );\n }\n const saving = states.some((s) => s.state === \"saving\");\n const saved = states.some((s) => s.state === \"saved\");\n if (!saving && !saved) return null;\n return (\n <p role=\"status\" className=\"flex shrink-0 items-center gap-1 px-1 text-xs text-muted-foreground\">\n <Icon name={saving ? \"loader\" : \"check\"} size={14} className={cx(saving && \"motion-safe:animate-spin\")} />\n <span className=\"max-sm:sr-only\">{saving ? \"Saving…\" : \"Saved\"}</span>\n </p>\n );\n}\n\n/** The empty board: how to start, and the templates. The layer under it\n * lets clicks through to the canvas; the card takes its own. */\nfunction Starter({ templates, onPick }) {\n return (\n <div className=\"pointer-events-auto w-full max-w-md rounded-xl border border-border bg-popover p-4 text-popover-foreground shadow-lg\">\n <p className=\"text-base font-semibold\">Start your board</p>\n <p className=\"mt-1 text-sm text-muted-foreground\">\n Pick a starting point, or double-click anywhere to write. <Kbd>P</Kbd> draws, <Kbd>R</Kbd> makes a box.\n </p>\n <ul className=\"mt-4 grid grid-cols-3 gap-2\" aria-label=\"Templates\">\n {templates.map((t) => (\n <li key={t.id} className=\"min-w-0\">\n {/* raw: a template tile holds a drawn preview above its name;\n Button is a one-line label. Keyboard and focus ring as Button. */}\n <button\n type=\"button\"\n onClick={() => onPick(t)}\n title={t.description}\n aria-label={t.description ? `${t.label}: ${t.description}` : t.label}\n className=\"group flex w-full flex-col gap-2 rounded-lg border border-border bg-background p-2 text-left outline-none transition-colors duration-150 hover:border-primary hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background motion-reduce:transition-none\"\n >\n <Preview template={t} />\n <span className=\"truncate text-xs font-medium\">{t.label}</span>\n </button>\n </li>\n ))}\n </ul>\n </div>\n );\n}\n\n/** A small drawing of what a template lays down, in tokens. */\nfunction Preview({ template }) {\n const box = \"aspect-[4/3] w-full overflow-hidden rounded-md border border-border bg-card\";\n if (template.preview) return <span className={cx(box, \"grid place-items-center\")}>{template.preview}</span>;\n if (template.id === \"grid\") {\n return (\n <span aria-hidden=\"true\" className={cx(box, \"grid grid-cols-3 grid-rows-2 gap-1 p-1.5\")}>\n {Array.from({ length: 6 }, (_, i) => (\n <span key={i} className=\"rounded-sm border border-dashed border-muted-foreground/50\" />\n ))}\n </span>\n );\n }\n if (template.id === \"matrix\") {\n return (\n <span aria-hidden=\"true\" className={cx(box, \"block p-1.5\")}>\n <span className=\"grid size-full grid-cols-2 grid-rows-2 gap-px border-b-2 border-l-2 border-muted-foreground\">\n <span className=\"bg-success/25\" />\n <span className=\"bg-info/25\" />\n <span className=\"bg-muted\" />\n <span className=\"bg-warning/25\" />\n </span>\n </span>\n );\n }\n if (template.id === \"blank\") {\n return (\n <span aria-hidden=\"true\" className={cx(box, \"grid place-items-center text-muted-foreground\")}>\n <Icon name=\"plus\" size={18} />\n </span>\n );\n }\n return (\n <span aria-hidden=\"true\" className={cx(box, \"grid place-items-center text-muted-foreground\")}>\n <Icon name={template.icon ?? \"frame\"} size={18} />\n </span>\n );\n}\n\n/** The board as a list: frames as sections, their words in reading order;\n * what sits outside any frame last. */\nfunction TextView({ sketch, onBoard, readOnly }) {\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 inFrame = (f) => rest.filter((i) => i.frame === f.id);\n const outside = rest.filter((i) => !i.frame || !frames.some((f) => f.id === i.frame));\n const [copied, setCopied] = useState(null);\n const copy = async () => {\n try {\n await navigator.clipboard.writeText(sketchToText(sketch));\n setCopied(\"Copied\");\n } catch {\n setCopied(\"Couldn't copy\");\n }\n setTimeout(() => setCopied(null), 1600);\n };\n const line = (it) => (it.kind === \"stamp\" ? (it.text ? `${it.stamp}: ${it.text}` : it.stamp) : it.kind === \"image\" ? \"Image\" : it.text);\n return (\n <div className=\"absolute inset-0 z-10 overflow-y-auto bg-background\">\n <div className=\"mx-auto grid max-w-2xl gap-6 px-4 py-6 sm:px-6\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <div>\n <h3 className=\"text-base font-semibold\">What the board says</h3>\n <p className=\"text-sm text-muted-foreground\">Every word on the board, top to bottom, grouped by frame.</p>\n </div>\n <div className=\"flex items-center gap-2\">\n {copied ? (\n <span role=\"status\" className=\"text-xs text-muted-foreground\">\n {copied}\n </span>\n ) : null}\n <Button size=\"sm\" icon=\"copy\" onClick={copy} disabled={!items.length}>\n Copy text\n </Button>\n </div>\n </div>\n {!items.length ? (\n <div className=\"grid justify-items-center gap-3 rounded-lg border border-dashed border-border px-4 py-10 text-center\">\n <p className=\"text-sm text-muted-foreground\">{readOnly ? \"Nothing on this board yet.\" : \"Nothing written on the board yet. Drawings without words don't show here.\"}</p>\n <Button size=\"sm\" icon=\"pencil\" onClick={onBoard}>\n Back to the board\n </Button>\n </div>\n ) : (\n <>\n {frames.map((f) => (\n <TextSection key={f.id} title={f.name} lines={inFrame(f).map(line).filter(Boolean)} />\n ))}\n {outside.length ? <TextSection title={frames.length ? \"Outside frames\" : null} lines={outside.map(line).filter(Boolean)} /> : null}\n </>\n )}\n </div>\n </div>\n );\n}\n\nfunction TextSection({ title, lines }) {\n return (\n <section className=\"grid gap-2\">\n {title ? <h4 className=\"text-sm font-semibold\">{title}</h4> : null}\n {lines.length ? (\n <ul className=\"grid gap-1.5\">\n {lines.map((l, i) => (\n <li key={i} className=\"rounded-md border border-border bg-card px-3 py-2 text-sm text-card-foreground\">\n {l}\n </li>\n ))}\n </ul>\n ) : (\n <p className=\"text-sm text-muted-foreground\">Empty.</p>\n )}\n </section>\n );\n}\n\n// ---- tasks ------------------------------------------------------------------\n\nconst clipTitle = (s) => {\n const t = String(s ?? \"\").replace(/\\s+/g, \" \").trim();\n return t.length > 200 ? `${t.slice(0, 199)}…` : t;\n};\n\n/** What a selected element could become a task from: a note (a shape with\n * words bound in it — selecting its words counts too) or a lone text.\n * { key: the element that carries the mark, ids: every element it came\n * from, text, x, y } or null. */\nfunction taskSource(el, byId) {\n let box = el;\n let words = null;\n if (el.type === \"text\") {\n words = el;\n if (el.containerId && byId.has(el.containerId)) box = byId.get(el.containerId);\n } else {\n const b = (el.boundElements ?? []).find((x) => x.type === \"text\");\n words = b ? byId.get(b.id) : null;\n }\n const text = String(words?.originalText ?? words?.text ?? \"\").replace(/\\s+/g, \" \").trim();\n if (!words || !text) return null;\n return { key: box.id, ids: box === words ? [words.id] : [box.id, words.id], text, x: box.x, y: box.y };\n}\n\nfunction isTask(src, byId) {\n return src.ids.some((id) => byId.get(id)?.customData?.task);\n}\n\n/** The small \"Task\" badges over converted notes, in screen px over the\n * canvas. Clickable (open the task) when the page gives `onOpenTask`;\n * otherwise they let the pointer through to the board. */\nfunction TaskMarks({ marks, onOpen }) {\n return (\n <div className=\"pointer-events-none absolute inset-0 z-[5] overflow-hidden\">\n {marks.map((m) => {\n const body = (\n <>\n <Icon name={m.pending ? \"loader\" : \"check-circle\"} size={12} className={cx(\"shrink-0\", m.pending ? \"motion-safe:animate-spin\" : \"text-success\")} />\n <span>{m.pending ? \"Creating…\" : \"Task\"}</span>\n </>\n );\n const place = { left: m.x, top: m.y };\n const look = \"absolute flex -translate-x-3/4 -translate-y-1/2 items-center gap-1 rounded-full border border-border bg-card px-2 py-0.5 text-[11px] leading-4 font-medium text-card-foreground shadow-sm\";\n return onOpen && m.id != null && !m.pending ? (\n // raw: a 20px badge pinned over a drawn note; a kit Button is 36px\n // and would cover the note it marks.\n <button\n key={m.key}\n type=\"button\"\n title={`Open task: ${m.title}`}\n aria-label={`Open task: ${m.title}`}\n onClick={() => onOpen({ id: m.id, title: m.title, sourceIds: [m.key] })}\n className={cx(look, \"pointer-events-auto cursor-pointer outline-none transition-colors duration-150 hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none\")}\n style={place}\n >\n {body}\n </button>\n ) : (\n <span key={m.key} title={m.title} className={look} style={place}>\n {body}\n </span>\n );\n })}\n </div>\n );\n}\n"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"path": "registry/blocks/whiteboard/templates.js",
|
|
27
|
+
"type": "registry:component",
|
|
28
|
+
"target": "@components/makersclaw/blocks/whiteboard/templates.js",
|
|
29
|
+
"content": "// The whiteboard's starter templates: plain functions that return a sketch\n// (hand-written elements, colours as theme TOKENS in customData.tokens —\n// sketch-canvas resolves them at run time). No Excalidraw import: they run\n// anywhere, the server and a node test included.\n//\n// A template is { id, label, description, create() }. `create()` returns a\n// sketch (or an element array) to lay on the empty board, or null for\n// \"start blank\". The board replaces the empty scene with it (one undo\n// step) and fits it in view.\n\nconst tok = (strokeColor, backgroundColor) => ({ tokens: { strokeColor, ...(backgroundColor ? { backgroundColor } : {}) } });\n\n/** A free text; Excalidraw re-measures it, so the size here is rough. */\nexport function textEl(id, x, y, text, { fontSize = 20, ink = \"foreground\", frameId = null, locked = false } = {}) {\n return {\n id,\n type: \"text\",\n x,\n y,\n width: Math.ceil(String(text).length * fontSize * 0.56),\n height: Math.ceil(fontSize * 1.25),\n text,\n originalText: text,\n fontSize,\n textAlign: \"left\",\n verticalAlign: \"top\",\n frameId,\n locked,\n customData: tok(ink),\n };\n}\n\n/** A titled frame: moving it moves what is inside. */\nexport function frameEl(id, x, y, width, height, name) {\n return { id, type: \"frame\", x, y, width, height, name, customData: tok(\"muted-foreground\") };\n}\n\n/** A plain box, clean-edged, filled with a token tint. */\nexport function boxEl(id, x, y, width, height, { fill = \"none\", ink = \"muted-foreground\", locked = false } = {}) {\n return {\n id,\n type: \"rectangle\",\n x,\n y,\n width,\n height,\n fillStyle: \"solid\",\n strokeWidth: 1,\n roughness: 0,\n roundness: null,\n locked,\n customData: tok(ink, fill === \"none\" ? undefined : fill),\n };\n}\n\n/** A straight arrow from (x, y) by (dx, dy). */\nexport function arrowEl(id, x, y, dx, dy, { ink = \"muted-foreground\", locked = false } = {}) {\n return {\n id,\n type: \"arrow\",\n x,\n y,\n width: Math.abs(dx),\n height: Math.abs(dy),\n points: [\n [0, 0],\n [dx, dy],\n ],\n strokeWidth: 2,\n roughness: 0,\n startArrowhead: null,\n endArrowhead: \"arrow\",\n locked,\n customData: tok(ink),\n };\n}\n\n/** Six titled frames, three by two: a board split into areas to fill. */\nexport function gridSketch({ columns = 3, rows = 2, width = 360, height = 260, gap = 48, names } = {}) {\n const els = [];\n let n = 0;\n for (let r = 0; r < rows; r++) {\n for (let c = 0; c < columns; c++) {\n n += 1;\n const name = names?.[n - 1] ?? `Area ${n}`;\n els.push(frameEl(`wb-grid-${n}`, c * (width + gap), r * (height + gap), width, height, name));\n }\n }\n return { elements: els, appState: {} };\n}\n\n/** A 2×2 matrix: four tinted quadrants (locked, so notes land on them\n * without moving them), two axes and editable quadrant names. */\nexport function matrixSketch({\n x = \"Effort\",\n y = \"Impact\",\n quadrants = [\"Quick wins\", \"Big bets\", \"Fill-ins\", \"Not now\"],\n size = 360,\n} = {}) {\n const q = size;\n const fills = [\"success:soft\", \"info:soft\", \"muted\", \"warning:soft\"];\n const at = [\n [0, 0],\n [q, 0],\n [0, q],\n [q, q],\n ];\n const els = [];\n at.forEach(([qx, qy], i) => {\n els.push(boxEl(`wb-mx-q${i + 1}`, qx, qy, q, q, { fill: fills[i], locked: true }));\n });\n at.forEach(([qx, qy], i) => {\n els.push(textEl(`wb-mx-t${i + 1}`, qx + 20, qy + 18, quadrants[i] ?? \"\", { fontSize: 24 }));\n });\n // The axes: up the left edge (y grows upward) and along the bottom.\n els.push(arrowEl(\"wb-mx-ay\", -28, 2 * q, 0, -2 * q, { locked: true }));\n els.push(arrowEl(\"wb-mx-ax\", 0, 2 * q + 28, 2 * q, 0, { locked: true }));\n els.push(textEl(\"wb-mx-ly\", -28, -44, `↑ ${y}`, { fontSize: 18, ink: \"muted-foreground\" }));\n els.push(textEl(\"wb-mx-lx\", 2 * q - 110, 2 * q + 44, `${x} →`, { fontSize: 18, ink: \"muted-foreground\" }));\n els.push(textEl(\"wb-mx-lo\", 0, 2 * q + 44, \"Low\", { fontSize: 14, ink: \"muted-foreground\" }));\n return { elements: els, appState: {} };\n}\n\n/** The three the board offers by default. */\nexport const WHITEBOARD_TEMPLATES = [\n { id: \"blank\", label: \"Blank\", description: \"An empty board to draw on\", create: () => null },\n { id: \"grid\", label: \"Grid\", description: \"Six titled areas, three by two\", create: () => gridSketch() },\n { id: \"matrix\", label: \"2×2 matrix\", description: \"Impact against effort, four quadrants\", create: () => matrixSketch() },\n];\n\n/** A file name from a board title: \"Q3 plan / draft\" → \"q3-plan-draft.png\". */\nexport function fileNameFor(title, ext = \"png\") {\n const base = String(title ?? \"\")\n .normalize(\"NFKD\")\n .replace(/[\\u0300-\\u036f]/g, \"\")\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 60);\n return `${base || \"whiteboard\"}.${ext}`;\n}\n\n/** A template's output → a sketch-shaped value, or null for blank. */\nexport function templateSketch(template) {\n const made = typeof template?.create === \"function\" ? template.create() : null;\n if (!made) return null;\n const elements = Array.isArray(made) ? made : Array.isArray(made.elements) ? made.elements : [];\n if (!elements.length) return null;\n return { type: \"sketch\", version: 1, elements, files: made.files ?? {}, appState: made.appState ?? {} };\n}\n"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"path": "registry/blocks/whiteboard/README.md",
|
|
33
|
+
"type": "registry:file",
|
|
34
|
+
"target": "@components/makersclaw/blocks/whiteboard/README.md",
|
|
35
|
+
"content": "# Whiteboard\n\nA full-page board to think on, friendly like Miro. A top bar holds the\nboard's title (click it to rename), the save state, a Board / Text view\nswitch, Export PNG and, when you give it `onShare`, a Share button. Under\nit is the canvas from `sketch-canvas`: the tool rail (a bottom bar on a\nphone), the toolbar that floats over a selection, zoom, undo and redo.\n\nGive it `onCreateTask` and a selected note or text becomes a task from\nthe top bar; the note keeps a small **Task** badge from then on.\n\nAn empty board offers three starting points: **Blank**, **Grid** (six\ntitled areas, three by two) and **2×2 matrix** (impact against effort,\nfour tinted quadrants with axes). The **Text view** lists every word on\nthe board, grouped by frame — the reading of the board for a screen\nreader, and what you hand an agent.\n\n```jsx\nimport { Whiteboard } from \"../components/blocks/whiteboard\"; // a Makersclaw app (\"./blocks/whiteboard\" from components/)\nimport { Whiteboard } from \"@/components/makersclaw/blocks/whiteboard\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component.\n`makersclaw add whiteboard` adds `sketch-canvas` with it (and its npm\npackage, `@excalidraw/excalidraw`: run `npm install` after). The helpers\nfor reading a board — `sketchToText`, `sketchItems`, `isSketchEmpty` —\ncome from `sketch-canvas`.\n\n## The data\n\nA board is two things you store: its **title** (a string) and its\n**sketch** (plain JSON, a `jsonb` column) — the sketch format is\n`sketch-canvas`'s (its README, \"The data\"). Colours in a sketch are theme\ntokens, so a board repaints when the workspace theme changes.\n\nOne table does it: `boards(id, title text, sketch jsonb, sketch_text text,\nupdated_at)`. Keep `sketch_text` (`sketchToText(sketch)`) for search and\nfor agents.\n\n## Props\n\n- `title` — the board's name, controlled. Change it when `onRename`\n answers ok.\n- `defaultTitle` — the name, uncontrolled (default \"Untitled board\").\n- `onRename(title)` — `async`, returns `{ ok: true }` or\n `{ ok: false, error? }`. Called when the reader renames the board (Enter\n or leaving the field; Esc puts the old name back). While it runs the\n new name shows; a failure keeps it and shows the error with Retry.\n Without it the title still edits, in memory.\n- `value` — the sketch, controlled (see sketch-canvas: pass back exactly\n what `onChange` gave you).\n- `defaultValue` — the sketch, uncontrolled. With neither, the board\n starts empty and shows the templates.\n- `onChange(sketch)` — the whole sketch after an edit settles (debounced).\n- `onSave(sketch)` — `async`, `{ ok }`. Called with every settled edit and\n on Cmd/Ctrl+S. The top bar shows \"Saving…\", \"Saved\", or the error with a\n Retry. A throw counts as a failure.\n- `changeDelay` — ms of quiet before `onChange` / `onSave` (default 600).\n- `templates` — the starting points an empty board offers (default\n `WHITEBOARD_TEMPLATES`: blank, grid, matrix). Each is\n `{ id, label, description?, create(), preview?, icon? }`: `create()`\n returns a sketch (or an element array) to lay on the board, or `null`\n for \"start blank\". `preview` is a small node drawn in the tile (else an\n `icon`, else a frame glyph; the three built-ins draw their own). `[]`\n shows sketch-canvas's plain hint instead.\n- `onShare()` — shows a Share button (primary) that calls it: open your\n `share-dialog`. Without it there is no Share button. `shareLabel`\n renames it (default \"Share\").\n- `onExport(blob, { title, fileName })` — `async`, `{ ok }`: take the PNG\n yourself (attach it, upload it). Without it, Export downloads\n `<title>.png`. Either way a failure shows on the button.\n- `onCreateTask({ title, sourceIds })` — turns notes into tasks. With it,\n selecting a note (a shape with words in it) or a text shows **Create\n task** in the top bar: a small form with the words as the title, ready\n to edit (Enter creates). Several selected notes make one task each.\n `async`, return `{ ok: true, id? }` or `{ ok: false, error? }` (a throw\n counts as failed). `sourceIds` are the board elements it came from (the\n note and its words). While it runs the note shows \"Creating…\"; on\n success the note keeps a **Task** badge (stored in the sketch, as\n `customData.task: { id, title }` on those elements) and is not offered\n again; a failure says so and leaves the note as it was. `taskLabel`\n renames the button (default \"Create task\").\n- `onOpenTask({ id, title, sourceIds })` — makes each Task badge (and the\n success toast) a link: open the task. Without it the badges only mark.\n- `onBack()` — shows a back arrow at the left of the top bar; `backLabel`\n is its name (default \"Back\").\n- `meta` — a short node beside the title on wide screens (\"Edited by Priya\n Raman\", \"View only\").\n- `readOnly` — `false` (default). `true`: the title is plain text, the\n canvas pans and zooms but does not edit, no templates.\n- `defaultView` — `\"board\"` (default) or `\"text\"`: which view opens.\n- `canvasRef` — a ref (`useSketchCanvas()` from sketch-canvas) that\n receives the canvas handle: `exportImage()`, `getScene()`,\n `insertElements()`… (sketch-canvas README, \"The handle\").\n- `className` — layout: width, margin, height. The default height is\n `h-[40rem]`; a height class of yours (`h-dvh` for a full page, `h-full`,\n `flex-1`) replaces it. Never colour.\n\n### Templates\n\n`WHITEBOARD_TEMPLATES` is the default list. `gridSketch(opts)` and\n`matrixSketch(opts)` build the two drawn ones, so your own templates can\nreuse them:\n\n- `gridSketch({ columns = 3, rows = 2, width = 360, height = 260, gap = 48, names })`\n — titled frames; `names` overrides \"Area 1\", \"Area 2\"…\n- `matrixSketch({ x = \"Effort\", y = \"Impact\", quadrants = [\"Quick wins\", \"Big bets\", \"Fill-ins\", \"Not now\"], size = 360 })`\n — four locked, tinted quadrants (notes land on them without moving\n them), two axes, the quadrant names as editable text.\n\n## Example\n\nA board page that saves itself:\n\n```jsx\n\"use client\";\nimport { useState } from \"react\";\nimport { Whiteboard } from \"./blocks/whiteboard\";\nimport { sketchToText } from \"./blocks/sketch-canvas\";\nimport { updateRow } from \"../lib/actions\";\n\nexport default function BoardPage({ board, onShare }) {\n const [title, setTitle] = useState(board.title);\n return (\n <Whiteboard\n title={title}\n onRename={async (next) => {\n const res = await updateRow({ source: \"public.boards\", id: board.id, patch: { title: next } });\n if (res.ok) setTitle(next);\n return res;\n }}\n defaultValue={board.sketch} // jsonb, may be null\n onSave={(sketch) => updateRow({ source: \"public.boards\", id: board.id, patch: { sketch, sketch_text: sketchToText(sketch) } })}\n onShare={onShare}\n className=\"h-dvh rounded-none border-0\"\n />\n );\n}\n```\n\nNotes that become tasks (a `tasks` table of your own):\n\n```jsx\n<Whiteboard\n defaultValue={board.sketch}\n onSave={save}\n onCreateTask={({ title, sourceIds }) => insertRow({ source: \"public.tasks\", values: { title, board_id: board.id, source_ids: sourceIds } })}\n onOpenTask={(task) => router.push(`/tasks/${task.id}`)}\n/>\n```\n\n`insertRow` answers `{ ok, id }`; the id is what the badge keeps.\n\nA retro template of your own, beside the built-in ones:\n\n```jsx\nimport { WHITEBOARD_TEMPLATES, gridSketch } from \"./blocks/whiteboard\";\n\nconst templates = [\n ...WHITEBOARD_TEMPLATES,\n { id: \"retro\", label: \"Retro\", description: \"Went well, to improve, actions\", icon: \"history\",\n create: () => gridSketch({ columns: 3, rows: 1, height: 520, names: [\"Went well\", \"To improve\", \"Actions\"] }) },\n];\n\n<Whiteboard templates={templates} onSave={save} />\n```\n\n## What it will not do\n\n- Collaborate live: no cursors of others, saves are last-write-wins (as\n sketch-canvas).\n- Share by itself: `onShare` is yours — open `share-dialog`, copy a link.\n- Keep a list of boards: one board per component; the list is your page.\n- Turn on snap-to-grid from a template: the canvas menu has it.\n- Show a minimap (sketch-canvas has none).\n\nA board 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 whiteboard` restores them.\n"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"categories": [
|
|
39
|
+
"whiteboard",
|
|
40
|
+
"board",
|
|
41
|
+
"sketch",
|
|
42
|
+
"draw",
|
|
43
|
+
"drawing",
|
|
44
|
+
"canvas",
|
|
45
|
+
"excalidraw",
|
|
46
|
+
"miro",
|
|
47
|
+
"brainstorm",
|
|
48
|
+
"matrix",
|
|
49
|
+
"2x2",
|
|
50
|
+
"grid",
|
|
51
|
+
"template",
|
|
52
|
+
"export",
|
|
53
|
+
"png",
|
|
54
|
+
"share",
|
|
55
|
+
"task",
|
|
56
|
+
"create task",
|
|
57
|
+
"action items"
|
|
58
|
+
],
|
|
59
|
+
"docs": "Installed to components/makersclaw/blocks/whiteboard. Import: import { Whiteboard, WHITEBOARD_TEMPLATES, gridSketch, matrixSketch } from \"@/components/makersclaw/blocks/whiteboard\". Props and an example: components/makersclaw/blocks/whiteboard/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
60
|
+
"meta": {
|
|
61
|
+
"makersclaw": {
|
|
62
|
+
"version": "0.3.1",
|
|
63
|
+
"useWhen": "A screen is one board to think on: a team whiteboard, a planning or prioritisation board, a place to sketch an idea and keep it with a record. Not for marking up a screenshot (annotate), sketching a UI to build (sketch-to-app), a structured board of cards and connectors (flow-canvas), or a kanban (board)."
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Annotate
|
|
2
|
+
|
|
3
|
+
Mark up a screenshot and say what each mark means. Drop a screenshot on
|
|
4
|
+
it, pick one, or paste one (Cmd/Ctrl+V). It lies locked at the back of the
|
|
5
|
+
canvas, and on top you draw: numbered notes (pins, <kbd>N</kbd>), arrows,
|
|
6
|
+
boxes, ellipses, freehand, text, a highlight band, and a solid bar to hide
|
|
7
|
+
something private. Every numbered pin has a row in the Notes list beside
|
|
8
|
+
the canvas, where its words are typed; a click on a row's number shows its
|
|
9
|
+
pin. A fresh screenshot offers three starters — Bug report, Design review,
|
|
10
|
+
Walkthrough — that lay pins (and a box or arrows) with prompts in their
|
|
11
|
+
rows. A top bar holds the title, the note count, the save state, Replace,
|
|
12
|
+
Start over and the send button.
|
|
13
|
+
|
|
14
|
+
It is `sketch-canvas` underneath (Excalidraw, in the workspace theme): its
|
|
15
|
+
tool rail, selection toolbar, zoom, undo and PNG export.
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
import { Annotate, annotationToText } from "../components/blocks/annotate"; // a Makersclaw app ("./blocks/annotate" from components/)
|
|
19
|
+
import { Annotate, annotationToText } from "@/components/makersclaw/blocks/annotate"; // a shadcn project
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`Annotate` is a client component: render it from a `"use client"`
|
|
23
|
+
component. The helpers are plain functions and work on the server.
|
|
24
|
+
`makersclaw add annotate` adds `sketch-canvas` with it (and Excalidraw:
|
|
25
|
+
run `npm install` after).
|
|
26
|
+
|
|
27
|
+
## The data
|
|
28
|
+
|
|
29
|
+
An annotation is plain JSON. Store it as it comes out of `onChange` /
|
|
30
|
+
`onSave` (a `jsonb` column).
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
{
|
|
34
|
+
type: "annotation",
|
|
35
|
+
version: 1,
|
|
36
|
+
image: { src: "data:image/png;base64,…", width: 1280, height: 800 }, // or an https URL; null before one is added
|
|
37
|
+
sketch: { type: "sketch", version: 1, elements: [ … ], files: {}, appState: {} }, // sketch-canvas's value: the marks
|
|
38
|
+
notes: [
|
|
39
|
+
{ n: 1, text: "The total is wrong here" },
|
|
40
|
+
{ n: 2, text: "", hint: "What did you expect instead?" }, // hint: a starter's prompt, shown while empty
|
|
41
|
+
],
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- **A pin is the truth for which notes exist.** A pin is an ellipse in the
|
|
46
|
+
sketch whose `customData.note` is its number, with the number as its
|
|
47
|
+
label. `notes` lists one entry per pin on the sketch, by number. Delete
|
|
48
|
+
a pin on the canvas and its entry is gone from the next `onChange`; the
|
|
49
|
+
words stay in memory, so an undo brings both back.
|
|
50
|
+
- **Numbers are stable.** Deleting 2 of 1, 2, 3 leaves 1 and 3, and the
|
|
51
|
+
next pin is 4. A pin duplicated (Cmd+D) or pasted gets the next free
|
|
52
|
+
number.
|
|
53
|
+
- **The screenshot is a data URL** unless you pass `uploadImage`. It is
|
|
54
|
+
scaled down to `maxImageSize` on its long side first (1600 px: about
|
|
55
|
+
0.5–2 MB as a PNG). To keep images out of the row, upload the file and
|
|
56
|
+
store the URL (see `uploadImage`).
|
|
57
|
+
- Colours in the marks are theme tokens (see sketch-canvas's README): a
|
|
58
|
+
saved annotation repaints with the theme.
|
|
59
|
+
|
|
60
|
+
## Props
|
|
61
|
+
|
|
62
|
+
- `value` — the annotation, controlled. A new `value` that is not what the
|
|
63
|
+
block last sent loads it (another record). Pass back exactly what
|
|
64
|
+
`onChange` gave you and nothing resets.
|
|
65
|
+
- `defaultValue` — the starting annotation, uncontrolled. `{ image }`
|
|
66
|
+
alone opens a screenshot with nothing on it. With neither, the block
|
|
67
|
+
starts at the drop zone.
|
|
68
|
+
- `onChange(annotation)` — the whole annotation after an edit settles: a
|
|
69
|
+
mark (250 ms after the canvas goes quiet), a note's words (500 ms after
|
|
70
|
+
typing stops), a new screenshot (at once). Not called for pan, zoom or
|
|
71
|
+
selection.
|
|
72
|
+
- `onSave(annotation)` — `async`, returns `{ ok: true }` or
|
|
73
|
+
`{ ok: false, error? }`. Called right after `onChange`. The top bar
|
|
74
|
+
shows "Saving…", "Saved", or the error with Retry. A throw counts as a
|
|
75
|
+
failure.
|
|
76
|
+
- `onSubmit({ image, notes, sketch, screenshot, text })` — `async`,
|
|
77
|
+
`{ ok }`. Without it there is no send button. `image` is a PNG `Blob`
|
|
78
|
+
of the screenshot with the marks (2× scale); `notes` is
|
|
79
|
+
`[{ n, text, hint?, x, y }]` (x, y: the pin's centre in screenshot
|
|
80
|
+
pixels); `sketch` the marks; `screenshot` the `{ src, width, height }`
|
|
81
|
+
underneath; `text` is `annotationToText` of it all. The bar shows
|
|
82
|
+
"Sent" or the error.
|
|
83
|
+
- `submitLabel` — the send button's words (default "Send"): "File the
|
|
84
|
+
bug", "Send to support", "Ask the agent".
|
|
85
|
+
- `uploadImage(file)` — `async`, returns `{ ok: true, src }` or
|
|
86
|
+
`{ ok: false, error? }`. With it, a dropped or picked screenshot is
|
|
87
|
+
uploaded as it is and `image.src` is the URL you return (same origin,
|
|
88
|
+
or `exportImage` cannot read it back). Without it, the file becomes a
|
|
89
|
+
data URL, scaled to `maxImageSize`.
|
|
90
|
+
- `maxImageSize` — px on the long side a screenshot is scaled down to when
|
|
91
|
+
it becomes a data URL (default 1600; `0` keeps it as it is).
|
|
92
|
+
- `title` — the top bar's heading and the region's name (default
|
|
93
|
+
"Annotate a screenshot").
|
|
94
|
+
- `readOnly` — `false` (default). `true`: the marks to look at (pan and
|
|
95
|
+
zoom), the notes as text; no rail, no drop, no send.
|
|
96
|
+
- `tone` — the pins' colour, a token: `destructive` (default), `primary`,
|
|
97
|
+
`warning`, `success`, `info`, `chart-2` … `chart-5`.
|
|
98
|
+
- `tools` — the canvas tools in the rail (sketch-canvas ids). Default
|
|
99
|
+
`ANNOTATE_TOOLS`: select, pan, arrow, rectangle, ellipse, draw, text,
|
|
100
|
+
eraser. The three annotate stamps (numbered note, highlight, hide) always
|
|
101
|
+
follow them.
|
|
102
|
+
- `canvasRef` — a ref (`useSketchCanvas()` from sketch-canvas) that
|
|
103
|
+
receives the canvas's handle: `exportImage()`, `getScene()`,
|
|
104
|
+
`zoomToFit()`…
|
|
105
|
+
- `label` — the region's accessible name (default: `title`).
|
|
106
|
+
- `className` — layout: width, margin, height. From 768 px the default
|
|
107
|
+
height is `h-[36rem]` (canvas and notes side by side); a height class of
|
|
108
|
+
yours replaces it. Below that the notes stack under a 26rem canvas.
|
|
109
|
+
Never colour.
|
|
110
|
+
|
|
111
|
+
### Keys
|
|
112
|
+
|
|
113
|
+
On the canvas: <kbd>N</kbd> a numbered note (click to place it; Enter
|
|
114
|
+
drops it in the middle), <kbd>A</kbd> arrow, <kbd>R</kbd> rectangle,
|
|
115
|
+
<kbd>O</kbd> ellipse, <kbd>P</kbd> draw, <kbd>T</kbd> text, <kbd>E</kbd>
|
|
116
|
+
eraser, <kbd>V</kbd> select, <kbd>H</kbd> pan, Delete, Cmd/Ctrl+D, Cmd/Ctrl+Z,
|
|
117
|
+
Shift+1 fit. "Add note" in the Notes list drops a pin in the middle of the
|
|
118
|
+
view without the mouse.
|
|
119
|
+
|
|
120
|
+
### Helpers
|
|
121
|
+
|
|
122
|
+
Plain functions; they take an annotation (or a sketch) and never throw on
|
|
123
|
+
a partial one:
|
|
124
|
+
|
|
125
|
+
- `emptyAnnotation()`, `toAnnotation(value)` — the shape above.
|
|
126
|
+
- `annotationNotes(sketch, notes)` — `[{ n, text, hint?, x, y }]`: one per
|
|
127
|
+
pin, by number.
|
|
128
|
+
- `annotationToText(annotation)` — the words, for an agent or a reader:
|
|
129
|
+
`"1. The total is wrong here\n2. …"`, then any free text drawn on it
|
|
130
|
+
under "Also written on it:".
|
|
131
|
+
- `noteMarkers(sketch)` — the pins: `[{ n, id, x, y, width, height }]`.
|
|
132
|
+
- `nextNoteNumber(sketch)` — the number the next pin gets.
|
|
133
|
+
- `markerSkeleton(n, { size, tone })` — a pin as sketch-canvas skeletons,
|
|
134
|
+
for `canvasRef.current.insertElements` (a pin you place from code).
|
|
135
|
+
- `ANNOTATE_TEMPLATES`, `ANNOTATE_TOOLS` — the starters and the default
|
|
136
|
+
tools.
|
|
137
|
+
|
|
138
|
+
## Example
|
|
139
|
+
|
|
140
|
+
A feedback widget that saves itself and files a ticket:
|
|
141
|
+
|
|
142
|
+
```jsx
|
|
143
|
+
"use client";
|
|
144
|
+
import { Annotate } from "./blocks/annotate";
|
|
145
|
+
import { insertRow, updateRow } from "../lib/actions";
|
|
146
|
+
|
|
147
|
+
export default function Feedback({ ticket }) {
|
|
148
|
+
return (
|
|
149
|
+
<Annotate
|
|
150
|
+
title="What should change?"
|
|
151
|
+
defaultValue={ticket.annotation} // jsonb, may be null
|
|
152
|
+
onSave={(annotation) => updateRow({ source: "public.tickets", id: ticket.id, patch: { annotation } })}
|
|
153
|
+
onSubmit={({ notes, text }) => // image: a PNG Blob, to upload if you keep it
|
|
154
|
+
insertRow({ source: "public.bug_reports", values: { ticket_id: ticket.id, summary: text, note_count: notes.length } })
|
|
155
|
+
}
|
|
156
|
+
submitLabel="File the bug"
|
|
157
|
+
className="h-dvh"
|
|
158
|
+
/>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## What it will not do
|
|
164
|
+
|
|
165
|
+
- Crop, rotate or resize the screenshot. Replace swaps it for another;
|
|
166
|
+
the marks stay where they were.
|
|
167
|
+
- Annotate several screenshots at once: one image per annotation (keep a
|
|
168
|
+
list of annotations for a series).
|
|
169
|
+
- Renumber pins after a delete — numbers stay what they were.
|
|
170
|
+
- Blur: "hide" is a solid bar drawn over the image. The original pixels
|
|
171
|
+
are still in `image.src`; send the exported PNG, not the source, when
|
|
172
|
+
what is hidden must not leave.
|
|
173
|
+
- Take a screenshot for you: the browser cannot capture the page without
|
|
174
|
+
asking; the person drops or pastes one.
|
|
175
|
+
- Collaborate live: saves are last-write-wins.
|
|
176
|
+
|
|
177
|
+
A markup tool it cannot be is your own component in `components/` —
|
|
178
|
+
not an edit to this block. Its files are fixed: `npm run check` refuses a
|
|
179
|
+
change, and `makersclaw add annotate` restores them.
|