makersclaw 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +41 -1
- package/THIRD-PARTY-NOTICES.md +180 -0
- package/package.json +15 -8
- package/r/annotate.json +62 -0
- package/r/approval-request.json +52 -0
- package/r/audio-player.json +74 -0
- package/r/auth-form.json +55 -0
- package/r/board.json +55 -0
- package/r/calendar-view.json +105 -0
- package/r/cart.json +62 -0
- package/r/chart.json +83 -0
- package/r/chat.json +95 -0
- package/r/code-editor.json +94 -0
- package/r/comment-box.json +64 -0
- package/r/contact-section.json +53 -0
- package/r/core.json +341 -0
- package/r/csv-import.json +63 -0
- package/r/data-table.json +69 -0
- package/r/diagram.json +74 -0
- package/r/doc-editor.json +82 -0
- package/r/doc-page.json +61 -0
- package/r/email-render.json +73 -0
- package/r/faq.json +50 -0
- package/r/file-upload.json +54 -0
- package/r/flow-canvas.json +93 -0
- package/r/footer.json +52 -0
- package/r/gantt.json +70 -0
- package/r/job-panel.json +56 -0
- package/r/json-editor.json +66 -0
- package/r/map.json +89 -0
- package/r/menubar.json +52 -0
- package/r/mind-map.json +67 -0
- package/r/navigation-menu.json +52 -0
- package/r/order-detail.json +54 -0
- package/r/org-chart.json +66 -0
- package/r/page-builder.json +105 -0
- package/r/payment-methods.json +51 -0
- package/r/pdf-document.json +79 -0
- package/r/pdf-viewer.json +88 -0
- package/r/product-detail.json +76 -0
- package/r/product-grid.json +65 -0
- package/r/record-form.json +67 -0
- package/r/record-notes.json +55 -0
- package/r/registry.json +3612 -0
- package/r/search-dialog.json +51 -0
- package/r/settings-list.json +66 -0
- package/r/setup-checklist.json +51 -0
- package/r/share-dialog.json +52 -0
- package/r/sidebar.json +69 -0
- package/r/signature-pad.json +72 -0
- package/r/sketch-canvas.json +99 -0
- package/r/spreadsheet.json +90 -0
- package/r/stat-cards.json +66 -0
- package/r/task-list.json +63 -0
- package/r/task-model.json +98 -0
- package/r/testimonials.json +52 -0
- package/r/transaction-list.json +53 -0
- package/r/whiteboard.json +66 -0
- package/registry/blocks/annotate/README.md +179 -0
- package/registry/blocks/annotate/annotate-data.js +250 -0
- package/registry/blocks/annotate/annotate.jsx +646 -0
- package/registry/blocks/annotate/block.json +10 -0
- package/registry/blocks/annotate/demo.jsx +135 -0
- package/registry/blocks/annotate/index.js +15 -0
- package/registry/blocks/approval-request/README.md +116 -0
- package/registry/blocks/approval-request/approval-request.jsx +386 -0
- package/registry/blocks/approval-request/block.json +10 -0
- package/registry/blocks/approval-request/demo.jsx +127 -0
- package/registry/blocks/approval-request/index.js +4 -0
- package/registry/blocks/audio-player/README.md +167 -0
- package/registry/blocks/audio-player/audio-player.jsx +623 -0
- package/registry/blocks/audio-player/block.json +14 -0
- package/registry/blocks/audio-player/colors.js +109 -0
- package/registry/blocks/audio-player/demo.jsx +209 -0
- package/registry/blocks/audio-player/index.js +6 -0
- package/registry/blocks/audio-player/time.js +67 -0
- package/registry/blocks/audio-player/wavesurfer.js +9 -0
- package/registry/blocks/auth-form/README.md +126 -0
- package/registry/blocks/auth-form/auth-form.jsx +568 -0
- package/registry/blocks/auth-form/block.json +10 -0
- package/registry/blocks/auth-form/demo.jsx +131 -0
- package/registry/blocks/auth-form/index.js +3 -0
- package/registry/blocks/board/README.md +64 -13
- package/registry/blocks/board/block.json +2 -2
- package/registry/blocks/board/board.jsx +237 -94
- package/registry/blocks/board/demo.jsx +103 -24
- package/registry/blocks/calendar-view/README.md +180 -0
- package/registry/blocks/calendar-view/agenda.jsx +147 -0
- package/registry/blocks/calendar-view/block.json +76 -0
- package/registry/blocks/calendar-view/calendar-model.js +442 -0
- package/registry/blocks/calendar-view/calendar.jsx +627 -0
- package/registry/blocks/calendar-view/demo.jsx +195 -0
- package/registry/blocks/calendar-view/draft.jsx +49 -0
- package/registry/blocks/calendar-view/index.js +39 -0
- package/registry/blocks/calendar-view/month.jsx +346 -0
- package/registry/blocks/calendar-view/paint.js +69 -0
- package/registry/blocks/calendar-view/time-grid.jsx +527 -0
- package/registry/blocks/calendar-view/tray.jsx +87 -0
- package/registry/blocks/cart/README.md +146 -0
- package/registry/blocks/cart/block.json +10 -0
- package/registry/blocks/cart/cart.jsx +597 -0
- package/registry/blocks/cart/demo.jsx +98 -0
- package/registry/blocks/cart/index.js +4 -0
- package/registry/blocks/cart/money.js +28 -0
- package/registry/blocks/chart/README.md +316 -0
- package/registry/blocks/chart/block.json +10 -0
- package/registry/blocks/chart/cartesian.jsx +642 -0
- package/registry/blocks/chart/core.jsx +262 -0
- package/registry/blocks/chart/demo.jsx +357 -0
- package/registry/blocks/chart/heatmap.jsx +336 -0
- package/registry/blocks/chart/index.js +8 -0
- package/registry/blocks/chart/pie.jsx +245 -0
- package/registry/blocks/chart/sparkline.jsx +88 -0
- package/registry/blocks/chat/README.md +273 -0
- package/registry/blocks/chat/attachment.jsx +95 -0
- package/registry/blocks/chat/block.json +10 -0
- package/registry/blocks/chat/chat.jsx +132 -0
- package/registry/blocks/chat/composer.jsx +364 -0
- package/registry/blocks/chat/demo.jsx +578 -0
- package/registry/blocks/chat/header.jsx +34 -0
- package/registry/blocks/chat/index.js +9 -0
- package/registry/blocks/chat/message.jsx +450 -0
- package/registry/blocks/chat/scroller.jsx +247 -0
- package/registry/blocks/chat/text.jsx +104 -0
- package/registry/blocks/code-editor/README.md +210 -0
- package/registry/blocks/code-editor/block.json +22 -0
- package/registry/blocks/code-editor/code-editor.jsx +534 -0
- package/registry/blocks/code-editor/codemirror.js +38 -0
- package/registry/blocks/code-editor/demo.jsx +205 -0
- package/registry/blocks/code-editor/index.js +8 -0
- package/registry/blocks/code-editor/json.js +317 -0
- package/registry/blocks/code-editor/sql.js +136 -0
- package/registry/blocks/code-editor/text.js +101 -0
- package/registry/blocks/code-editor/theme.js +186 -0
- package/registry/blocks/comment-box/README.md +123 -0
- package/registry/blocks/comment-box/block.json +10 -0
- package/registry/blocks/comment-box/comment-body.jsx +217 -0
- package/registry/blocks/comment-box/comment-box.jsx +198 -0
- package/registry/blocks/comment-box/comment-time.jsx +77 -0
- package/registry/blocks/comment-box/demo.jsx +141 -0
- package/registry/blocks/comment-box/index.js +8 -0
- package/registry/blocks/contact-section/README.md +152 -0
- package/registry/blocks/contact-section/block.json +11 -0
- package/registry/blocks/contact-section/contact-section.jsx +509 -0
- package/registry/blocks/contact-section/demo.jsx +93 -0
- package/registry/blocks/contact-section/index.js +4 -0
- package/registry/blocks/csv-import/README.md +133 -0
- package/registry/blocks/csv-import/block.json +13 -0
- package/registry/blocks/csv-import/csv-import.jsx +479 -0
- package/registry/blocks/csv-import/demo.jsx +106 -0
- package/registry/blocks/csv-import/import.js +200 -0
- package/registry/blocks/csv-import/index.js +7 -0
- package/registry/blocks/data-table/README.md +386 -0
- package/registry/blocks/data-table/block.json +10 -0
- package/registry/blocks/data-table/data-table.jsx +1164 -0
- package/registry/blocks/data-table/demo.jsx +588 -0
- package/registry/blocks/data-table/index.js +5 -0
- package/registry/blocks/data-table/model.js +169 -0
- package/registry/blocks/diagram/README.md +108 -0
- package/registry/blocks/diagram/block.json +13 -0
- package/registry/blocks/diagram/demo.jsx +113 -0
- package/registry/blocks/diagram/diagram.jsx +481 -0
- package/registry/blocks/diagram/index.js +7 -0
- package/registry/blocks/diagram/mermaid.js +52 -0
- package/registry/blocks/diagram/palette.js +313 -0
- package/registry/blocks/diagram/source.js +98 -0
- package/registry/blocks/doc-editor/README.md +215 -0
- package/registry/blocks/doc-editor/block.json +15 -0
- package/registry/blocks/doc-editor/demo.jsx +225 -0
- package/registry/blocks/doc-editor/doc-editor.jsx +291 -0
- package/registry/blocks/doc-editor/doc.js +309 -0
- package/registry/blocks/doc-editor/index.js +7 -0
- package/registry/blocks/doc-editor/schema.jsx +56 -0
- package/registry/blocks/doc-editor/shadcn-classes.js +110 -0
- package/registry/blocks/doc-editor/theme.js +82 -0
- package/registry/blocks/doc-page/README.md +119 -0
- package/registry/blocks/doc-page/autosave.js +104 -0
- package/registry/blocks/doc-page/block.json +10 -0
- package/registry/blocks/doc-page/demo.jsx +137 -0
- package/registry/blocks/doc-page/doc-page.jsx +276 -0
- package/registry/blocks/doc-page/index.js +4 -0
- package/registry/blocks/email-render/README.md +151 -0
- package/registry/blocks/email-render/block.json +14 -0
- package/registry/blocks/email-render/demo.jsx +153 -0
- package/registry/blocks/email-render/doc-email.jsx +210 -0
- package/registry/blocks/email-render/email.jsx +221 -0
- package/registry/blocks/email-render/index.js +10 -0
- package/registry/blocks/email-render/render.js +50 -0
- package/registry/blocks/email-render/theme.js +215 -0
- package/registry/blocks/faq/README.md +101 -0
- package/registry/blocks/faq/block.json +11 -0
- package/registry/blocks/faq/demo.jsx +89 -0
- package/registry/blocks/faq/faq.jsx +220 -0
- package/registry/blocks/faq/index.js +4 -0
- package/registry/blocks/file-upload/README.md +138 -0
- package/registry/blocks/file-upload/block.json +10 -0
- package/registry/blocks/file-upload/demo.jsx +204 -0
- package/registry/blocks/file-upload/file-upload.jsx +635 -0
- package/registry/blocks/file-upload/index.js +4 -0
- package/registry/blocks/flow-canvas/README.md +420 -0
- package/registry/blocks/flow-canvas/block.json +13 -0
- package/registry/blocks/flow-canvas/board.js +819 -0
- package/registry/blocks/flow-canvas/chrome.jsx +649 -0
- package/registry/blocks/flow-canvas/context.jsx +35 -0
- package/registry/blocks/flow-canvas/demo.jsx +249 -0
- package/registry/blocks/flow-canvas/edges.jsx +107 -0
- package/registry/blocks/flow-canvas/flow-canvas.jsx +1329 -0
- package/registry/blocks/flow-canvas/index.js +41 -0
- package/registry/blocks/flow-canvas/nodes.jsx +392 -0
- package/registry/blocks/flow-canvas/theme.js +174 -0
- package/registry/blocks/footer/README.md +133 -0
- package/registry/blocks/footer/block.json +11 -0
- package/registry/blocks/footer/demo.jsx +119 -0
- package/registry/blocks/footer/footer.jsx +406 -0
- package/registry/blocks/footer/index.js +4 -0
- package/registry/blocks/gantt/README.md +220 -0
- package/registry/blocks/gantt/block.json +10 -0
- package/registry/blocks/gantt/demo.jsx +148 -0
- package/registry/blocks/gantt/gantt.jsx +923 -0
- package/registry/blocks/gantt/index.js +7 -0
- package/registry/blocks/gantt/rows.jsx +468 -0
- package/registry/blocks/gantt/schedule.js +278 -0
- package/registry/blocks/job-panel/README.md +86 -2
- package/registry/blocks/job-panel/block.json +3 -3
- package/registry/blocks/job-panel/demo.jsx +230 -3
- package/registry/blocks/job-panel/job-panel.jsx +169 -3
- package/registry/blocks/json-editor/README.md +133 -0
- package/registry/blocks/json-editor/block.json +10 -0
- package/registry/blocks/json-editor/demo.jsx +92 -0
- package/registry/blocks/json-editor/index.js +7 -0
- package/registry/blocks/json-editor/json-editor.jsx +305 -0
- package/registry/blocks/json-editor/json-tree.jsx +393 -0
- package/registry/blocks/json-editor/tree.js +225 -0
- package/registry/blocks/map/README.md +221 -0
- package/registry/blocks/map/block.json +14 -0
- package/registry/blocks/map/demo.jsx +201 -0
- package/registry/blocks/map/geo.js +198 -0
- package/registry/blocks/map/index.js +8 -0
- package/registry/blocks/map/list.jsx +128 -0
- package/registry/blocks/map/map-view.jsx +715 -0
- package/registry/blocks/map/maplibre.js +30 -0
- package/registry/blocks/map/marks.jsx +146 -0
- package/registry/blocks/map/theme.js +142 -0
- package/registry/blocks/menubar/README.md +109 -0
- package/registry/blocks/menubar/block.json +10 -0
- package/registry/blocks/menubar/demo.jsx +128 -0
- package/registry/blocks/menubar/index.js +4 -0
- package/registry/blocks/menubar/menubar.jsx +199 -0
- package/registry/blocks/mind-map/README.md +206 -0
- package/registry/blocks/mind-map/block.json +10 -0
- package/registry/blocks/mind-map/demo.jsx +103 -0
- package/registry/blocks/mind-map/index.js +31 -0
- package/registry/blocks/mind-map/mind-map.jsx +1125 -0
- package/registry/blocks/mind-map/topic.jsx +143 -0
- package/registry/blocks/mind-map/tree.js +645 -0
- package/registry/blocks/navigation-menu/README.md +124 -0
- package/registry/blocks/navigation-menu/block.json +10 -0
- package/registry/blocks/navigation-menu/demo.jsx +84 -0
- package/registry/blocks/navigation-menu/index.js +4 -0
- package/registry/blocks/navigation-menu/navigation-menu.jsx +448 -0
- package/registry/blocks/order-detail/README.md +109 -0
- package/registry/blocks/order-detail/block.json +10 -0
- package/registry/blocks/order-detail/demo.jsx +88 -0
- package/registry/blocks/order-detail/index.js +4 -0
- package/registry/blocks/order-detail/order-detail.jsx +354 -0
- package/registry/blocks/org-chart/README.md +145 -0
- package/registry/blocks/org-chart/block.json +10 -0
- package/registry/blocks/org-chart/demo.jsx +94 -0
- package/registry/blocks/org-chart/index.js +7 -0
- package/registry/blocks/org-chart/org-chart.jsx +631 -0
- package/registry/blocks/org-chart/org.js +245 -0
- package/registry/blocks/org-chart/person.jsx +96 -0
- package/registry/blocks/page-builder/README.md +257 -0
- package/registry/blocks/page-builder/block.json +14 -0
- package/registry/blocks/page-builder/demo.jsx +145 -0
- package/registry/blocks/page-builder/grapes.js +18 -0
- package/registry/blocks/page-builder/index.js +11 -0
- package/registry/blocks/page-builder/page-builder.jsx +704 -0
- package/registry/blocks/page-builder/page-view.jsx +33 -0
- package/registry/blocks/page-builder/page.js +118 -0
- package/registry/blocks/page-builder/panels.jsx +588 -0
- package/registry/blocks/page-builder/sections.js +209 -0
- package/registry/blocks/page-builder/theme.js +87 -0
- package/registry/blocks/page-builder/thumbs.jsx +189 -0
- package/registry/blocks/page-builder/tokens.js +329 -0
- package/registry/blocks/payment-methods/README.md +104 -0
- package/registry/blocks/payment-methods/block.json +10 -0
- package/registry/blocks/payment-methods/demo.jsx +71 -0
- package/registry/blocks/payment-methods/index.js +4 -0
- package/registry/blocks/payment-methods/payment-methods.jsx +302 -0
- package/registry/blocks/pdf-document/README.md +213 -0
- package/registry/blocks/pdf-document/block.json +13 -0
- package/registry/blocks/pdf-document/demo.jsx +136 -0
- package/registry/blocks/pdf-document/index.js +33 -0
- package/registry/blocks/pdf-document/money.js +83 -0
- package/registry/blocks/pdf-document/pdf-document.jsx +144 -0
- package/registry/blocks/pdf-document/primitives.jsx +271 -0
- package/registry/blocks/pdf-document/quote.jsx +179 -0
- package/registry/blocks/pdf-document/theme.js +193 -0
- package/registry/blocks/pdf-viewer/README.md +191 -0
- package/registry/blocks/pdf-viewer/block.json +14 -0
- package/registry/blocks/pdf-viewer/demo.jsx +252 -0
- package/registry/blocks/pdf-viewer/helpers.js +270 -0
- package/registry/blocks/pdf-viewer/index.js +8 -0
- package/registry/blocks/pdf-viewer/notes.jsx +195 -0
- package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1368 -0
- package/registry/blocks/pdf-viewer/pdfjs.js +37 -0
- package/registry/blocks/pdf-viewer/ssr-shim-end.js +6 -0
- package/registry/blocks/pdf-viewer/ssr-shim.js +44 -0
- package/registry/blocks/product-detail/README.md +217 -0
- package/registry/blocks/product-detail/block.json +10 -0
- package/registry/blocks/product-detail/demo.jsx +233 -0
- package/registry/blocks/product-detail/index.js +7 -0
- package/registry/blocks/product-detail/price.jsx +115 -0
- package/registry/blocks/product-detail/product-detail.jsx +348 -0
- package/registry/blocks/product-detail/product-media.jsx +102 -0
- package/registry/blocks/product-detail/product-options.jsx +144 -0
- package/registry/blocks/product-grid/README.md +162 -0
- package/registry/blocks/product-grid/block.json +10 -0
- package/registry/blocks/product-grid/demo.jsx +255 -0
- package/registry/blocks/product-grid/index.js +5 -0
- package/registry/blocks/product-grid/product-card.jsx +232 -0
- package/registry/blocks/product-grid/product-grid.jsx +235 -0
- package/registry/blocks/record-form/README.md +196 -0
- package/registry/blocks/record-form/block.json +10 -0
- package/registry/blocks/record-form/demo.jsx +525 -0
- package/registry/blocks/record-form/index.js +4 -0
- package/registry/blocks/record-form/record-fields.jsx +457 -0
- package/registry/blocks/record-form/record-form.jsx +415 -0
- package/registry/blocks/record-form/rules.js +81 -0
- package/registry/blocks/record-notes/README.md +139 -0
- package/registry/blocks/record-notes/block.json +10 -0
- package/registry/blocks/record-notes/demo.jsx +179 -0
- package/registry/blocks/record-notes/index.js +4 -0
- package/registry/blocks/record-notes/record-notes.jsx +266 -0
- package/registry/blocks/search-dialog/README.md +108 -0
- package/registry/blocks/search-dialog/block.json +10 -0
- package/registry/blocks/search-dialog/demo.jsx +116 -0
- package/registry/blocks/search-dialog/index.js +3 -0
- package/registry/blocks/search-dialog/search-dialog.jsx +235 -0
- package/registry/blocks/settings-list/README.md +171 -0
- package/registry/blocks/settings-list/block.json +10 -0
- package/registry/blocks/settings-list/demo.jsx +333 -0
- package/registry/blocks/settings-list/index.js +4 -0
- package/registry/blocks/settings-list/row-state.jsx +93 -0
- package/registry/blocks/settings-list/settings-list.jsx +349 -0
- package/registry/blocks/settings-list/settings-matrix.jsx +118 -0
- package/registry/blocks/setup-checklist/README.md +142 -0
- package/registry/blocks/setup-checklist/block.json +10 -0
- package/registry/blocks/setup-checklist/demo.jsx +159 -0
- package/registry/blocks/setup-checklist/index.js +4 -0
- package/registry/blocks/setup-checklist/setup-checklist.jsx +446 -0
- package/registry/blocks/share-dialog/README.md +113 -0
- package/registry/blocks/share-dialog/block.json +10 -0
- package/registry/blocks/share-dialog/demo.jsx +140 -0
- package/registry/blocks/share-dialog/index.js +3 -0
- package/registry/blocks/share-dialog/share-dialog.jsx +384 -0
- package/registry/blocks/sidebar/README.md +190 -0
- package/registry/blocks/sidebar/block.json +43 -0
- package/registry/blocks/sidebar/demo.jsx +577 -0
- package/registry/blocks/sidebar/index.js +4 -0
- package/registry/blocks/sidebar/sidebar-panel.jsx +149 -0
- package/registry/blocks/sidebar/sidebar.jsx +864 -0
- package/registry/blocks/signature-pad/README.md +137 -0
- package/registry/blocks/signature-pad/block.json +13 -0
- package/registry/blocks/signature-pad/colors.js +115 -0
- package/registry/blocks/signature-pad/demo.jsx +94 -0
- package/registry/blocks/signature-pad/index.js +6 -0
- package/registry/blocks/signature-pad/pad.js +7 -0
- package/registry/blocks/signature-pad/signature-pad.jsx +434 -0
- package/registry/blocks/signature-pad/signature.js +68 -0
- package/registry/blocks/sketch-canvas/README.md +280 -0
- package/registry/blocks/sketch-canvas/block.json +13 -0
- package/registry/blocks/sketch-canvas/demo.jsx +159 -0
- package/registry/blocks/sketch-canvas/excalidraw.js +40 -0
- package/registry/blocks/sketch-canvas/index.js +9 -0
- package/registry/blocks/sketch-canvas/palette.js +226 -0
- package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1188 -0
- package/registry/blocks/sketch-canvas/sketch.js +179 -0
- package/registry/blocks/sketch-canvas/ssr-shim-end.js +6 -0
- package/registry/blocks/sketch-canvas/ssr-shim.js +45 -0
- package/registry/blocks/sketch-canvas/stamps.js +104 -0
- package/registry/blocks/sketch-canvas/theme.js +131 -0
- package/registry/blocks/spreadsheet/README.md +307 -0
- package/registry/blocks/spreadsheet/block.json +13 -0
- package/registry/blocks/spreadsheet/cells.jsx +455 -0
- package/registry/blocks/spreadsheet/chrome.jsx +325 -0
- package/registry/blocks/spreadsheet/demo.jsx +177 -0
- package/registry/blocks/spreadsheet/grid.js +387 -0
- package/registry/blocks/spreadsheet/index.js +34 -0
- package/registry/blocks/spreadsheet/spreadsheet.jsx +1732 -0
- package/registry/blocks/spreadsheet/types.js +473 -0
- package/registry/blocks/stat-cards/README.md +173 -0
- package/registry/blocks/stat-cards/block.json +10 -0
- package/registry/blocks/stat-cards/demo.jsx +257 -0
- package/registry/blocks/stat-cards/index.js +4 -0
- package/registry/blocks/stat-cards/stat-cards.jsx +307 -0
- package/registry/blocks/stat-cards/usage.jsx +243 -0
- package/registry/blocks/task-list/README.md +176 -0
- package/registry/blocks/task-list/block.json +10 -0
- package/registry/blocks/task-list/demo.jsx +134 -0
- package/registry/blocks/task-list/index.js +5 -0
- package/registry/blocks/task-list/task-list.jsx +907 -0
- package/registry/blocks/task-model/README.md +455 -0
- package/registry/blocks/task-model/block.json +167 -0
- package/registry/blocks/task-model/changes.js +89 -0
- package/registry/blocks/task-model/dates.js +371 -0
- package/registry/blocks/task-model/demo.jsx +215 -0
- package/registry/blocks/task-model/dnd.jsx +240 -0
- package/registry/blocks/task-model/index.js +97 -0
- package/registry/blocks/task-model/model.js +919 -0
- package/registry/blocks/task-model/parts.jsx +449 -0
- package/registry/blocks/task-model/toolbar.jsx +271 -0
- package/registry/blocks/testimonials/README.md +139 -0
- package/registry/blocks/testimonials/block.json +11 -0
- package/registry/blocks/testimonials/demo.jsx +137 -0
- package/registry/blocks/testimonials/index.js +4 -0
- package/registry/blocks/testimonials/testimonials.jsx +430 -0
- package/registry/blocks/transaction-list/README.md +126 -0
- package/registry/blocks/transaction-list/block.json +10 -0
- package/registry/blocks/transaction-list/demo.jsx +79 -0
- package/registry/blocks/transaction-list/index.js +4 -0
- package/registry/blocks/transaction-list/transaction-list.jsx +383 -0
- package/registry/blocks/whiteboard/README.md +179 -0
- package/registry/blocks/whiteboard/block.json +10 -0
- package/registry/blocks/whiteboard/demo.jsx +169 -0
- package/registry/blocks/whiteboard/index.js +6 -0
- package/registry/blocks/whiteboard/templates.js +150 -0
- package/registry/blocks/whiteboard/whiteboard.jsx +721 -0
- package/src/install.mjs +18 -0
- package/src/shadcn.mjs +171 -0
- package/templates/next/files/README.md +145 -11
- package/templates/next/files/app/globals.css +55 -4
- package/templates/next/files/check/run.mjs +3 -3
- package/templates/next/files/components/parts/fixture.jsx +1 -1
- package/templates/next/files/components/parts/sample.js +15 -0
- package/templates/next/files/components/ui/avatar.jsx +141 -0
- package/templates/next/files/components/ui/breadcrumb.jsx +149 -0
- package/templates/next/files/components/ui/button-group.jsx +70 -0
- package/templates/next/files/components/ui/button.jsx +132 -12
- package/templates/next/files/components/ui/calendar.jsx +609 -0
- package/templates/next/files/components/ui/carousel.jsx +281 -0
- package/templates/next/files/components/ui/collapsible.jsx +208 -0
- package/templates/next/files/components/ui/combobox.jsx +608 -0
- package/templates/next/files/components/ui/command.jsx +577 -0
- package/templates/next/files/components/ui/context-menu.jsx +191 -0
- package/templates/next/files/components/ui/date-picker.jsx +360 -0
- package/templates/next/files/components/ui/distribution.jsx +117 -0
- package/templates/next/files/components/ui/feedback.jsx +143 -0
- package/templates/next/files/components/ui/field.jsx +115 -3
- package/templates/next/files/components/ui/file-input.jsx +234 -0
- package/templates/next/files/components/ui/icon.jsx +130 -8
- package/templates/next/files/components/ui/image.jsx +143 -0
- package/templates/next/files/components/ui/index.js +47 -2
- package/templates/next/files/components/ui/input-group.jsx +213 -0
- package/templates/next/files/components/ui/input-otp.jsx +170 -0
- package/templates/next/files/components/ui/item.jsx +206 -0
- package/templates/next/files/components/ui/menu.jsx +454 -82
- package/templates/next/files/components/ui/notice.jsx +126 -14
- package/templates/next/files/components/ui/pagination.jsx +156 -0
- package/templates/next/files/components/ui/popover.jsx +284 -0
- package/templates/next/files/components/ui/radio.jsx +193 -0
- package/templates/next/files/components/ui/rating.jsx +131 -0
- package/templates/next/files/components/ui/resizable.jsx +338 -0
- package/templates/next/files/components/ui/scroll-area.jsx +123 -0
- package/templates/next/files/components/ui/separator.jsx +119 -0
- package/templates/next/files/components/ui/sheet.jsx +96 -11
- package/templates/next/files/components/ui/slider.jsx +174 -0
- package/templates/next/files/components/ui/stat.jsx +574 -0
- package/templates/next/files/components/ui/steps.jsx +338 -0
- package/templates/next/files/components/ui/swatch.jsx +33 -0
- package/templates/next/files/components/ui/table.jsx +130 -0
- package/templates/next/files/components/ui/toast.jsx +440 -0
- package/templates/next/files/components/ui/toggle.jsx +195 -0
- package/templates/next/files/components/ui/tooltip.jsx +270 -0
- package/templates/next/files/lib/sample.js +4 -12
- package/templates/next/template.json +264 -8
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cogaid Solutions Private Limited
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -13,6 +13,37 @@ router, JSX, server actions), Postgres, Tailwind v4 mapped to the
|
|
|
13
13
|
workspace's theme, the data layer and write actions, the `_claw` platform
|
|
14
14
|
client, and a kit of parts. The app designs its own screens on top.
|
|
15
15
|
|
|
16
|
+
## In a shadcn project
|
|
17
|
+
|
|
18
|
+
The kit and the free blocks are also a shadcn registry. In any Next.js
|
|
19
|
+
15+ project with Tailwind v4 and shadcn set up:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npx shadcn@latest add https://cdn.jsdelivr.net/npm/makersclaw@0.2/r/board.json
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The first block brings the kit with it (`core`: into
|
|
26
|
+
`components/makersclaw/ui` and `components/makersclaw/parts`, plus the
|
|
27
|
+
success, warning and info colours your theme may not have). Blocks land
|
|
28
|
+
in `components/makersclaw/blocks/<name>/`, with their README:
|
|
29
|
+
|
|
30
|
+
```jsx
|
|
31
|
+
import { Board } from "@/components/makersclaw/blocks/board";
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or name the registry once in `components.json` and add by name:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{ "registries": { "@makersclaw": "https://cdn.jsdelivr.net/npm/makersclaw@0.2/r/{name}.json" } }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npx shadcn@latest add @makersclaw/board
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Makersclaw builds whole apps from these blocks, with an agent that runs
|
|
45
|
+
them: [makersclaw.com](https://makersclaw.com).
|
|
46
|
+
|
|
16
47
|
## Components come with the template; blocks come when a surface needs one
|
|
17
48
|
|
|
18
49
|
- **Components** — the kit (`components/ui`: Button, Field, Select, Sheet,
|
|
@@ -58,7 +89,8 @@ against the old one may need rebuilding.
|
|
|
58
89
|
templates/next/template.json which files are fixed, docs, starters or merged; the kit's entries
|
|
59
90
|
templates/next/files/ the app — what `init` lays
|
|
60
91
|
registry/blocks/<name>/ block.json, the files `add` lays, README.md (what view/add print), demo.jsx
|
|
61
|
-
src/ the CLI (no dependencies)
|
|
92
|
+
src/ the CLI (no dependencies); src/shadcn.mjs builds the shadcn registry
|
|
93
|
+
r/ built at `npm pack` (never committed): the registry jsDelivr serves
|
|
62
94
|
test/ npm test · npm run check:regress · npm run test:app
|
|
63
95
|
```
|
|
64
96
|
|
|
@@ -66,3 +98,11 @@ test/ npm test · npm run check:regress · npm run test
|
|
|
66
98
|
- `npm run check:regress` — the template's `npm run check`, case by case.
|
|
67
99
|
- `npm run test:app` — a real app: init, every block added with its demo as
|
|
68
100
|
a surface, `npm install`, `npm run check` (build, start, every surface).
|
|
101
|
+
- `npm run build:registry` — `r/`, the shadcn registry (`--base <url>
|
|
102
|
+
--out <dir>` to serve it locally for a test).
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT (see LICENSE) — the template, the kit and the free blocks. Premium
|
|
107
|
+
blocks, when they come, are served by Makersclaw's registry under their own
|
|
108
|
+
license and never ship in this package.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
Some free blocks in `registry/blocks/` are rewrites, onto this kit, of blocks
|
|
4
|
+
from the MIT-licensed libraries below. Each such block credits its source
|
|
5
|
+
in its README. Their licences follow in full.
|
|
6
|
+
|
|
7
|
+
## blocks.so — https://github.com/ephraimduncan/blocks
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2025 Ephraim Duncan
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Creative Tim UI — https://github.com/creativetimofficial/ui
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
MIT License
|
|
37
|
+
|
|
38
|
+
Copyright (c) 2025 [Creative Tim](https://www.creative-tim.com/)
|
|
39
|
+
|
|
40
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
41
|
+
of this software and associated documentation files (the "Software "), to deal
|
|
42
|
+
in the Software without restriction, including without limitation the rights
|
|
43
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
44
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
45
|
+
furnished to do so, subject to the following conditions:
|
|
46
|
+
|
|
47
|
+
The above copyright notice and this permission notice shall be included in all
|
|
48
|
+
copies or substantial portions of the Software.
|
|
49
|
+
|
|
50
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
51
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
52
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
53
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
54
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
55
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
56
|
+
SOFTWARE.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Dependencies, not copied code
|
|
60
|
+
|
|
61
|
+
Some free blocks declare npm packages in their `block.json` `dependencies`.
|
|
62
|
+
`makersclaw add` writes them into the app's package.json (the shadcn CLI
|
|
63
|
+
installs them the same way); npm fetches them. No code of theirs is in
|
|
64
|
+
this package, so their licences are not reproduced here — each ships its
|
|
65
|
+
own in its npm tarball.
|
|
66
|
+
|
|
67
|
+
### BlockNote — https://github.com/TypeCellOS/BlockNote
|
|
68
|
+
|
|
69
|
+
`@blocknote/core`, `@blocknote/react` and `@blocknote/shadcn`, MPL-2.0,
|
|
70
|
+
pinned exactly by the `doc-editor` block. The Mozilla Public License is
|
|
71
|
+
file-level copyleft: the BlockNote files stay under the MPL (and their
|
|
72
|
+
source is public on npm and GitHub); our files that import them are not
|
|
73
|
+
affected and stay MIT. The block uses them unmodified.
|
|
74
|
+
|
|
75
|
+
`registry/blocks/doc-editor/shadcn-classes.js` is a list of Tailwind class
|
|
76
|
+
names read out of `@blocknote/shadcn` (by `scripts/blocknote-classes.mjs`)
|
|
77
|
+
so the app's Tailwind generates them; it holds class names only, no code.
|
|
78
|
+
|
|
79
|
+
BlockNote's `@blocknote/xl-*` packages (multi-column, AI, PDF/DOCX/ODT
|
|
80
|
+
export) are GPL-3.0 or commercial and are never used by any block here.
|
|
81
|
+
|
|
82
|
+
### React Flow — https://github.com/xyflow/xyflow
|
|
83
|
+
|
|
84
|
+
`@xyflow/react`, MIT (webkid GmbH), pinned exactly by the `flow-canvas`
|
|
85
|
+
block and used unmodified: its stylesheet is imported from the package,
|
|
86
|
+
its colours mapped onto the workspace's tokens by the block. Its small
|
|
87
|
+
attribution link stays unless an app passes `hideAttribution`.
|
|
88
|
+
|
|
89
|
+
### dnd kit — https://github.com/clauderic/dnd-kit
|
|
90
|
+
|
|
91
|
+
`@dnd-kit/core`, `@dnd-kit/sortable` and `@dnd-kit/utilities`, MIT
|
|
92
|
+
(Claudéric Demers), pinned exactly by the `task-model` block and used
|
|
93
|
+
unmodified. The block ships none of their code and no stylesheet; the
|
|
94
|
+
views built on task-model reach them only through its drag primitives.
|
|
95
|
+
|
|
96
|
+
### Excalidraw — https://github.com/excalidraw/excalidraw
|
|
97
|
+
|
|
98
|
+
`@excalidraw/excalidraw`, MIT, pinned exactly by the `sketch-canvas` block
|
|
99
|
+
and used unmodified. The block ships none of its code, fonts or assets:
|
|
100
|
+
its stylesheet is imported from the package, and its fonts are replaced by
|
|
101
|
+
the device's system font (see the block's README).
|
|
102
|
+
|
|
103
|
+
### wavesurfer.js — https://github.com/katspaugh/wavesurfer.js
|
|
104
|
+
|
|
105
|
+
`wavesurfer.js` and `@wavesurfer/react`, BSD-3-Clause (katspaugh), pinned
|
|
106
|
+
exactly by the `audio-player` block and used unmodified. The block ships
|
|
107
|
+
none of their code and no stylesheet; the waveform's colours are read
|
|
108
|
+
from the workspace's tokens at run time.
|
|
109
|
+
|
|
110
|
+
### signature_pad — https://github.com/szimek/signature_pad
|
|
111
|
+
|
|
112
|
+
`signature_pad`, MIT (Szymon Nowak), pinned exactly by the
|
|
113
|
+
`signature-pad` block and used unmodified. The block ships none of its
|
|
114
|
+
code; the ink is read from the workspace's tokens at run time.
|
|
115
|
+
|
|
116
|
+
### MapLibre GL JS — https://github.com/maplibre/maplibre-gl-js
|
|
117
|
+
|
|
118
|
+
`maplibre-gl`, BSD-3-Clause (MapLibre contributors), and `react-map-gl`,
|
|
119
|
+
MIT (vis.gl / Uber), pinned exactly by the `map` block and used
|
|
120
|
+
unmodified: maplibre's stylesheet is imported from the package, and its
|
|
121
|
+
tile worker is loaded at run time from jsDelivr at the installed version.
|
|
122
|
+
The default base map is OpenFreeMap (https://openfreemap.org) over
|
|
123
|
+
OpenStreetMap data (ODbL); the map keeps their attribution line on screen.
|
|
124
|
+
|
|
125
|
+
### GrapesJS — https://github.com/GrapesJS/grapesjs
|
|
126
|
+
|
|
127
|
+
`grapesjs`, BSD-3-Clause (Artur Arseniev), and `@grapesjs/react`, MIT
|
|
128
|
+
(Artur Arseniev), pinned exactly by the `page-builder` block and used
|
|
129
|
+
unmodified — the free core only, never the commercial GrapesJS Studio
|
|
130
|
+
SDK. The block ships none of their code: GrapesJS's stylesheet is
|
|
131
|
+
imported from the package and repainted with the workspace's tokens, and
|
|
132
|
+
its telemetry is switched off.
|
|
133
|
+
|
|
134
|
+
### React-PDF and PDF.js — https://github.com/wojtekmaj/react-pdf, https://github.com/mozilla/pdf.js
|
|
135
|
+
|
|
136
|
+
`react-pdf`, MIT (Wojciech Maj), and `pdfjs-dist`, Apache-2.0 (Mozilla
|
|
137
|
+
Foundation), pinned exactly by the `pdf-viewer` block and used
|
|
138
|
+
unmodified. The block ships none of their code: the text and annotation
|
|
139
|
+
layers' stylesheets are imported from react-pdf, and pdf.js's worker is
|
|
140
|
+
bundled from `pdfjs-dist` by the app's own build (a `new URL(…,
|
|
141
|
+
import.meta.url)` asset), never copied into the app or fetched from a CDN.
|
|
142
|
+
|
|
143
|
+
### @react-pdf/renderer — https://github.com/diegomura/react-pdf
|
|
144
|
+
|
|
145
|
+
`@react-pdf/renderer`, MIT (Diego Muracciole), pinned exactly by the
|
|
146
|
+
`pdf-document` block and used unmodified. The block ships none of its
|
|
147
|
+
code; PDFs use the reader's built-in Helvetica unless an app registers a
|
|
148
|
+
font, and their colours are read from the workspace's tokens when a PDF
|
|
149
|
+
is made.
|
|
150
|
+
|
|
151
|
+
### TanStack Virtual and Papa Parse — https://github.com/TanStack/virtual, https://github.com/mholt/PapaParse
|
|
152
|
+
|
|
153
|
+
`@tanstack/react-virtual`, MIT (Tanner Linsley), pinned exactly by the
|
|
154
|
+
`spreadsheet` block, and `papaparse`, MIT (Matthew Holt), pinned exactly
|
|
155
|
+
by the `csv-import` block; both used unmodified. The blocks ship none of
|
|
156
|
+
their code and no stylesheet: the grid is drawn with the kit and tokens,
|
|
157
|
+
and the virtualiser only says which rows and columns are in the window.
|
|
158
|
+
|
|
159
|
+
### CodeMirror — https://github.com/codemirror/dev
|
|
160
|
+
|
|
161
|
+
`@codemirror/state`, `view`, `commands`, `language`, `autocomplete`,
|
|
162
|
+
`search`, `lint`, `lang-sql`, `lang-json` and `@lezer/highlight`, MIT
|
|
163
|
+
(Marijn Haverbeke and others), pinned exactly by the `code-editor` block
|
|
164
|
+
and used unmodified. The block ships none of their code; its theme is
|
|
165
|
+
written as CSS that reads the workspace's tokens (`var(--color-…)`).
|
|
166
|
+
|
|
167
|
+
### react-email — https://github.com/resend/react-email
|
|
168
|
+
|
|
169
|
+
`@react-email/components` and `@react-email/render`, MIT (Resend), pinned
|
|
170
|
+
exactly by the `email-render` block and used unmodified. The block ships
|
|
171
|
+
none of their code; an email's colours are read from the workspace's
|
|
172
|
+
tokens when it is rendered and written into the HTML as plain values,
|
|
173
|
+
because email clients cannot read CSS variables.
|
|
174
|
+
|
|
175
|
+
### Mermaid — https://github.com/mermaid-js/mermaid
|
|
176
|
+
|
|
177
|
+
`mermaid`, MIT (Knut Sveidqvist and contributors), pinned exactly by the
|
|
178
|
+
`diagram` block and used unmodified, in its strict security mode. The
|
|
179
|
+
block ships none of its code; the diagram's colours are read from the
|
|
180
|
+
workspace's tokens at run time and handed to Mermaid as its theme.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "makersclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The Makersclaw app template, its kit and its blocks: init an app, add the blocks a surface needs, search the registry.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"src",
|
|
12
12
|
"templates",
|
|
13
13
|
"registry",
|
|
14
|
-
"
|
|
14
|
+
"r",
|
|
15
|
+
"README.md",
|
|
16
|
+
"THIRD-PARTY-NOTICES.md"
|
|
15
17
|
],
|
|
16
18
|
"engines": {
|
|
17
19
|
"node": ">=18.17"
|
|
@@ -19,16 +21,21 @@
|
|
|
19
21
|
"scripts": {
|
|
20
22
|
"test": "node --test test/*.test.mjs",
|
|
21
23
|
"test:app": "node test/smoke-app.mjs",
|
|
22
|
-
"check:regress": "bash test/check-regress.sh"
|
|
24
|
+
"check:regress": "bash test/check-regress.sh",
|
|
25
|
+
"build:registry": "node scripts/build-registry.mjs",
|
|
26
|
+
"prepack": "node scripts/build-registry.mjs"
|
|
23
27
|
},
|
|
24
28
|
"keywords": [
|
|
25
|
-
"makersclaw",
|
|
26
|
-
"template",
|
|
27
|
-
"nextjs",
|
|
28
29
|
"blocks",
|
|
30
|
+
"cli",
|
|
29
31
|
"components",
|
|
30
|
-
"
|
|
32
|
+
"kanban",
|
|
33
|
+
"makersclaw",
|
|
34
|
+
"nextjs",
|
|
35
|
+
"registry",
|
|
36
|
+
"shadcn",
|
|
37
|
+
"template"
|
|
31
38
|
],
|
|
32
39
|
"homepage": "https://makersclaw.com",
|
|
33
|
-
"license": "
|
|
40
|
+
"license": "MIT"
|
|
34
41
|
}
|
package/r/annotate.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "annotate",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Annotate",
|
|
6
|
+
"description": "Mark up a screenshot: drop, pick or paste one, then draw arrows, boxes, highlights, redaction bars and numbered notes on it (Excalidraw underneath, via sketch-canvas). A side list holds the words of each numbered note; a click on a note's number shows its pin. Starter templates (bug report, design review, walkthrough) on a fresh screenshot. The annotation is plain JSON saved through onChange / an async onSave; onSubmit hands over a PNG of the marked-up screenshot with the notes and the sketch.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/core.json",
|
|
9
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/sketch-canvas.json"
|
|
10
|
+
],
|
|
11
|
+
"dependencies": [],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "registry/blocks/annotate/index.js",
|
|
15
|
+
"type": "registry:component",
|
|
16
|
+
"target": "@components/makersclaw/blocks/annotate/index.js",
|
|
17
|
+
"content": "// The Annotate block — `makersclaw add annotate`. Fixed: compose it\n// through its props (README.md beside this file). Built on sketch-canvas\n// (the canvas underneath); the helpers read an annotation anywhere, the\n// server included.\nexport { Annotate, ANNOTATE_TOOLS } from \"./annotate\";\nexport {\n ANNOTATE_TEMPLATES,\n emptyAnnotation,\n toAnnotation,\n annotationNotes,\n annotationToText,\n noteMarkers,\n nextNoteNumber,\n markerSkeleton,\n} from \"./annotate-data\";\n"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"path": "registry/blocks/annotate/annotate.jsx",
|
|
21
|
+
"type": "registry:component",
|
|
22
|
+
"target": "@components/makersclaw/blocks/annotate/annotate.jsx",
|
|
23
|
+
"content": "\"use client\";\n\n// Annotate — mark up a screenshot and say what each mark means.\n//\n// Drop, pick or paste a screenshot; it lies locked at the back of a\n// SketchCanvas (the foundation: Excalidraw, themed). On top: arrows, boxes,\n// freehand, text, a highlight band, a redaction bar and numbered notes.\n// Every numbered note is a pin on the image AND a row in the side list,\n// where its words are typed. The pin is the truth for which notes exist:\n// delete it on the canvas and the row goes (an undo brings both back).\n//\n// Out: `onChange(annotation)` / `onSave` (async → { ok }) with the whole\n// annotation as plain JSON, and `onSubmit({ image, notes, sketch, … })`\n// (async → { ok }) with a PNG of the marked-up screenshot.\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { Badge, Button, Confirm, FileDrop, FileInput, Icon, IconButton, Kbd, Textarea, Tooltip, cx } from \"../../ui\";\nimport { SketchCanvas, emptySketch, useSketchCanvas } from \"../sketch-canvas\";\nimport {\n ANNOTATE_TEMPLATES,\n ANNOTATION_VERSION,\n annotationNotes,\n annotationToText,\n highlightSkeleton,\n markerSize,\n markerSkeleton,\n nextNoteNumber,\n noteMarkers,\n redactSkeleton,\n renumberDuplicates,\n templateMarks,\n toAnnotation,\n} from \"./annotate-data\";\n\n/** The canvas tools Annotate offers, in rail order (SketchCanvas ids). */\nexport const ANNOTATE_TOOLS = [\"select\", \"hand\", \"arrow\", \"rectangle\", \"ellipse\", \"draw\", \"text\", \"eraser\"];\n\n// Tailwind needs whole class names in the source: one per marker tone.\nconst CHIP = {\n destructive: \"bg-destructive/15 text-destructive border-destructive/40\",\n primary: \"bg-primary/15 text-primary border-primary/40\",\n warning: \"bg-warning/15 text-warning border-warning/40\",\n success: \"bg-success/15 text-success border-success/40\",\n info: \"bg-info/15 text-info border-info/40\",\n \"chart-2\": \"bg-chart-2/15 text-chart-2 border-chart-2/40\",\n \"chart-3\": \"bg-chart-3/15 text-chart-3 border-chart-3/40\",\n \"chart-4\": \"bg-chart-4/15 text-chart-4 border-chart-4/40\",\n \"chart-5\": \"bg-chart-5/15 text-chart-5 border-chart-5/40\",\n};\n\nconst isTyping = (t) => t?.tagName === \"TEXTAREA\" || t?.tagName === \"INPUT\" || t?.tagName === \"SELECT\" || t?.isContentEditable;\nconst plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;\nconst wordsFrom = (notes) => Object.fromEntries((notes ?? []).map((x) => [x.n, { text: x.text ?? \"\", ...(x.hint ? { hint: x.hint } : {}) }]));\nconst wordsList = (words) => Object.entries(words).map(([n, w]) => ({ n: Number(n), ...w }));\n\n/** A File → a data URL no larger than `max` px on its long side. */\nfunction readImage(file, max) {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onerror = () => reject(new Error(\"That file couldn't be read.\"));\n reader.onload = () => {\n const src = String(reader.result);\n const img = new window.Image();\n img.onerror = () => reject(new Error(\"That file isn't an image we can show.\"));\n img.onload = () => {\n const w = img.naturalWidth;\n const h = img.naturalHeight;\n const k = max > 0 ? Math.min(1, max / Math.max(w, h)) : 1;\n if (k >= 1) return resolve({ src, width: w, height: h });\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.round(w * k);\n canvas.height = Math.round(h * k);\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return resolve({ src, width: w, height: h });\n ctx.imageSmoothingQuality = \"high\";\n ctx.drawImage(img, 0, 0, canvas.width, canvas.height);\n const type = file.type === \"image/jpeg\" ? \"image/jpeg\" : \"image/png\";\n resolve({ src: canvas.toDataURL(type, 0.9), width: canvas.width, height: canvas.height });\n };\n img.src = src;\n };\n reader.readAsDataURL(file);\n });\n}\n\n/** An image URL's natural size. */\nfunction measure(src) {\n return new Promise((resolve, reject) => {\n const img = new window.Image();\n img.onerror = () => reject(new Error(\"The screenshot didn't load.\"));\n img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight });\n img.src = src;\n });\n}\n\nexport function Annotate({\n value,\n defaultValue,\n onChange,\n onSave,\n onSubmit,\n submitLabel = \"Send\",\n uploadImage,\n maxImageSize = 1600,\n title = \"Annotate a screenshot\",\n readOnly = false,\n tone = \"destructive\",\n tools = ANNOTATE_TOOLS,\n canvasRef,\n label,\n className,\n}) {\n const own = useSketchCanvas();\n const canvas = canvasRef ?? own;\n const first = useRef(null);\n if (!first.current) first.current = toAnnotation(value ?? defaultValue);\n\n const [image, setImage] = useState(first.current.image);\n const [sketch, setSketch] = useState(first.current.sketch);\n const [words, setWords] = useState(() => wordsFrom(first.current.notes));\n const [intake, setIntake] = useState({ state: \"idle\" });\n const [save, setSave] = useState({ state: \"idle\" });\n const [send, setSend] = useState({ state: \"idle\" });\n const [restart, setRestart] = useState(false);\n const [focusN, setFocusN] = useState(null);\n const tone_ = CHIP[tone] ? tone : \"destructive\";\n\n // Latest state for callbacks that outlive a render.\n const live = useRef({});\n live.current = { image, sketch, words };\n const cbs = useRef({});\n cbs.current = { onChange, onSave };\n\n // ---- output ---------------------------------------------------------------\n\n const lastOut = useRef(value);\n const compose = useCallback(() => {\n const { image: img, sketch: sk, words: w } = live.current;\n const notes = annotationNotes(sk, wordsList(w)).map(({ n, text, hint }) => ({ n, text, ...(hint ? { hint } : {}) }));\n return { type: \"annotation\", version: ANNOTATION_VERSION, image: img, sketch: sk, notes };\n }, []);\n\n const saveSeq = useRef(0);\n const runSave = useCallback(async (v) => {\n const fn = cbs.current.onSave;\n if (!fn) return;\n const seq = ++saveSeq.current;\n setSave({ state: \"saving\" });\n let res;\n try {\n res = await fn(v);\n } catch (e) {\n res = { ok: false, error: e?.message };\n }\n if (seq === saveSeq.current) setSave(res?.ok ? { state: \"saved\" } : { state: \"error\", error: res?.error, value: v });\n }, []);\n\n const pending = useRef(false);\n const emit = useCallback(() => {\n pending.current = false;\n const v = compose();\n lastOut.current = v;\n cbs.current.onChange?.(v);\n runSave(v);\n }, [compose, runSave]);\n\n // A user change marks the annotation dirty; it goes out after `delay`\n // ms of quiet (the canvas has already debounced its own edits).\n const [dirty, setDirty] = useState(0);\n const delay = useRef(0);\n const touch = useCallback((ms = 0) => {\n delay.current = ms;\n pending.current = true;\n setDirty((d) => d + 1);\n }, []);\n useEffect(() => {\n if (!dirty) return;\n const t = setTimeout(emit, delay.current);\n return () => clearTimeout(t);\n }, [dirty, emit]);\n const flush = useRef(emit);\n flush.current = emit;\n useEffect(() => () => pending.current && flush.current(), []);\n\n // Controlled: a `value` that is not what we last sent loads another record.\n useEffect(() => {\n if (value === undefined || value === lastOut.current) return;\n lastOut.current = value;\n const a = toAnnotation(value);\n setImage(a.image);\n setSketch(a.sketch);\n setWords(wordsFrom(a.notes));\n seen.current = new Set(noteMarkers(a.sketch).map((m) => m.n));\n }, [value]);\n\n // A saved image without its size: measure it (templates and pins scale by it).\n useEffect(() => {\n if (!image?.src || (image.width && image.height)) return;\n let gone = false;\n measure(image.src)\n .then((d) => !gone && setImage((cur) => (cur?.src === image.src ? { ...cur, ...d } : cur)))\n .catch(() => {});\n return () => {\n gone = true;\n };\n }, [image]);\n\n // ---- the screenshot -------------------------------------------------------\n\n const takeFile = useCallback(\n async (file) => {\n if (!file) return;\n if (!/^image\\//.test(file.type ?? \"\")) {\n setIntake({ state: \"error\", error: \"That isn't an image. Try a PNG, JPG or WebP screenshot.\" });\n return;\n }\n setIntake({ state: \"busy\" });\n try {\n let next;\n if (uploadImage) {\n const res = await uploadImage(file);\n if (!res?.ok || !res.src) throw new Error(res?.error || \"The screenshot couldn't be uploaded.\");\n next = { src: res.src, ...(await measure(res.src)) };\n } else next = await readImage(file, maxImageSize);\n const hadImage = !!live.current.image;\n setImage(next);\n setIntake({ state: \"idle\" });\n touch(0);\n if (hadImage) setTimeout(() => canvas.current?.zoomToFit(), 80);\n } catch (e) {\n setIntake({ state: \"error\", error: e?.message || \"The screenshot couldn't be read.\" });\n }\n },\n [uploadImage, maxImageSize, touch, canvas],\n );\n\n // Paste a screenshot while there is none yet (once there is one, a paste\n // lands on the canvas as Excalidraw's own image).\n useEffect(() => {\n if (image || readOnly) return;\n const onPaste = (e) => {\n if (isTyping(e.target)) return;\n const file = [...(e.clipboardData?.files ?? [])].find((f) => /^image\\//.test(f.type));\n if (!file) return;\n e.preventDefault();\n takeFile(file);\n };\n document.addEventListener(\"paste\", onPaste);\n return () => document.removeEventListener(\"paste\", onPaste);\n }, [image, readOnly, takeFile]);\n\n // ---- the marks ------------------------------------------------------------\n\n const seen = useRef(null);\n if (!seen.current) seen.current = new Set(noteMarkers(first.current.sketch).map((m) => m.n));\n\n const onSketch = useCallback(\n (v) => {\n // A last edit flushed by a canvas that is closing (Start over): not ours.\n if (!live.current.image) return;\n // A duplicate (Cmd+D) or a paste copies a pin's number: give the copy\n // the next free one, quietly, so undo still undoes the duplicate.\n const fixed = renumberDuplicates(v);\n const api = canvas.current?.api;\n if (fixed && api) {\n const byId = new Map(fixed.elements.map((el) => [el.id, el]));\n api.updateScene({ elements: api.getSceneElementsIncludingDeleted().map((el) => byId.get(el.id) ?? el), captureUpdate: \"NEVER\" });\n return;\n }\n const ns = noteMarkers(v).map((m) => m.n);\n const added = ns.filter((n) => !seen.current.has(n));\n seen.current = new Set(ns);\n setSketch(v);\n if (added.length) setFocusN(Math.min(...added));\n touch(0);\n },\n [canvas, touch],\n );\n\n const size = markerSize(image?.width);\n const place = useRef({});\n place.current = { size, tone: tone_ };\n const stamps = useMemo(\n () => [\n {\n id: \"note\",\n label: \"Numbered note\",\n icon: \"pin\",\n create: () => markerSkeleton(nextNoteNumber(canvas.current?.getScene() ?? live.current.sketch), { size: place.current.size, tone: place.current.tone }),\n },\n { id: \"highlight\", label: \"Highlight\", icon: \"sparkles\", create: () => highlightSkeleton({ width: place.current.size * 7, height: Math.round(place.current.size * 1.2) }) },\n { id: \"redact\", label: \"Hide something private\", icon: \"eye-off\", create: () => redactSkeleton({ width: place.current.size * 5, height: Math.round(place.current.size * 0.8) }) },\n ],\n [canvas],\n );\n\n const addNote = () => {\n const c = canvas.current;\n if (!c) return;\n c.insertElements(markerSkeleton(nextNoteNumber(c.getScene()), { size, tone: tone_ }));\n };\n\n const applyTemplate = (id) => {\n const c = canvas.current;\n if (!c || !image) return;\n const { steps, hints } = templateMarks(id, { width: image.width, height: image.height, tone: tone_, first: nextNoteNumber(c.getScene()) });\n setWords((w) => {\n const next = { ...w };\n for (const [n, hint] of Object.entries(hints)) next[n] = { text: next[n]?.text ?? \"\", hint };\n return next;\n });\n c.insertElements(\n steps.flatMap((s) => s.skeletons),\n { at: false, select: false },\n );\n };\n\n const locate = (m) => {\n const api = canvas.current?.api;\n const el = api?.getSceneElements().find((e) => e.id === m.id);\n if (!el) return;\n api.updateScene({ appState: { selectedElementIds: { [el.id]: true } } });\n api.scrollToContent(el, { animate: true });\n canvas.current?.focus();\n };\n\n const removeNote = (n) => {\n const c = canvas.current;\n if (!c) return;\n const s = c.getScene();\n c.setScene({ ...s, elements: s.elements.filter((el) => el.customData?.note !== n) });\n };\n\n const setText = (n, text) => {\n setWords((w) => ({ ...w, [n]: { ...(w[n] ?? {}), text } }));\n touch(500);\n };\n\n // New pin: its row's field takes the focus (not on a phone — the keyboard\n // would cover the screenshot being marked).\n const fields = useRef(new Map());\n useEffect(() => {\n if (focusN == null) return;\n const el = fields.current.get(focusN);\n if (el && typeof matchMedia !== \"undefined\" && matchMedia(\"(min-width: 768px)\").matches) el.focus({ preventScroll: true });\n setFocusN(null);\n }, [focusN]);\n\n // N: the numbered-note tool.\n const keyDown = (e) => {\n if (readOnly || !image || isTyping(e.target) || e.metaKey || e.ctrlKey || e.altKey) return;\n if (e.key === \"n\" || e.key === \"N\") {\n e.preventDefault();\n e.stopPropagation();\n canvas.current?.setTool(\"note\");\n }\n };\n\n // ---- send -----------------------------------------------------------------\n\n const submit = async () => {\n const c = canvas.current;\n if (!c || !image || !onSubmit) return;\n setSend({ state: \"sending\" });\n let res;\n try {\n const blob = await c.exportImage({ padding: 0 });\n const sk = c.getScene();\n const notes = annotationNotes(sk, wordsList(live.current.words));\n res = await onSubmit({ image: blob, notes, sketch: sk, screenshot: image, text: annotationToText({ sketch: sk, notes }) });\n } catch (e) {\n res = { ok: false, error: e?.message };\n }\n setSend(res?.ok ? { state: \"sent\" } : { state: \"error\", error: res?.error || \"It didn't send.\" });\n };\n\n const startOver = () => {\n setImage(null);\n setSketch(emptySketch());\n setWords({});\n seen.current = new Set();\n setSend({ state: \"idle\" });\n setRestart(false);\n touch(0);\n };\n\n // ---- render ---------------------------------------------------------------\n\n const markers = noteMarkers(sketch);\n const notes = annotationNotes(sketch, wordsList(words));\n const sized = /(^|\\s)(h-|min-h-|size-|flex-1|grow)/.test(className ?? \"\");\n\n return (\n // raw: a plain box that hears N on its way into the canvas (capture), to\n // pick the numbered-note tool; no control of its own.\n <section\n aria-label={label ?? title}\n onKeyDownCapture={keyDown}\n className={cx(\"flex flex-col overflow-hidden rounded-lg border border-border bg-card text-card-foreground\", !sized && \"md:h-[36rem]\", className)}\n >\n <header className=\"flex flex-wrap items-center gap-2 border-b border-border px-3 py-2\">\n <div className=\"min-w-0 flex-1\">\n <h2 className=\"truncate text-sm font-semibold\">{title}</h2>\n <StatusLine count={markers.length} image={!!image} save={save} send={send} onRetry={() => runSave(save.value ?? compose())} />\n </div>\n {image && !readOnly ? (\n <>\n <FileInput accept=\"image/*\" onFiles={([f]) => takeFile(f)} variant=\"secondary\" size=\"sm\" icon=\"image\" disabled={intake.state === \"busy\"}>\n Replace\n </FileInput>\n <Tooltip label=\"Start over\">\n <IconButton icon=\"refresh\" label=\"Start over\" size=\"sm\" onClick={() => setRestart(true)} />\n </Tooltip>\n </>\n ) : null}\n {onSubmit && !readOnly ? (\n <Button variant=\"primary\" size=\"sm\" icon=\"send\" loading={send.state === \"sending\"} disabled={!image} onClick={submit}>\n {submitLabel}\n </Button>\n ) : null}\n </header>\n\n {intake.state === \"error\" && image ? (\n <p role=\"alert\" className=\"border-b border-border px-3 py-2 text-xs text-destructive\">\n {intake.error}\n </p>\n ) : null}\n\n {!image ? (\n <Intake readOnly={readOnly} intake={intake} onFile={takeFile} onReject={() => setIntake({ state: \"error\", error: \"That isn't an image. Try a PNG, JPG or WebP screenshot.\" })} />\n ) : (\n <div className=\"flex min-h-0 flex-1 flex-col md:flex-row\">\n <div className=\"h-[26rem] min-w-0 md:h-auto md:min-h-0 md:flex-1\">\n <SketchCanvas\n label={`${title}: the screenshot and its marks`}\n value={sketch}\n onChange={onSketch}\n changeDelay={250}\n background={image}\n readOnly={readOnly}\n tools={tools}\n stamps={readOnly ? undefined : stamps}\n grid={false}\n canvasRef={canvas}\n empty={<Starter onTemplate={applyTemplate} />}\n className=\"h-full rounded-none border-0\"\n />\n </div>\n <NotesPanel\n notes={notes}\n markers={markers}\n tone={tone_}\n readOnly={readOnly}\n fields={fields}\n onText={setText}\n onLocate={locate}\n onRemove={removeNote}\n onAdd={addNote}\n />\n </div>\n )}\n\n <Confirm\n open={restart}\n onClose={() => setRestart(false)}\n onConfirm={startOver}\n title=\"Start over?\"\n body=\"The screenshot, every mark and every note are removed.\"\n confirmLabel=\"Start over\"\n tone=\"danger\"\n />\n </section>\n );\n}\n\nfunction StatusLine({ count, image, save, send, onRetry }) {\n const bits = [];\n if (image) bits.push(plural(count, \"note\", \"notes\"));\n return (\n <div className=\"flex flex-wrap items-center gap-x-2 text-xs text-muted-foreground\">\n {bits.length ? <span>{bits.join(\" · \")}</span> : <span>No screenshot yet</span>}\n {save.state === \"saving\" ? <span role=\"status\">Saving…</span> : null}\n {save.state === \"saved\" ? <span role=\"status\">Saved</span> : null}\n {save.state === \"error\" ? (\n <span role=\"alert\" className=\"inline-flex items-center gap-1 text-destructive\">\n {save.error || \"Couldn't save\"}\n <Button variant=\"link\" size=\"sm\" onClick={onRetry}>\n Retry\n </Button>\n </span>\n ) : null}\n {send.state === \"sent\" ? (\n <span role=\"status\" className=\"inline-flex items-center gap-1 text-success\">\n <Icon name=\"circle-check\" size={12} /> Sent\n </span>\n ) : null}\n {send.state === \"error\" ? (\n <span role=\"alert\" className=\"text-destructive\">\n {send.error}\n </span>\n ) : null}\n </div>\n );\n}\n\n/** No screenshot yet: a big drop zone, and how it works. */\nfunction Intake({ readOnly, intake, onFile, onReject }) {\n if (readOnly) {\n return (\n <div className=\"grid min-h-[20rem] flex-1 place-items-center p-6 text-sm text-muted-foreground\">\n <p>No screenshot to show.</p>\n </div>\n );\n }\n return (\n <div className=\"flex min-h-[24rem] flex-1 flex-col items-center justify-center gap-5 p-4 sm:p-8\">\n <FileDrop\n accept=\"image/*\"\n onFiles={([f]) => onFile(f)}\n onReject={onReject}\n disabled={intake.state === \"busy\"}\n invalid={intake.state === \"error\"}\n label={intake.state === \"busy\" ? \"Reading the screenshot…\" : \"Drop a screenshot here, or browse\"}\n hint=\"PNG, JPG or WebP. Or paste one: Cmd/Ctrl+V.\"\n className=\"w-full max-w-xl\"\n />\n {intake.state === \"error\" ? (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {intake.error}\n </p>\n ) : null}\n <ol className=\"grid w-full max-w-xl gap-3 text-sm sm:grid-cols-3\">\n {[\n [\"image\", \"Add a screenshot\", \"Drop it, pick it or paste it.\"],\n [\"pin\", \"Mark it up\", \"Pins, arrows, boxes, highlights.\"],\n [\"message-circle\", \"Say what you mean\", \"A line of words for every pin.\"],\n ].map(([icon, head, body], i) => (\n <li key={head} className=\"flex items-start gap-2.5\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-full bg-muted text-muted-foreground\">\n <Icon name={icon} size={16} />\n </span>\n <span className=\"min-w-0\">\n <span className=\"block font-medium\">\n {i + 1}. {head}\n </span>\n <span className=\"block text-muted-foreground\">{body}</span>\n </span>\n </li>\n ))}\n </ol>\n </div>\n );\n}\n\n/** A screenshot with nothing on it yet: how to start, and three starters.\n * Sits low over the image so the screenshot stays in view. */\nfunction Starter({ onTemplate }) {\n return (\n <div className=\"pointer-events-auto mb-[7.5rem] flex max-w-sm flex-col items-center gap-2 self-end rounded-xl border border-border bg-popover px-4 py-3 text-center text-popover-foreground shadow-lg md:mb-12\">\n <p className=\"text-sm\">\n Press <Kbd>N</Kbd> for a numbered note, <Kbd>A</Kbd> for an arrow, <Kbd>R</Kbd> for a box.\n </p>\n <div className=\"flex flex-wrap justify-center gap-1.5\">\n {ANNOTATE_TEMPLATES.map((t) => (\n <Tooltip key={t.id} label={t.description}>\n <Button variant=\"secondary\" size=\"sm\" icon={t.icon} onClick={() => onTemplate(t.id)}>\n {t.label}\n </Button>\n </Tooltip>\n ))}\n </div>\n </div>\n );\n}\n\n/** The numbered notes, in number order: the pin's chip (shows it on the\n * screenshot), its words, delete. Also the text view of the annotation. */\nfunction NotesPanel({ notes, markers, tone, readOnly, fields, onText, onLocate, onRemove, onAdd }) {\n const byN = new Map(markers.map((m) => [m.n, m]));\n return (\n <aside aria-label=\"Notes\" className=\"flex max-h-[26rem] shrink-0 flex-col border-t border-border md:max-h-none md:w-80 md:border-l md:border-t-0\">\n <div className=\"flex items-center gap-2 px-3 py-2\">\n <h3 className=\"text-sm font-semibold\">Notes</h3>\n {notes.length ? <Badge>{notes.length}</Badge> : null}\n <span className=\"flex-1\" />\n {!readOnly ? (\n <Button variant=\"quiet\" size=\"sm\" icon=\"plus\" onClick={onAdd}>\n Add note\n </Button>\n ) : null}\n </div>\n {notes.length ? (\n <ol className=\"grid min-h-0 flex-1 content-start gap-3 overflow-y-auto px-3 pb-3\">\n {notes.map((x) => (\n <li key={x.n} className=\"flex items-start gap-2\">\n <Tooltip label=\"Show on the screenshot\" side=\"left\">\n <Button\n variant=\"quiet\"\n size=\"sm\"\n iconOnly\n aria-label={`Note ${x.n}: show on the screenshot`}\n onClick={() => byN.has(x.n) && onLocate(byN.get(x.n))}\n className=\"shrink-0\"\n >\n <span className={cx(\"grid size-7 place-items-center rounded-full border text-xs font-semibold tabular-nums\", CHIP[tone])}>{x.n}</span>\n </Button>\n </Tooltip>\n {readOnly ? (\n <p className={cx(\"min-w-0 flex-1 whitespace-pre-wrap break-words pt-2 text-sm\", !x.text.trim() && \"text-muted-foreground\")}>{x.text.trim() || \"No words.\"}</p>\n ) : (\n <>\n <Textarea\n ref={(el) => {\n if (el) fields.current.set(x.n, el);\n else fields.current.delete(x.n);\n }}\n aria-label={`Note ${x.n}`}\n placeholder={x.hint || \"What should change here?\"}\n value={x.text}\n onChange={(e) => onText(x.n, e.target.value)}\n autoGrow\n minRows={2}\n maxRows={8}\n className=\"min-w-0 flex-1 text-sm\"\n />\n <Tooltip label=\"Delete note\">\n <IconButton icon=\"trash\" label={`Delete note ${x.n}`} size=\"sm\" onClick={() => onRemove(x.n)} className=\"shrink-0\" />\n </Tooltip>\n </>\n )}\n </li>\n ))}\n </ol>\n ) : (\n <div className=\"px-3 pb-4 text-sm text-muted-foreground\">\n {readOnly ? (\n <p>No numbered notes.</p>\n ) : (\n <p>\n Press <Kbd>N</Kbd> or pick the pin in the rail, then click the screenshot. Each pin gets a number, and its words go here.\n </p>\n )}\n </div>\n )}\n </aside>\n );\n}\n"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"path": "registry/blocks/annotate/annotate-data.js",
|
|
27
|
+
"type": "registry:component",
|
|
28
|
+
"target": "@components/makersclaw/blocks/annotate/annotate-data.js",
|
|
29
|
+
"content": "// The annotation as data: what Annotate takes and gives back, and the\n// helpers that read it. Plain functions of plain JSON — no React, no\n// Excalidraw — so they run anywhere, the server included.\n//\n// An annotation is\n// { type: \"annotation\", version: 1,\n// image: { src, width, height } | null, the screenshot\n// sketch: { … }, SketchCanvas's value: the marks\n// notes: [{ n, text, hint? }] } the numbered notes' words\n//\n// A numbered note is two things kept in step: a marker on the screenshot\n// (an ellipse in the sketch whose customData.note is its number, with the\n// number as its label) and its words in `notes`. The marker is the truth\n// for which notes exist: delete it on the canvas and the note is gone from\n// the output; its words stay in memory, so an undo brings both back.\nimport { emptySketch, sketchItems, toSketch } from \"../sketch-canvas\";\n\nexport const ANNOTATION_VERSION = 1;\n\nexport function emptyAnnotation() {\n return { type: \"annotation\", version: ANNOTATION_VERSION, image: null, sketch: emptySketch(), notes: [] };\n}\n\n/** An image prop or saved image → { src, width?, height? }, or null. */\nexport function toImage(image) {\n if (!image) return null;\n if (typeof image === \"string\") return { src: image };\n if (typeof image === \"object\" && typeof image.src === \"string\" && image.src) {\n const out = { src: image.src };\n if (image.width > 0 && image.height > 0) {\n out.width = Math.round(image.width);\n out.height = Math.round(image.height);\n }\n return out;\n }\n return null;\n}\n\n/** Anything a caller may hand us → an annotation (never throws). */\nexport function toAnnotation(value) {\n if (!value || typeof value !== \"object\") return emptyAnnotation();\n const notes = Array.isArray(value.notes)\n ? value.notes\n .filter((x) => x && Number.isInteger(Number(x.n)) && Number(x.n) > 0)\n .map((x) => ({ n: Number(x.n), text: String(x.text ?? \"\"), ...(x.hint ? { hint: String(x.hint) } : {}) }))\n : [];\n return { type: \"annotation\", version: ANNOTATION_VERSION, image: toImage(value.image), sketch: toSketch(value.sketch), notes };\n}\n\n/** The note number an element carries, or null. */\nexport function noteNumber(el) {\n const n = el?.customData?.note;\n return Number.isInteger(n) && n > 0 ? n : null;\n}\n\n/** An element that is a note's marker (not its label). */\nexport const isMarker = (el) => !!el && !el.isDeleted && el.type !== \"text\" && noteNumber(el) !== null;\n\n/** The markers in a sketch, by number: [{ n, id, x, y, width, height }].\n * A number that appears twice (a duplicate, a paste) is listed once, at\n * its first marker; `duplicateMarkers` finds the others. */\nexport function noteMarkers(sketch) {\n const seen = new Map();\n for (const el of toSketch(sketch).elements) {\n if (!isMarker(el)) continue;\n const n = noteNumber(el);\n if (!seen.has(n)) seen.set(n, { n, id: el.id, x: el.x, y: el.y, width: el.width, height: el.height });\n }\n return [...seen.values()].sort((a, b) => a.n - b.n);\n}\n\n/** The number the next note gets: one past the highest on the sketch\n * (numbers are stable — deleting 2 of 1, 2, 3 leaves 1 and 3, and the\n * next is 4). */\nexport function nextNoteNumber(sketch) {\n let max = 0;\n for (const el of toSketch(sketch).elements) if (isMarker(el)) max = Math.max(max, noteNumber(el));\n return max + 1;\n}\n\n/** Markers whose number an earlier marker already has: [{ id, n }] in paint\n * order. What a duplicate (Cmd+D) or a paste leaves behind. */\nexport function duplicateMarkers(sketch) {\n const seen = new Set();\n const out = [];\n for (const el of toSketch(sketch).elements) {\n if (!isMarker(el)) continue;\n const n = noteNumber(el);\n if (seen.has(n)) out.push({ id: el.id, n });\n else seen.add(n);\n }\n return out;\n}\n\nconst bump = (el, patch) => ({ ...el, ...patch, version: (el.version ?? 1) + 1, versionNonce: Math.floor(Math.random() * 2 ** 31) });\n\n/** The sketch with every duplicate marker given a fresh number (and its\n * label rewritten), or null when there is nothing to fix. */\nexport function renumberDuplicates(sketch) {\n const s = toSketch(sketch);\n const dups = duplicateMarkers(s);\n if (!dups.length) return null;\n let next = nextNoteNumber(s);\n const fresh = new Map(dups.map((d) => [d.id, next++]));\n const labelOf = new Map();\n for (const el of s.elements) {\n if (!fresh.has(el.id)) continue;\n for (const b of el.boundElements ?? []) if (b?.type === \"text\") labelOf.set(b.id, fresh.get(el.id));\n }\n const elements = s.elements.map((el) => {\n if (fresh.has(el.id)) return bump(el, { customData: { ...el.customData, note: fresh.get(el.id) } });\n if (labelOf.has(el.id)) {\n const n = labelOf.get(el.id);\n return bump(el, { text: String(n), originalText: String(n), customData: { ...(el.customData ?? {}), note: n } });\n }\n return el;\n });\n return { ...s, elements };\n}\n\n/** The sketch without note `n` (its marker, its label). */\nexport function removeNote(sketch, n) {\n const s = toSketch(sketch);\n return { ...s, elements: s.elements.filter((el) => noteNumber(el) !== n) };\n}\n\n/** The notes as they go out: one per marker on the sketch, by number,\n * with its words (and where it sits on the screenshot). */\nexport function annotationNotes(sketch, notes = []) {\n const words = new Map((notes ?? []).map((x) => [x.n, x]));\n return noteMarkers(sketch).map((m) => {\n const w = words.get(m.n);\n return {\n n: m.n,\n text: String(w?.text ?? \"\"),\n ...(w?.hint ? { hint: w.hint } : {}),\n x: Math.round(m.x + m.width / 2),\n y: Math.round(m.y + m.height / 2),\n };\n });\n}\n\n/** The annotation in words, for an agent or a reader:\n * \"1. The total is wrong\\n2. …\", then any other words drawn on it. */\nexport function annotationToText(annotation) {\n const a = toAnnotation(annotation);\n const lines = annotationNotes(a.sketch, a.notes).map((x) => `${x.n}. ${x.text.trim() || \"(no words yet)\"}`);\n const other = sketchItems(a.sketch)\n .filter((it) => it.kind === \"text\" && !it.container)\n .map((it) => it.text)\n .filter(Boolean);\n if (other.length) lines.push(...(lines.length ? [\"\"] : []), \"Also written on it:\", ...other.map((t) => `- ${t}`));\n return lines.join(\"\\n\");\n}\n\n// ---- what the block places ---------------------------------------------------\n\n/** The marker size for a screenshot this wide: readable at fit-to-screen. */\nexport const markerSize = (imageWidth) => Math.round(Math.max(28, Math.min(56, (imageWidth || 1000) / 32)));\n\n/** A numbered marker, laid out from (0, 0). `tone` is a token. */\nexport function markerSkeleton(n, { size = 32, tone = \"destructive\" } = {}) {\n return [\n {\n type: \"ellipse\",\n x: 0,\n y: 0,\n width: size,\n height: size,\n strokeColor: tone,\n backgroundColor: `${tone}:soft`,\n fillStyle: \"solid\",\n strokeWidth: 2,\n roughness: 0,\n customData: { note: n },\n label: { text: String(n), fontSize: Math.round(size * 0.5), strokeColor: tone, customData: { note: n } },\n },\n ];\n}\n\n/** A highlight: a translucent band over what matters. */\nexport function highlightSkeleton({ width = 240, height = 44, tone = \"warning\" } = {}) {\n return [{ type: \"rectangle\", x: 0, y: 0, width, height, strokeColor: tone, backgroundColor: tone, fillStyle: \"solid\", strokeWidth: 1, roughness: 0, opacity: 35 }];\n}\n\n/** A redaction: a solid bar over something private (an email, a name). */\nexport function redactSkeleton({ width = 180, height = 28 } = {}) {\n return [{ type: \"rectangle\", x: 0, y: 0, width, height, strokeColor: \"foreground\", backgroundColor: \"foreground\", fillStyle: \"solid\", strokeWidth: 1, roughness: 0 }];\n}\n\n/** Starter templates: marks laid on a screenshot w × h, as insert steps\n * ({ skeletons, note? }) and the hints their notes start with. */\nexport const ANNOTATE_TEMPLATES = [\n { id: \"bug\", label: \"Bug report\", icon: \"warning\", description: \"Box the problem, say what you expected\" },\n { id: \"review\", label: \"Design review\", icon: \"message-circle\", description: \"Three notes: keep, change, ask\" },\n { id: \"steps\", label: \"Walkthrough\", icon: \"arrow-right\", description: \"Numbered steps joined by arrows\" },\n];\n\nexport function templateMarks(id, { width = 1000, height = 640, tone = \"destructive\", first = 1 } = {}) {\n const size = markerSize(width);\n const at = (fx, fy) => ({ x: Math.round(width * fx), y: Math.round(height * fy) });\n const marker = (n, fx, fy) => {\n const p = at(fx, fy);\n return { note: n, skeletons: markerSkeleton(n, { size, tone }).map((s) => ({ ...s, x: p.x - size / 2, y: p.y - size / 2 })) };\n };\n const n = (i) => first + i;\n if (id === \"bug\") {\n const a = at(0.3, 0.32);\n const b = at(0.7, 0.62);\n return {\n steps: [\n { skeletons: [{ type: \"rectangle\", x: a.x, y: a.y, width: b.x - a.x, height: b.y - a.y, strokeColor: tone, backgroundColor: \"none\", strokeWidth: 3, roughness: 0, roundness: { type: 3 } }] },\n marker(n(0), 0.7, 0.32),\n marker(n(1), 0.3, 0.62),\n ],\n hints: { [n(0)]: \"What went wrong here?\", [n(1)]: \"What did you expect instead?\" },\n };\n }\n if (id === \"review\") {\n return {\n steps: [marker(n(0), 0.22, 0.25), marker(n(1), 0.55, 0.45), marker(n(2), 0.8, 0.72)],\n hints: { [n(0)]: \"What works — keep it\", [n(1)]: \"What to change\", [n(2)]: \"An open question\" },\n };\n }\n if (id === \"steps\") {\n const pts = [\n [0.2, 0.3],\n [0.5, 0.5],\n [0.8, 0.3],\n ];\n const gap = size * 0.9;\n const arrows = pts.slice(1).map(([fx, fy], i) => {\n const p = at(...pts[i]);\n const q = at(fx, fy);\n const len = Math.hypot(q.x - p.x, q.y - p.y) || 1;\n const ux = (q.x - p.x) / len;\n const uy = (q.y - p.y) / len;\n const sx = p.x + ux * gap;\n const sy = p.y + uy * gap;\n const dx = q.x - ux * gap - sx;\n const dy = q.y - uy * gap - sy;\n return { type: \"arrow\", x: sx, y: sy, width: Math.abs(dx), height: Math.abs(dy), points: [[0, 0], [dx, dy]], strokeColor: tone, strokeWidth: 2, roughness: 0, endArrowhead: \"arrow\" };\n });\n return {\n steps: [...pts.map(([fx, fy], i) => marker(n(i), fx, fy)), { skeletons: arrows }],\n hints: { [n(0)]: \"Step 1: start here\", [n(1)]: \"Step 2: then this\", [n(2)]: \"Step 3: and this happens\" },\n };\n }\n return { steps: [], hints: {} };\n}\n"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"path": "registry/blocks/annotate/README.md",
|
|
33
|
+
"type": "registry:file",
|
|
34
|
+
"target": "@components/makersclaw/blocks/annotate/README.md",
|
|
35
|
+
"content": "# Annotate\n\nMark up a screenshot and say what each mark means. Drop a screenshot on\nit, pick one, or paste one (Cmd/Ctrl+V). It lies locked at the back of the\ncanvas, and on top you draw: numbered notes (pins, <kbd>N</kbd>), arrows,\nboxes, ellipses, freehand, text, a highlight band, and a solid bar to hide\nsomething private. Every numbered pin has a row in the Notes list beside\nthe canvas, where its words are typed; a click on a row's number shows its\npin. A fresh screenshot offers three starters — Bug report, Design review,\nWalkthrough — that lay pins (and a box or arrows) with prompts in their\nrows. A top bar holds the title, the note count, the save state, Replace,\nStart over and the send button.\n\nIt is `sketch-canvas` underneath (Excalidraw, in the workspace theme): its\ntool rail, selection toolbar, zoom, undo and PNG export.\n\n```jsx\nimport { Annotate, annotationToText } from \"../components/blocks/annotate\"; // a Makersclaw app (\"./blocks/annotate\" from components/)\nimport { Annotate, annotationToText } from \"@/components/makersclaw/blocks/annotate\"; // a shadcn project\n```\n\n`Annotate` is a client component: render it from a `\"use client\"`\ncomponent. The helpers are plain functions and work on the server.\n`makersclaw add annotate` adds `sketch-canvas` with it (and Excalidraw:\nrun `npm install` after).\n\n## The data\n\nAn annotation is plain JSON. Store it as it comes out of `onChange` /\n`onSave` (a `jsonb` column).\n\n```js\n{\n type: \"annotation\",\n version: 1,\n image: { src: \"data:image/png;base64,…\", width: 1280, height: 800 }, // or an https URL; null before one is added\n sketch: { type: \"sketch\", version: 1, elements: [ … ], files: {}, appState: {} }, // sketch-canvas's value: the marks\n notes: [\n { n: 1, text: \"The total is wrong here\" },\n { n: 2, text: \"\", hint: \"What did you expect instead?\" }, // hint: a starter's prompt, shown while empty\n ],\n}\n```\n\n- **A pin is the truth for which notes exist.** A pin is an ellipse in the\n sketch whose `customData.note` is its number, with the number as its\n label. `notes` lists one entry per pin on the sketch, by number. Delete\n a pin on the canvas and its entry is gone from the next `onChange`; the\n words stay in memory, so an undo brings both back.\n- **Numbers are stable.** Deleting 2 of 1, 2, 3 leaves 1 and 3, and the\n next pin is 4. A pin duplicated (Cmd+D) or pasted gets the next free\n number.\n- **The screenshot is a data URL** unless you pass `uploadImage`. It is\n scaled down to `maxImageSize` on its long side first (1600 px: about\n 0.5–2 MB as a PNG). To keep images out of the row, upload the file and\n store the URL (see `uploadImage`).\n- Colours in the marks are theme tokens (see sketch-canvas's README): a\n saved annotation repaints with the theme.\n\n## Props\n\n- `value` — the annotation, controlled. A new `value` that is not what the\n block last sent loads it (another record). Pass back exactly what\n `onChange` gave you and nothing resets.\n- `defaultValue` — the starting annotation, uncontrolled. `{ image }`\n alone opens a screenshot with nothing on it. With neither, the block\n starts at the drop zone.\n- `onChange(annotation)` — the whole annotation after an edit settles: a\n mark (250 ms after the canvas goes quiet), a note's words (500 ms after\n typing stops), a new screenshot (at once). Not called for pan, zoom or\n selection.\n- `onSave(annotation)` — `async`, returns `{ ok: true }` or\n `{ ok: false, error? }`. Called right after `onChange`. The top bar\n shows \"Saving…\", \"Saved\", or the error with Retry. A throw counts as a\n failure.\n- `onSubmit({ image, notes, sketch, screenshot, text })` — `async`,\n `{ ok }`. Without it there is no send button. `image` is a PNG `Blob`\n of the screenshot with the marks (2× scale); `notes` is\n `[{ n, text, hint?, x, y }]` (x, y: the pin's centre in screenshot\n pixels); `sketch` the marks; `screenshot` the `{ src, width, height }`\n underneath; `text` is `annotationToText` of it all. The bar shows\n \"Sent\" or the error.\n- `submitLabel` — the send button's words (default \"Send\"): \"File the\n bug\", \"Send to support\", \"Ask the agent\".\n- `uploadImage(file)` — `async`, returns `{ ok: true, src }` or\n `{ ok: false, error? }`. With it, a dropped or picked screenshot is\n uploaded as it is and `image.src` is the URL you return (same origin,\n or `exportImage` cannot read it back). Without it, the file becomes a\n data URL, scaled to `maxImageSize`.\n- `maxImageSize` — px on the long side a screenshot is scaled down to when\n it becomes a data URL (default 1600; `0` keeps it as it is).\n- `title` — the top bar's heading and the region's name (default\n \"Annotate a screenshot\").\n- `readOnly` — `false` (default). `true`: the marks to look at (pan and\n zoom), the notes as text; no rail, no drop, no send.\n- `tone` — the pins' colour, a token: `destructive` (default), `primary`,\n `warning`, `success`, `info`, `chart-2` … `chart-5`.\n- `tools` — the canvas tools in the rail (sketch-canvas ids). Default\n `ANNOTATE_TOOLS`: select, pan, arrow, rectangle, ellipse, draw, text,\n eraser. The three annotate stamps (numbered note, highlight, hide) always\n follow them.\n- `canvasRef` — a ref (`useSketchCanvas()` from sketch-canvas) that\n receives the canvas's handle: `exportImage()`, `getScene()`,\n `zoomToFit()`…\n- `label` — the region's accessible name (default: `title`).\n- `className` — layout: width, margin, height. From 768 px the default\n height is `h-[36rem]` (canvas and notes side by side); a height class of\n yours replaces it. Below that the notes stack under a 26rem canvas.\n Never colour.\n\n### Keys\n\nOn the canvas: <kbd>N</kbd> a numbered note (click to place it; Enter\ndrops it in the middle), <kbd>A</kbd> arrow, <kbd>R</kbd> rectangle,\n<kbd>O</kbd> ellipse, <kbd>P</kbd> draw, <kbd>T</kbd> text, <kbd>E</kbd>\neraser, <kbd>V</kbd> select, <kbd>H</kbd> pan, Delete, Cmd/Ctrl+D, Cmd/Ctrl+Z,\nShift+1 fit. \"Add note\" in the Notes list drops a pin in the middle of the\nview without the mouse.\n\n### Helpers\n\nPlain functions; they take an annotation (or a sketch) and never throw on\na partial one:\n\n- `emptyAnnotation()`, `toAnnotation(value)` — the shape above.\n- `annotationNotes(sketch, notes)` — `[{ n, text, hint?, x, y }]`: one per\n pin, by number.\n- `annotationToText(annotation)` — the words, for an agent or a reader:\n `\"1. The total is wrong here\\n2. …\"`, then any free text drawn on it\n under \"Also written on it:\".\n- `noteMarkers(sketch)` — the pins: `[{ n, id, x, y, width, height }]`.\n- `nextNoteNumber(sketch)` — the number the next pin gets.\n- `markerSkeleton(n, { size, tone })` — a pin as sketch-canvas skeletons,\n for `canvasRef.current.insertElements` (a pin you place from code).\n- `ANNOTATE_TEMPLATES`, `ANNOTATE_TOOLS` — the starters and the default\n tools.\n\n## Example\n\nA feedback widget that saves itself and files a ticket:\n\n```jsx\n\"use client\";\nimport { Annotate } from \"./blocks/annotate\";\nimport { insertRow, updateRow } from \"../lib/actions\";\n\nexport default function Feedback({ ticket }) {\n return (\n <Annotate\n title=\"What should change?\"\n defaultValue={ticket.annotation} // jsonb, may be null\n onSave={(annotation) => updateRow({ source: \"public.tickets\", id: ticket.id, patch: { annotation } })}\n onSubmit={({ notes, text }) => // image: a PNG Blob, to upload if you keep it\n insertRow({ source: \"public.bug_reports\", values: { ticket_id: ticket.id, summary: text, note_count: notes.length } })\n }\n submitLabel=\"File the bug\"\n className=\"h-dvh\"\n />\n );\n}\n```\n\n## What it will not do\n\n- Crop, rotate or resize the screenshot. Replace swaps it for another;\n the marks stay where they were.\n- Annotate several screenshots at once: one image per annotation (keep a\n list of annotations for a series).\n- Renumber pins after a delete — numbers stay what they were.\n- Blur: \"hide\" is a solid bar drawn over the image. The original pixels\n are still in `image.src`; send the exported PNG, not the source, when\n what is hidden must not leave.\n- Take a screenshot for you: the browser cannot capture the page without\n asking; the person drops or pastes one.\n- Collaborate live: saves are last-write-wins.\n\nA markup tool it cannot be is your own component in `components/` —\nnot an edit to this block. Its files are fixed: `npm run check` refuses a\nchange, and `makersclaw add annotate` restores them.\n"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"categories": [
|
|
39
|
+
"annotate",
|
|
40
|
+
"annotation",
|
|
41
|
+
"screenshot",
|
|
42
|
+
"markup",
|
|
43
|
+
"mark up",
|
|
44
|
+
"feedback",
|
|
45
|
+
"bug report",
|
|
46
|
+
"design review",
|
|
47
|
+
"pins",
|
|
48
|
+
"numbered notes",
|
|
49
|
+
"arrows",
|
|
50
|
+
"highlight",
|
|
51
|
+
"redact",
|
|
52
|
+
"excalidraw",
|
|
53
|
+
"png"
|
|
54
|
+
],
|
|
55
|
+
"docs": "Installed to components/makersclaw/blocks/annotate. Import: import { Annotate, ANNOTATE_TOOLS, ANNOTATE_TEMPLATES, emptyAnnotation, toAnnotation, annotationNotes, annotationToText, noteMarkers, nextNoteNumber, markerSkeleton } from \"@/components/makersclaw/blocks/annotate\". Props and an example: components/makersclaw/blocks/annotate/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
56
|
+
"meta": {
|
|
57
|
+
"makersclaw": {
|
|
58
|
+
"version": "0.3.0",
|
|
59
|
+
"useWhen": "Someone needs to point at parts of a screen and say something about each: a bug report, design feedback, a support question, a walkthrough, a request to an agent about what to change. Not a blank drawing board (whiteboard or sketch-canvas), not a wireframe (sketch-to-app), not an image upload alone (file-upload)."
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "approval-request",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "ApprovalRequest",
|
|
6
|
+
"description": "One request waiting on a decision: what is asked for as label/value facts (an amount in large type), who asked and when, why, a status badge, and Approve / Reject (reject asks for a reason). The decision goes out through async onApprove / onReject; the block shows it working, a failure, then the outcome in place of the buttons. A card alone, or a page of detail sections beside the card.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/core.json"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "registry/blocks/approval-request/index.js",
|
|
14
|
+
"type": "registry:component",
|
|
15
|
+
"target": "@components/makersclaw/blocks/approval-request/index.js",
|
|
16
|
+
"content": "// The ApprovalRequest block — `makersclaw add approval-request`. Fixed:\n// compose it through its props (README.md beside this file); a request it\n// cannot be is your own component in components/.\nexport { ApprovalRequest } from \"./approval-request\";\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "registry/blocks/approval-request/approval-request.jsx",
|
|
20
|
+
"type": "registry:component",
|
|
21
|
+
"target": "@components/makersclaw/blocks/approval-request/approval-request.jsx",
|
|
22
|
+
"content": "\"use client\";\n\nimport { useEffect, useId, useState } from \"react\";\nimport { Avatar, Badge, Button, Empty, Field, Icon, Notice, Separator, Skeleton, Textarea, cx } from \"../../ui\";\n\n// ApprovalRequest — one request waiting on a decision: what is asked for\n// (label/value facts), who asked and when, why, and Approve / Reject. The\n// decision goes out through `onApprove()` / `onReject(reason)`; the\n// block shows it working, shows a failure, and once it lands shows the\n// outcome in place of the buttons.\n\nconst STATUS = {\n pending: { tone: \"warning\", label: \"Pending approval\" },\n approved: { tone: \"success\", label: \"Approved\" },\n rejected: { tone: \"danger\", label: \"Rejected\" },\n};\n\nfunction defaultFormat(value) {\n if (!value) return \"\";\n const d = value instanceof Date ? value : new Date(value);\n if (Number.isNaN(d.getTime())) return String(value);\n return new Intl.DateTimeFormat(undefined, { dateStyle: \"long\" }).format(d);\n}\n\nfunction When({ value, format }) {\n if (!value) return null;\n const iso = value instanceof Date ? value.toISOString() : String(value);\n return (\n <time dateTime={iso} suppressHydrationWarning>\n {format(value)}\n </time>\n );\n}\n\nfunction StatusBadge({ status }) {\n const s = STATUS[status] ?? STATUS.pending;\n return (\n <Badge tone={s.tone} variant=\"soft\">\n {s.label}\n </Badge>\n );\n}\n\nfunction Requester({ requester, requestedAt, format }) {\n if (!requester && !requestedAt) return null;\n return (\n <div className=\"flex min-w-0 items-center gap-2 text-sm text-muted-foreground\">\n {requester ? <Avatar name={requester.name} src={requester.src} size=\"xs\" /> : null}\n <p className=\"min-w-0\">\n {requester ? (\n <>\n <span className=\"font-medium text-foreground\">{requester.name}</span>\n {requester.role ? `, ${requester.role}` : \"\"}\n </>\n ) : null}\n {requester && requestedAt ? \" · \" : null}\n <When value={requestedAt} format={format} />\n </p>\n </div>\n );\n}\n\n/* ------------------------------------------------------------------------\n The card: facts and the decision\n ------------------------------------------------------------------------ */\n\nfunction DecisionCard({\n title,\n kind,\n details,\n requester,\n requestedAt,\n justification,\n status,\n decidedBy,\n decidedAt,\n decisionNote,\n onApprove,\n onReject,\n reasonRequired,\n approveLabel,\n rejectLabel,\n format,\n showContext,\n className,\n}) {\n const [local, setLocal] = useState(null);\n const [mode, setMode] = useState(\"idle\"); // idle · rejecting\n const [busy, setBusy] = useState(null); // approve · reject\n const [reason, setReason] = useState(\"\");\n const [error, setError] = useState(null);\n const reasonId = useId();\n\n // A new status from the app wins over what the block showed meanwhile.\n useEffect(() => setLocal(null), [status]);\n\n const shown = local?.status ?? status ?? \"pending\";\n const pending = shown === \"pending\";\n const facts = (details ?? []).filter(Boolean);\n\n async function decide(kindOf) {\n const fn = kindOf === \"approve\" ? onApprove : onReject;\n if (!fn) return;\n setBusy(kindOf);\n setError(null);\n let res;\n try {\n res = await fn(kindOf === \"reject\" ? reason.trim() : undefined);\n } catch (e) {\n res = { ok: false, error: e?.message };\n }\n setBusy(null);\n if (res?.ok) {\n setLocal({ status: kindOf === \"approve\" ? \"approved\" : \"rejected\", note: kindOf === \"reject\" ? reason.trim() : undefined, at: new Date() });\n setMode(\"idle\");\n } else {\n setError(res?.error || (kindOf === \"approve\" ? \"The approval was not saved. Try again.\" : \"The rejection was not saved. Try again.\"));\n }\n }\n\n const note = local?.note ?? decisionNote;\n const at = local?.at ?? decidedAt;\n const by = local ? \"you\" : decidedBy;\n\n return (\n <section aria-label={title} className={cx(\"grid gap-4 rounded-lg border border-border bg-card p-4 text-card-foreground sm:p-5\", className)}>\n <header className=\"grid gap-2\">\n <div className=\"flex flex-wrap items-start justify-between gap-2\">\n <h2 className=\"min-w-0 text-lg font-semibold leading-tight\">{title}</h2>\n <div className=\"flex flex-wrap gap-1.5\">\n {kind ? <Badge variant=\"soft\">{kind}</Badge> : null}\n <StatusBadge status={shown} />\n </div>\n </div>\n {showContext ? <Requester requester={requester} requestedAt={requestedAt} format={format} /> : null}\n </header>\n\n {facts.length ? (\n <dl className=\"grid gap-4\">\n {facts.map((f, i) => (\n <div key={f.label ?? i} className=\"flex items-center gap-3\">\n {f.icon ? (\n <span className=\"flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground\">\n <Icon name={f.icon} size={16} />\n </span>\n ) : null}\n <div className=\"grid min-w-0 gap-0.5\">\n <dt className=\"text-xs text-muted-foreground\">{f.label}</dt>\n <dd data-nums className={cx(\"min-w-0 break-words\", f.emphasis ? \"text-xl font-semibold\" : \"text-sm font-medium\")}>\n {f.value}\n </dd>\n </div>\n </div>\n ))}\n </dl>\n ) : null}\n\n {showContext && justification ? (\n <div className=\"grid gap-1\">\n <p className=\"text-xs text-muted-foreground\">Why</p>\n <p className=\"text-sm leading-relaxed text-foreground\">{justification}</p>\n </div>\n ) : null}\n\n <Separator />\n\n {pending ? (\n mode === \"rejecting\" ? (\n <form\n className=\"grid gap-3\"\n onSubmit={(e) => {\n e.preventDefault();\n if (reasonRequired && !reason.trim()) return;\n decide(\"reject\");\n }}\n >\n <Field id={reasonId} label=\"Why is it rejected?\" hint=\"The requester sees this.\" required={reasonRequired}>\n <Textarea\n id={reasonId}\n rows={3}\n value={reason}\n onChange={(e) => setReason(e.target.value)}\n autoFocus\n disabled={busy !== null}\n placeholder=\"Over this quarter's budget; ask again in January.\"\n />\n </Field>\n <div className=\"flex flex-wrap justify-end gap-2\">\n <Button\n variant=\"quiet\"\n onClick={() => {\n setMode(\"idle\");\n setError(null);\n }}\n disabled={busy !== null}\n >\n Back\n </Button>\n <Button type=\"submit\" variant=\"danger\" loading={busy === \"reject\"} disabled={reasonRequired && !reason.trim()}>\n {rejectLabel}\n </Button>\n </div>\n </form>\n ) : (\n <div className=\"flex gap-3\">\n {onReject ? (\n <Button\n className=\"flex-1\"\n onClick={() => {\n setError(null);\n if (reasonRequired) setMode(\"rejecting\");\n else decide(\"reject\");\n }}\n loading={busy === \"reject\"}\n disabled={busy !== null}\n >\n {rejectLabel}\n </Button>\n ) : null}\n {onApprove ? (\n <Button className=\"flex-1\" variant=\"primary\" icon=\"check\" onClick={() => decide(\"approve\")} loading={busy === \"approve\"} disabled={busy !== null}>\n {approveLabel}\n </Button>\n ) : null}\n </div>\n )\n ) : (\n <div role=\"status\" className=\"flex items-start gap-3 text-sm\">\n <Icon name={shown === \"approved\" ? \"circle-check\" : \"circle-x\"} size={20} className={cx(\"mt-0.5 shrink-0\", shown === \"approved\" ? \"text-success\" : \"text-destructive\")} />\n <div className=\"min-w-0\">\n <p className=\"font-medium\">\n {shown === \"approved\" ? \"Approved\" : \"Rejected\"}\n {by ? ` by ${by}` : \"\"}\n {at ? (\n <span className=\"font-normal text-muted-foreground\">\n {\" · \"}\n <When value={at} format={format} />\n </span>\n ) : null}\n </p>\n {note ? <p className=\"mt-0.5 text-muted-foreground\">{note}</p> : null}\n </div>\n </div>\n )}\n\n {error ? (\n <Notice tone=\"danger\" onDismiss={() => setError(null)}>\n {error}\n </Notice>\n ) : null}\n </section>\n );\n}\n\n/* ------------------------------------------------------------------------\n ApprovalRequest\n ------------------------------------------------------------------------ */\n\nexport function ApprovalRequest({\n variant = \"card\",\n title,\n kind,\n details,\n sections,\n requester,\n requestedAt,\n justification,\n status = \"pending\",\n decidedBy,\n decidedAt,\n decisionNote,\n onApprove,\n onReject,\n reasonRequired = true,\n approveLabel = \"Approve\",\n rejectLabel = \"Reject\",\n heading,\n description,\n formatDate = defaultFormat,\n loading = false,\n emptyLabel = \"Nothing is waiting for a decision.\",\n className,\n}) {\n const card = {\n title,\n kind,\n details,\n requester,\n requestedAt,\n justification,\n status,\n decidedBy,\n decidedAt,\n decisionNote,\n onApprove,\n onReject,\n reasonRequired,\n approveLabel,\n rejectLabel,\n format: formatDate,\n };\n\n if (loading) {\n return (\n <div aria-busy=\"true\" aria-label=\"Loading the request\" className={cx(\"grid w-full max-w-md gap-4 rounded-lg border border-border bg-card p-5\", className)}>\n <Skeleton className=\"w-2/3\" />\n {[0, 1, 2].map((i) => (\n <div key={i} className=\"flex items-center gap-3\">\n <Skeleton shape=\"rect\" className=\"size-8\" />\n <Skeleton className=\"w-1/2\" />\n </div>\n ))}\n <div className=\"flex gap-3\">\n <Skeleton shape=\"rect\" className=\"h-11 flex-1\" />\n <Skeleton shape=\"rect\" className=\"h-11 flex-1\" />\n </div>\n </div>\n );\n }\n if (!title) return <Empty icon=\"inbox\" title=\"No request\" body={emptyLabel} className={className} />;\n\n if (variant !== \"page\") {\n return <DecisionCard {...card} showContext className={cx(\"w-full max-w-md\", className)} />;\n }\n\n const groups = (sections ?? []).filter(Boolean);\n return (\n <div className={cx(\"grid gap-8 lg:grid-cols-[minmax(0,1fr)_24rem] lg:items-start\", className)}>\n <div className=\"grid min-w-0 gap-6\">\n <div className=\"grid gap-1\">\n <h1 className=\"text-2xl font-semibold leading-tight\">{heading ?? title}</h1>\n {description ? <p className=\"text-muted-foreground\">{description}</p> : null}\n </div>\n {groups.map((g, gi) => (\n <section key={g.title ?? gi} className=\"grid gap-4 border-t border-border pt-6\">\n {g.title ? <h2 className=\"text-lg font-semibold\">{g.title}</h2> : null}\n <dl className=\"grid gap-4 sm:grid-cols-2\">\n {(g.fields ?? []).filter(Boolean).map((f, fi) => (\n <div key={f.label ?? fi} className=\"grid min-w-0 gap-1\">\n <dt className=\"text-sm font-medium\">{f.label}</dt>\n <dd className={cx(\"min-w-0 break-words text-muted-foreground\", f.emphasis && \"font-semibold text-foreground\")}>{f.value}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n {requester || requestedAt || justification ? (\n <section className=\"grid gap-4 border-t border-border pt-6\">\n <h2 className=\"text-lg font-semibold\">The request</h2>\n <dl className=\"grid gap-4 sm:grid-cols-2\">\n {requester ? (\n <div className=\"grid min-w-0 gap-1\">\n <dt className=\"text-sm font-medium\">Requested by</dt>\n <dd className=\"flex min-w-0 items-center gap-2 text-muted-foreground\">\n <Avatar name={requester.name} src={requester.src} size=\"xs\" />\n <span className=\"min-w-0\">\n {requester.name}\n {requester.role ? ` (${requester.role})` : \"\"}\n </span>\n </dd>\n </div>\n ) : null}\n {requestedAt ? (\n <div className=\"grid gap-1\">\n <dt className=\"text-sm font-medium\">Requested on</dt>\n <dd className=\"text-muted-foreground\">\n <When value={requestedAt} format={formatDate} />\n </dd>\n </div>\n ) : null}\n {justification ? (\n <div className=\"grid gap-1 sm:col-span-2\">\n <dt className=\"text-sm font-medium\">Why</dt>\n <dd className=\"text-sm leading-relaxed text-muted-foreground\">{justification}</dd>\n </div>\n ) : null}\n </dl>\n </section>\n ) : null}\n </div>\n <div className=\"lg:sticky lg:top-6\">\n <DecisionCard {...card} showContext={false} />\n </div>\n </div>\n );\n}\n"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "registry/blocks/approval-request/README.md",
|
|
26
|
+
"type": "registry:file",
|
|
27
|
+
"target": "@components/makersclaw/blocks/approval-request/README.md",
|
|
28
|
+
"content": "# ApprovalRequest\n\nOne request waiting on a decision: what is asked for (label and value\nfacts, the amount in large type), who asked and when, why, and Approve /\nReject. Reject asks for a reason the requester will see. The decision\ngoes out through your callbacks; while one runs its button spins, when it\nfails the card says why and the buttons stay, and once it lands the card\nshows the outcome (\"Approved by you · 23 September 2026\") in their place.\n\n```jsx\nimport { ApprovalRequest } from \"../components/blocks/approval-request\"; // a Makersclaw app (\"./blocks/approval-request\" from components/)\nimport { ApprovalRequest } from \"@/components/makersclaw/blocks/approval-request\"; // a shadcn project\n```\n\nA client component. Load the request in the server surface (README §4)\nand pass its fields in.\n\n## The data\n\n- `details` — the card's facts, in order:\n `[{ label, value, icon?, emphasis? }]`. `value` is a string or a node;\n `icon` is a kit `Icon` name; `emphasis` sets the value large (the\n amount).\n- `sections` — `variant=\"page\"` only, the detail beside the card:\n `[{ title, fields: [{ label, value, emphasis? }] }]`.\n- `requester` — `{ name, role?, src? }` (`src` a picture URL).\n\n## Props\n\n- `title` — what is asked for (\"Linear Business\", \"Refund for order\n 20931\"). Without it (and not `loading`) the block shows `emptyLabel`.\n- `onApprove()` — save the yes. Return `{ ok: true }` or\n `{ ok: false, error }`; a throw counts as failed and its message shows.\n- `onReject(reason)` — save the no, with the reason typed (a trimmed\n string, \"\" when `reasonRequired` is off). Same return as `onApprove`.\n Leave either out and its button is not drawn.\n- `variant` — `\"card\"` (default: the card alone, with the requester, the\n date and the reason why) or `\"page\"`: a heading, the `sections` and \"The\n request\" (requester, date, why) on the left, the card on the right\n (under them on a phone), sticky from 1024px.\n- `status` — `\"pending\"` (default) · `\"approved\"` · `\"rejected\"`. The\n block shows its own outcome as soon as a decision saves; a new\n `status` from you replaces it.\n- `decidedBy` · `decidedAt` · `decisionNote` — for a request decided\n before: who, when, and the reason, shown under the outcome.\n- `kind` — a small badge beside the title (\"Per seat\", \"Refund\").\n- `requestedAt` — an ISO date or a `Date`.\n- `justification` — the requester's reason, as a paragraph.\n- `reasonRequired` — default `true`: Reject opens a reason field and\n sends only when it is filled. `false`: Reject sends at once.\n- `approveLabel` · `rejectLabel` — default \"Approve\" / \"Reject\"\n (\"Refund €149.00\", \"Decline\").\n- `heading` · `description` — `page`: the page's title (default `title`)\n and the line under it.\n- `formatDate(value)` — how a date reads; default the viewer's long date\n (\"21 September 2026\").\n- `loading` — a skeleton card while the request loads.\n- `emptyLabel` — default \"Nothing is waiting for a decision.\".\n- `className` — layout around it (width, margin). Never colour.\n\n## Example\n\n```jsx\n// components/PurchaseApproval.jsx — client\n\"use client\";\nimport { useRouter } from \"next/navigation\";\nimport { updateRow } from \"../lib/actions\";\nimport { ApprovalRequest } from \"./blocks/approval-request\";\n\nconst usd = new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency: \"USD\" });\n\nexport default function PurchaseApproval({ request }) {\n const router = useRouter();\n const decide = (status, reason) =>\n updateRow({ source: \"public.purchase_requests\", id: request.id, patch: { status, reason: reason ?? null }, detail: request.product }).then(\n (res) => (res.ok && router.refresh(), res),\n );\n return (\n <ApprovalRequest\n title={request.product}\n kind={request.pricing}\n details={[\n { label: \"Starts\", value: request.starts_on, icon: \"calendar\" },\n { label: \"Seats\", value: `${request.seats} people`, icon: \"users\" },\n { label: \"Yearly cost\", value: usd.format(request.amount), icon: \"dollar\", emphasis: true },\n ]}\n requester={{ name: request.requested_by, role: request.team }}\n requestedAt={request.created_at}\n justification={request.why}\n status={request.status}\n decidedBy={request.decided_by}\n decidedAt={request.decided_at}\n decisionNote={request.reason}\n onApprove={() => decide(\"approved\")}\n onReject={(reason) => decide(\"rejected\", reason)}\n />\n );\n}\n```\n\n## What it will not do\n\n- List many requests: put them in a `data-table` and open one in this\n block (a `Sheet`), or render one per row.\n- Take a decision in several steps (two approvers, a delegate): `status`\n is one of three.\n- Edit the request: its facts are read-only here.\n\nA request it cannot be is your own component in `components/`, built\nfrom the kit — not an edit to this block. Its files are fixed: `npm run\ncheck` refuses a change, and `makersclaw add approval-request` restores\nthem.\n\n## Credits\n\nPorted from Creative Tim UI (https://github.com/creativetimofficial/ui, software-purchase-01 and software-purchase-card, MIT), rewritten on the Makersclaw kit.\n"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"categories": [
|
|
32
|
+
"approval",
|
|
33
|
+
"approve",
|
|
34
|
+
"reject",
|
|
35
|
+
"request",
|
|
36
|
+
"review",
|
|
37
|
+
"decision",
|
|
38
|
+
"purchase",
|
|
39
|
+
"expense",
|
|
40
|
+
"refund",
|
|
41
|
+
"time off",
|
|
42
|
+
"sign off",
|
|
43
|
+
"pending"
|
|
44
|
+
],
|
|
45
|
+
"docs": "Installed to components/makersclaw/blocks/approval-request. Import: import { ApprovalRequest } from \"@/components/makersclaw/blocks/approval-request\". Props and an example: components/makersclaw/blocks/approval-request/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
46
|
+
"meta": {
|
|
47
|
+
"makersclaw": {
|
|
48
|
+
"version": "0.3.0",
|
|
49
|
+
"useWhen": "Someone must say yes or no to one thing another person asked for: a purchase, a refund, time off, a discount, an access request, a published post. Not for a queue of many requests (a data-table with row actions) or a yes/no about the founder's own action (kit Confirm)."
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|