makersclaw 0.2.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/THIRD-PARTY-NOTICES.md +180 -0
- package/package.json +3 -2
- package/r/annotate.json +62 -0
- package/r/approval-request.json +52 -0
- package/r/audio-player.json +74 -0
- package/r/auth-form.json +55 -0
- package/r/board.json +10 -6
- package/r/calendar-view.json +105 -0
- package/r/cart.json +62 -0
- package/r/chart.json +83 -0
- package/r/chat.json +95 -0
- package/r/code-editor.json +94 -0
- package/r/comment-box.json +64 -0
- package/r/contact-section.json +53 -0
- package/r/core.json +205 -8
- package/r/csv-import.json +63 -0
- package/r/data-table.json +69 -0
- package/r/diagram.json +74 -0
- package/r/doc-editor.json +82 -0
- package/r/doc-page.json +61 -0
- package/r/email-render.json +73 -0
- package/r/faq.json +50 -0
- package/r/file-upload.json +54 -0
- package/r/flow-canvas.json +93 -0
- package/r/footer.json +52 -0
- package/r/gantt.json +70 -0
- package/r/job-panel.json +12 -7
- package/r/json-editor.json +66 -0
- package/r/map.json +89 -0
- package/r/menubar.json +52 -0
- package/r/mind-map.json +67 -0
- package/r/navigation-menu.json +52 -0
- package/r/order-detail.json +54 -0
- package/r/org-chart.json +66 -0
- package/r/page-builder.json +105 -0
- package/r/payment-methods.json +51 -0
- package/r/pdf-document.json +79 -0
- package/r/pdf-viewer.json +88 -0
- package/r/product-detail.json +76 -0
- package/r/product-grid.json +65 -0
- package/r/record-form.json +67 -0
- package/r/record-notes.json +55 -0
- package/r/registry.json +3411 -29
- package/r/search-dialog.json +51 -0
- package/r/settings-list.json +66 -0
- package/r/setup-checklist.json +51 -0
- package/r/share-dialog.json +52 -0
- package/r/sidebar.json +69 -0
- package/r/signature-pad.json +72 -0
- package/r/sketch-canvas.json +99 -0
- package/r/spreadsheet.json +90 -0
- package/r/stat-cards.json +66 -0
- package/r/task-list.json +63 -0
- package/r/task-model.json +98 -0
- package/r/testimonials.json +52 -0
- package/r/transaction-list.json +53 -0
- package/r/whiteboard.json +66 -0
- package/registry/blocks/annotate/README.md +179 -0
- package/registry/blocks/annotate/annotate-data.js +250 -0
- package/registry/blocks/annotate/annotate.jsx +646 -0
- package/registry/blocks/annotate/block.json +10 -0
- package/registry/blocks/annotate/demo.jsx +135 -0
- package/registry/blocks/annotate/index.js +15 -0
- package/registry/blocks/approval-request/README.md +116 -0
- package/registry/blocks/approval-request/approval-request.jsx +386 -0
- package/registry/blocks/approval-request/block.json +10 -0
- package/registry/blocks/approval-request/demo.jsx +127 -0
- package/registry/blocks/approval-request/index.js +4 -0
- package/registry/blocks/audio-player/README.md +167 -0
- package/registry/blocks/audio-player/audio-player.jsx +623 -0
- package/registry/blocks/audio-player/block.json +14 -0
- package/registry/blocks/audio-player/colors.js +109 -0
- package/registry/blocks/audio-player/demo.jsx +209 -0
- package/registry/blocks/audio-player/index.js +6 -0
- package/registry/blocks/audio-player/time.js +67 -0
- package/registry/blocks/audio-player/wavesurfer.js +9 -0
- package/registry/blocks/auth-form/README.md +126 -0
- package/registry/blocks/auth-form/auth-form.jsx +568 -0
- package/registry/blocks/auth-form/block.json +10 -0
- package/registry/blocks/auth-form/demo.jsx +131 -0
- package/registry/blocks/auth-form/index.js +3 -0
- package/registry/blocks/board/README.md +55 -5
- package/registry/blocks/board/block.json +2 -2
- package/registry/blocks/board/board.jsx +230 -86
- package/registry/blocks/board/demo.jsx +103 -24
- package/registry/blocks/calendar-view/README.md +180 -0
- package/registry/blocks/calendar-view/agenda.jsx +147 -0
- package/registry/blocks/calendar-view/block.json +76 -0
- package/registry/blocks/calendar-view/calendar-model.js +442 -0
- package/registry/blocks/calendar-view/calendar.jsx +627 -0
- package/registry/blocks/calendar-view/demo.jsx +195 -0
- package/registry/blocks/calendar-view/draft.jsx +49 -0
- package/registry/blocks/calendar-view/index.js +39 -0
- package/registry/blocks/calendar-view/month.jsx +346 -0
- package/registry/blocks/calendar-view/paint.js +69 -0
- package/registry/blocks/calendar-view/time-grid.jsx +527 -0
- package/registry/blocks/calendar-view/tray.jsx +87 -0
- package/registry/blocks/cart/README.md +146 -0
- package/registry/blocks/cart/block.json +10 -0
- package/registry/blocks/cart/cart.jsx +597 -0
- package/registry/blocks/cart/demo.jsx +98 -0
- package/registry/blocks/cart/index.js +4 -0
- package/registry/blocks/cart/money.js +28 -0
- package/registry/blocks/chart/README.md +316 -0
- package/registry/blocks/chart/block.json +10 -0
- package/registry/blocks/chart/cartesian.jsx +642 -0
- package/registry/blocks/chart/core.jsx +262 -0
- package/registry/blocks/chart/demo.jsx +357 -0
- package/registry/blocks/chart/heatmap.jsx +336 -0
- package/registry/blocks/chart/index.js +8 -0
- package/registry/blocks/chart/pie.jsx +245 -0
- package/registry/blocks/chart/sparkline.jsx +88 -0
- package/registry/blocks/chat/README.md +273 -0
- package/registry/blocks/chat/attachment.jsx +95 -0
- package/registry/blocks/chat/block.json +10 -0
- package/registry/blocks/chat/chat.jsx +132 -0
- package/registry/blocks/chat/composer.jsx +364 -0
- package/registry/blocks/chat/demo.jsx +578 -0
- package/registry/blocks/chat/header.jsx +34 -0
- package/registry/blocks/chat/index.js +9 -0
- package/registry/blocks/chat/message.jsx +450 -0
- package/registry/blocks/chat/scroller.jsx +247 -0
- package/registry/blocks/chat/text.jsx +104 -0
- package/registry/blocks/code-editor/README.md +210 -0
- package/registry/blocks/code-editor/block.json +22 -0
- package/registry/blocks/code-editor/code-editor.jsx +534 -0
- package/registry/blocks/code-editor/codemirror.js +38 -0
- package/registry/blocks/code-editor/demo.jsx +205 -0
- package/registry/blocks/code-editor/index.js +8 -0
- package/registry/blocks/code-editor/json.js +317 -0
- package/registry/blocks/code-editor/sql.js +136 -0
- package/registry/blocks/code-editor/text.js +101 -0
- package/registry/blocks/code-editor/theme.js +186 -0
- package/registry/blocks/comment-box/README.md +123 -0
- package/registry/blocks/comment-box/block.json +10 -0
- package/registry/blocks/comment-box/comment-body.jsx +217 -0
- package/registry/blocks/comment-box/comment-box.jsx +198 -0
- package/registry/blocks/comment-box/comment-time.jsx +77 -0
- package/registry/blocks/comment-box/demo.jsx +141 -0
- package/registry/blocks/comment-box/index.js +8 -0
- package/registry/blocks/contact-section/README.md +152 -0
- package/registry/blocks/contact-section/block.json +11 -0
- package/registry/blocks/contact-section/contact-section.jsx +509 -0
- package/registry/blocks/contact-section/demo.jsx +93 -0
- package/registry/blocks/contact-section/index.js +4 -0
- package/registry/blocks/csv-import/README.md +133 -0
- package/registry/blocks/csv-import/block.json +13 -0
- package/registry/blocks/csv-import/csv-import.jsx +479 -0
- package/registry/blocks/csv-import/demo.jsx +106 -0
- package/registry/blocks/csv-import/import.js +200 -0
- package/registry/blocks/csv-import/index.js +7 -0
- package/registry/blocks/data-table/README.md +386 -0
- package/registry/blocks/data-table/block.json +10 -0
- package/registry/blocks/data-table/data-table.jsx +1164 -0
- package/registry/blocks/data-table/demo.jsx +588 -0
- package/registry/blocks/data-table/index.js +5 -0
- package/registry/blocks/data-table/model.js +169 -0
- package/registry/blocks/diagram/README.md +108 -0
- package/registry/blocks/diagram/block.json +13 -0
- package/registry/blocks/diagram/demo.jsx +113 -0
- package/registry/blocks/diagram/diagram.jsx +481 -0
- package/registry/blocks/diagram/index.js +7 -0
- package/registry/blocks/diagram/mermaid.js +52 -0
- package/registry/blocks/diagram/palette.js +313 -0
- package/registry/blocks/diagram/source.js +98 -0
- package/registry/blocks/doc-editor/README.md +215 -0
- package/registry/blocks/doc-editor/block.json +15 -0
- package/registry/blocks/doc-editor/demo.jsx +225 -0
- package/registry/blocks/doc-editor/doc-editor.jsx +291 -0
- package/registry/blocks/doc-editor/doc.js +309 -0
- package/registry/blocks/doc-editor/index.js +7 -0
- package/registry/blocks/doc-editor/schema.jsx +56 -0
- package/registry/blocks/doc-editor/shadcn-classes.js +110 -0
- package/registry/blocks/doc-editor/theme.js +82 -0
- package/registry/blocks/doc-page/README.md +119 -0
- package/registry/blocks/doc-page/autosave.js +104 -0
- package/registry/blocks/doc-page/block.json +10 -0
- package/registry/blocks/doc-page/demo.jsx +137 -0
- package/registry/blocks/doc-page/doc-page.jsx +276 -0
- package/registry/blocks/doc-page/index.js +4 -0
- package/registry/blocks/email-render/README.md +151 -0
- package/registry/blocks/email-render/block.json +14 -0
- package/registry/blocks/email-render/demo.jsx +153 -0
- package/registry/blocks/email-render/doc-email.jsx +210 -0
- package/registry/blocks/email-render/email.jsx +221 -0
- package/registry/blocks/email-render/index.js +10 -0
- package/registry/blocks/email-render/render.js +50 -0
- package/registry/blocks/email-render/theme.js +215 -0
- package/registry/blocks/faq/README.md +101 -0
- package/registry/blocks/faq/block.json +11 -0
- package/registry/blocks/faq/demo.jsx +89 -0
- package/registry/blocks/faq/faq.jsx +220 -0
- package/registry/blocks/faq/index.js +4 -0
- package/registry/blocks/file-upload/README.md +138 -0
- package/registry/blocks/file-upload/block.json +10 -0
- package/registry/blocks/file-upload/demo.jsx +204 -0
- package/registry/blocks/file-upload/file-upload.jsx +635 -0
- package/registry/blocks/file-upload/index.js +4 -0
- package/registry/blocks/flow-canvas/README.md +420 -0
- package/registry/blocks/flow-canvas/block.json +13 -0
- package/registry/blocks/flow-canvas/board.js +819 -0
- package/registry/blocks/flow-canvas/chrome.jsx +649 -0
- package/registry/blocks/flow-canvas/context.jsx +35 -0
- package/registry/blocks/flow-canvas/demo.jsx +249 -0
- package/registry/blocks/flow-canvas/edges.jsx +107 -0
- package/registry/blocks/flow-canvas/flow-canvas.jsx +1329 -0
- package/registry/blocks/flow-canvas/index.js +41 -0
- package/registry/blocks/flow-canvas/nodes.jsx +392 -0
- package/registry/blocks/flow-canvas/theme.js +174 -0
- package/registry/blocks/footer/README.md +133 -0
- package/registry/blocks/footer/block.json +11 -0
- package/registry/blocks/footer/demo.jsx +119 -0
- package/registry/blocks/footer/footer.jsx +406 -0
- package/registry/blocks/footer/index.js +4 -0
- package/registry/blocks/gantt/README.md +220 -0
- package/registry/blocks/gantt/block.json +10 -0
- package/registry/blocks/gantt/demo.jsx +148 -0
- package/registry/blocks/gantt/gantt.jsx +923 -0
- package/registry/blocks/gantt/index.js +7 -0
- package/registry/blocks/gantt/rows.jsx +468 -0
- package/registry/blocks/gantt/schedule.js +278 -0
- package/registry/blocks/job-panel/README.md +80 -0
- package/registry/blocks/job-panel/block.json +3 -3
- package/registry/blocks/job-panel/demo.jsx +230 -3
- package/registry/blocks/job-panel/job-panel.jsx +169 -3
- package/registry/blocks/json-editor/README.md +133 -0
- package/registry/blocks/json-editor/block.json +10 -0
- package/registry/blocks/json-editor/demo.jsx +92 -0
- package/registry/blocks/json-editor/index.js +7 -0
- package/registry/blocks/json-editor/json-editor.jsx +305 -0
- package/registry/blocks/json-editor/json-tree.jsx +393 -0
- package/registry/blocks/json-editor/tree.js +225 -0
- package/registry/blocks/map/README.md +221 -0
- package/registry/blocks/map/block.json +14 -0
- package/registry/blocks/map/demo.jsx +201 -0
- package/registry/blocks/map/geo.js +198 -0
- package/registry/blocks/map/index.js +8 -0
- package/registry/blocks/map/list.jsx +128 -0
- package/registry/blocks/map/map-view.jsx +715 -0
- package/registry/blocks/map/maplibre.js +30 -0
- package/registry/blocks/map/marks.jsx +146 -0
- package/registry/blocks/map/theme.js +142 -0
- package/registry/blocks/menubar/README.md +109 -0
- package/registry/blocks/menubar/block.json +10 -0
- package/registry/blocks/menubar/demo.jsx +128 -0
- package/registry/blocks/menubar/index.js +4 -0
- package/registry/blocks/menubar/menubar.jsx +199 -0
- package/registry/blocks/mind-map/README.md +206 -0
- package/registry/blocks/mind-map/block.json +10 -0
- package/registry/blocks/mind-map/demo.jsx +103 -0
- package/registry/blocks/mind-map/index.js +31 -0
- package/registry/blocks/mind-map/mind-map.jsx +1125 -0
- package/registry/blocks/mind-map/topic.jsx +143 -0
- package/registry/blocks/mind-map/tree.js +645 -0
- package/registry/blocks/navigation-menu/README.md +124 -0
- package/registry/blocks/navigation-menu/block.json +10 -0
- package/registry/blocks/navigation-menu/demo.jsx +84 -0
- package/registry/blocks/navigation-menu/index.js +4 -0
- package/registry/blocks/navigation-menu/navigation-menu.jsx +448 -0
- package/registry/blocks/order-detail/README.md +109 -0
- package/registry/blocks/order-detail/block.json +10 -0
- package/registry/blocks/order-detail/demo.jsx +88 -0
- package/registry/blocks/order-detail/index.js +4 -0
- package/registry/blocks/order-detail/order-detail.jsx +354 -0
- package/registry/blocks/org-chart/README.md +145 -0
- package/registry/blocks/org-chart/block.json +10 -0
- package/registry/blocks/org-chart/demo.jsx +94 -0
- package/registry/blocks/org-chart/index.js +7 -0
- package/registry/blocks/org-chart/org-chart.jsx +631 -0
- package/registry/blocks/org-chart/org.js +245 -0
- package/registry/blocks/org-chart/person.jsx +96 -0
- package/registry/blocks/page-builder/README.md +257 -0
- package/registry/blocks/page-builder/block.json +14 -0
- package/registry/blocks/page-builder/demo.jsx +145 -0
- package/registry/blocks/page-builder/grapes.js +18 -0
- package/registry/blocks/page-builder/index.js +11 -0
- package/registry/blocks/page-builder/page-builder.jsx +704 -0
- package/registry/blocks/page-builder/page-view.jsx +33 -0
- package/registry/blocks/page-builder/page.js +118 -0
- package/registry/blocks/page-builder/panels.jsx +588 -0
- package/registry/blocks/page-builder/sections.js +209 -0
- package/registry/blocks/page-builder/theme.js +87 -0
- package/registry/blocks/page-builder/thumbs.jsx +189 -0
- package/registry/blocks/page-builder/tokens.js +329 -0
- package/registry/blocks/payment-methods/README.md +104 -0
- package/registry/blocks/payment-methods/block.json +10 -0
- package/registry/blocks/payment-methods/demo.jsx +71 -0
- package/registry/blocks/payment-methods/index.js +4 -0
- package/registry/blocks/payment-methods/payment-methods.jsx +302 -0
- package/registry/blocks/pdf-document/README.md +213 -0
- package/registry/blocks/pdf-document/block.json +13 -0
- package/registry/blocks/pdf-document/demo.jsx +136 -0
- package/registry/blocks/pdf-document/index.js +33 -0
- package/registry/blocks/pdf-document/money.js +83 -0
- package/registry/blocks/pdf-document/pdf-document.jsx +144 -0
- package/registry/blocks/pdf-document/primitives.jsx +271 -0
- package/registry/blocks/pdf-document/quote.jsx +179 -0
- package/registry/blocks/pdf-document/theme.js +193 -0
- package/registry/blocks/pdf-viewer/README.md +191 -0
- package/registry/blocks/pdf-viewer/block.json +14 -0
- package/registry/blocks/pdf-viewer/demo.jsx +252 -0
- package/registry/blocks/pdf-viewer/helpers.js +270 -0
- package/registry/blocks/pdf-viewer/index.js +8 -0
- package/registry/blocks/pdf-viewer/notes.jsx +195 -0
- package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1368 -0
- package/registry/blocks/pdf-viewer/pdfjs.js +37 -0
- package/registry/blocks/pdf-viewer/ssr-shim-end.js +6 -0
- package/registry/blocks/pdf-viewer/ssr-shim.js +44 -0
- package/registry/blocks/product-detail/README.md +217 -0
- package/registry/blocks/product-detail/block.json +10 -0
- package/registry/blocks/product-detail/demo.jsx +233 -0
- package/registry/blocks/product-detail/index.js +7 -0
- package/registry/blocks/product-detail/price.jsx +115 -0
- package/registry/blocks/product-detail/product-detail.jsx +348 -0
- package/registry/blocks/product-detail/product-media.jsx +102 -0
- package/registry/blocks/product-detail/product-options.jsx +144 -0
- package/registry/blocks/product-grid/README.md +162 -0
- package/registry/blocks/product-grid/block.json +10 -0
- package/registry/blocks/product-grid/demo.jsx +255 -0
- package/registry/blocks/product-grid/index.js +5 -0
- package/registry/blocks/product-grid/product-card.jsx +232 -0
- package/registry/blocks/product-grid/product-grid.jsx +235 -0
- package/registry/blocks/record-form/README.md +196 -0
- package/registry/blocks/record-form/block.json +10 -0
- package/registry/blocks/record-form/demo.jsx +525 -0
- package/registry/blocks/record-form/index.js +4 -0
- package/registry/blocks/record-form/record-fields.jsx +457 -0
- package/registry/blocks/record-form/record-form.jsx +415 -0
- package/registry/blocks/record-form/rules.js +81 -0
- package/registry/blocks/record-notes/README.md +139 -0
- package/registry/blocks/record-notes/block.json +10 -0
- package/registry/blocks/record-notes/demo.jsx +179 -0
- package/registry/blocks/record-notes/index.js +4 -0
- package/registry/blocks/record-notes/record-notes.jsx +266 -0
- package/registry/blocks/search-dialog/README.md +108 -0
- package/registry/blocks/search-dialog/block.json +10 -0
- package/registry/blocks/search-dialog/demo.jsx +116 -0
- package/registry/blocks/search-dialog/index.js +3 -0
- package/registry/blocks/search-dialog/search-dialog.jsx +235 -0
- package/registry/blocks/settings-list/README.md +171 -0
- package/registry/blocks/settings-list/block.json +10 -0
- package/registry/blocks/settings-list/demo.jsx +333 -0
- package/registry/blocks/settings-list/index.js +4 -0
- package/registry/blocks/settings-list/row-state.jsx +93 -0
- package/registry/blocks/settings-list/settings-list.jsx +349 -0
- package/registry/blocks/settings-list/settings-matrix.jsx +118 -0
- package/registry/blocks/setup-checklist/README.md +142 -0
- package/registry/blocks/setup-checklist/block.json +10 -0
- package/registry/blocks/setup-checklist/demo.jsx +159 -0
- package/registry/blocks/setup-checklist/index.js +4 -0
- package/registry/blocks/setup-checklist/setup-checklist.jsx +446 -0
- package/registry/blocks/share-dialog/README.md +113 -0
- package/registry/blocks/share-dialog/block.json +10 -0
- package/registry/blocks/share-dialog/demo.jsx +140 -0
- package/registry/blocks/share-dialog/index.js +3 -0
- package/registry/blocks/share-dialog/share-dialog.jsx +384 -0
- package/registry/blocks/sidebar/README.md +190 -0
- package/registry/blocks/sidebar/block.json +43 -0
- package/registry/blocks/sidebar/demo.jsx +577 -0
- package/registry/blocks/sidebar/index.js +4 -0
- package/registry/blocks/sidebar/sidebar-panel.jsx +149 -0
- package/registry/blocks/sidebar/sidebar.jsx +864 -0
- package/registry/blocks/signature-pad/README.md +137 -0
- package/registry/blocks/signature-pad/block.json +13 -0
- package/registry/blocks/signature-pad/colors.js +115 -0
- package/registry/blocks/signature-pad/demo.jsx +94 -0
- package/registry/blocks/signature-pad/index.js +6 -0
- package/registry/blocks/signature-pad/pad.js +7 -0
- package/registry/blocks/signature-pad/signature-pad.jsx +434 -0
- package/registry/blocks/signature-pad/signature.js +68 -0
- package/registry/blocks/sketch-canvas/README.md +280 -0
- package/registry/blocks/sketch-canvas/block.json +13 -0
- package/registry/blocks/sketch-canvas/demo.jsx +159 -0
- package/registry/blocks/sketch-canvas/excalidraw.js +40 -0
- package/registry/blocks/sketch-canvas/index.js +9 -0
- package/registry/blocks/sketch-canvas/palette.js +226 -0
- package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1188 -0
- package/registry/blocks/sketch-canvas/sketch.js +179 -0
- package/registry/blocks/sketch-canvas/ssr-shim-end.js +6 -0
- package/registry/blocks/sketch-canvas/ssr-shim.js +45 -0
- package/registry/blocks/sketch-canvas/stamps.js +104 -0
- package/registry/blocks/sketch-canvas/theme.js +131 -0
- package/registry/blocks/spreadsheet/README.md +307 -0
- package/registry/blocks/spreadsheet/block.json +13 -0
- package/registry/blocks/spreadsheet/cells.jsx +455 -0
- package/registry/blocks/spreadsheet/chrome.jsx +325 -0
- package/registry/blocks/spreadsheet/demo.jsx +177 -0
- package/registry/blocks/spreadsheet/grid.js +387 -0
- package/registry/blocks/spreadsheet/index.js +34 -0
- package/registry/blocks/spreadsheet/spreadsheet.jsx +1732 -0
- package/registry/blocks/spreadsheet/types.js +473 -0
- package/registry/blocks/stat-cards/README.md +173 -0
- package/registry/blocks/stat-cards/block.json +10 -0
- package/registry/blocks/stat-cards/demo.jsx +257 -0
- package/registry/blocks/stat-cards/index.js +4 -0
- package/registry/blocks/stat-cards/stat-cards.jsx +307 -0
- package/registry/blocks/stat-cards/usage.jsx +243 -0
- package/registry/blocks/task-list/README.md +176 -0
- package/registry/blocks/task-list/block.json +10 -0
- package/registry/blocks/task-list/demo.jsx +134 -0
- package/registry/blocks/task-list/index.js +5 -0
- package/registry/blocks/task-list/task-list.jsx +907 -0
- package/registry/blocks/task-model/README.md +455 -0
- package/registry/blocks/task-model/block.json +167 -0
- package/registry/blocks/task-model/changes.js +89 -0
- package/registry/blocks/task-model/dates.js +371 -0
- package/registry/blocks/task-model/demo.jsx +215 -0
- package/registry/blocks/task-model/dnd.jsx +240 -0
- package/registry/blocks/task-model/index.js +97 -0
- package/registry/blocks/task-model/model.js +919 -0
- package/registry/blocks/task-model/parts.jsx +449 -0
- package/registry/blocks/task-model/toolbar.jsx +271 -0
- package/registry/blocks/testimonials/README.md +139 -0
- package/registry/blocks/testimonials/block.json +11 -0
- package/registry/blocks/testimonials/demo.jsx +137 -0
- package/registry/blocks/testimonials/index.js +4 -0
- package/registry/blocks/testimonials/testimonials.jsx +430 -0
- package/registry/blocks/transaction-list/README.md +126 -0
- package/registry/blocks/transaction-list/block.json +10 -0
- package/registry/blocks/transaction-list/demo.jsx +79 -0
- package/registry/blocks/transaction-list/index.js +4 -0
- package/registry/blocks/transaction-list/transaction-list.jsx +383 -0
- package/registry/blocks/whiteboard/README.md +179 -0
- package/registry/blocks/whiteboard/block.json +10 -0
- package/registry/blocks/whiteboard/demo.jsx +169 -0
- package/registry/blocks/whiteboard/index.js +6 -0
- package/registry/blocks/whiteboard/templates.js +150 -0
- package/registry/blocks/whiteboard/whiteboard.jsx +721 -0
- package/src/install.mjs +18 -0
- package/src/shadcn.mjs +8 -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/ui/avatar.jsx +141 -0
- package/templates/next/files/components/ui/breadcrumb.jsx +149 -0
- package/templates/next/files/components/ui/button-group.jsx +70 -0
- package/templates/next/files/components/ui/button.jsx +132 -12
- package/templates/next/files/components/ui/calendar.jsx +609 -0
- package/templates/next/files/components/ui/carousel.jsx +281 -0
- package/templates/next/files/components/ui/collapsible.jsx +208 -0
- package/templates/next/files/components/ui/combobox.jsx +608 -0
- package/templates/next/files/components/ui/command.jsx +577 -0
- package/templates/next/files/components/ui/context-menu.jsx +191 -0
- package/templates/next/files/components/ui/date-picker.jsx +360 -0
- package/templates/next/files/components/ui/distribution.jsx +117 -0
- package/templates/next/files/components/ui/feedback.jsx +143 -0
- package/templates/next/files/components/ui/field.jsx +115 -3
- package/templates/next/files/components/ui/file-input.jsx +234 -0
- package/templates/next/files/components/ui/icon.jsx +130 -8
- package/templates/next/files/components/ui/image.jsx +143 -0
- package/templates/next/files/components/ui/index.js +47 -2
- package/templates/next/files/components/ui/input-group.jsx +213 -0
- package/templates/next/files/components/ui/input-otp.jsx +170 -0
- package/templates/next/files/components/ui/item.jsx +206 -0
- package/templates/next/files/components/ui/menu.jsx +454 -82
- package/templates/next/files/components/ui/notice.jsx +126 -14
- package/templates/next/files/components/ui/pagination.jsx +156 -0
- package/templates/next/files/components/ui/popover.jsx +284 -0
- package/templates/next/files/components/ui/radio.jsx +193 -0
- package/templates/next/files/components/ui/rating.jsx +131 -0
- package/templates/next/files/components/ui/resizable.jsx +338 -0
- package/templates/next/files/components/ui/scroll-area.jsx +123 -0
- package/templates/next/files/components/ui/separator.jsx +119 -0
- package/templates/next/files/components/ui/sheet.jsx +96 -11
- package/templates/next/files/components/ui/slider.jsx +174 -0
- package/templates/next/files/components/ui/stat.jsx +574 -0
- package/templates/next/files/components/ui/steps.jsx +338 -0
- package/templates/next/files/components/ui/swatch.jsx +33 -0
- package/templates/next/files/components/ui/table.jsx +130 -0
- package/templates/next/files/components/ui/toast.jsx +440 -0
- package/templates/next/files/components/ui/toggle.jsx +195 -0
- package/templates/next/files/components/ui/tooltip.jsx +270 -0
- package/templates/next/template.json +264 -8
package/r/sidebar.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "sidebar",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Sidebar",
|
|
6
|
+
"description": "An app's navigation down the left edge: app name or workspace switcher, groups of items with icons, counts and nested items, the signed-in person with their menu; collapses to icons (remembered) and becomes a Sheet behind a trigger on a phone. Optional: inset or floating look, a ⌘K search over every item, folding groups with colour dots, a second column (a mail list, or a parent's children to drill into), a \"New\" button and a \"…\" menu per item (a chat history).",
|
|
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/sidebar/index.js",
|
|
14
|
+
"type": "registry:component",
|
|
15
|
+
"target": "@components/makersclaw/blocks/sidebar/index.js",
|
|
16
|
+
"content": "// The Sidebar block — `makersclaw add sidebar`. Fixed: compose it through\n// its props (README.md beside this file); a sidebar it cannot be is your\n// own component in components/.\nexport { Sidebar, SidebarLayout, SidebarTrigger, useSidebar } from \"./sidebar\";\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "registry/blocks/sidebar/sidebar.jsx",
|
|
20
|
+
"type": "registry:component",
|
|
21
|
+
"target": "@components/makersclaw/blocks/sidebar/sidebar.jsx",
|
|
22
|
+
"content": "\"use client\";\n\nimport { createContext, isValidElement, useContext, useEffect, useId, useState } from \"react\";\nimport {\n Avatar,\n Badge,\n Button,\n Collapsible,\n CommandDialog,\n Icon,\n IconButton,\n KbdGroup,\n Menu,\n ScrollArea,\n Separator,\n Sheet,\n StatusDot,\n Tooltip,\n cx,\n} from \"../../ui\";\nimport { PanelBody, guard, run } from \"./sidebar-panel\";\n\n// Sidebar — an app's navigation down the left edge: the app's name (and a\n// workspace switcher when there is more than one), groups of items with\n// icons, counts and nested items, a custom slot, and the signed-in person\n// at the foot with their menu. The block owns what a hand-drawn sidebar\n// gets wrong: the active item said to a screen reader (aria-current), a\n// collapse to icons that keeps every label (as a tooltip) and is\n// remembered, and a phone layout where the sidebar is a Sheet behind a\n// trigger instead of a column squeezing the page. The app owns the items\n// (data in) and what choosing one does (href or onSelect).\n//\n// <SidebarLayout storageKey=\"crm.sidebar\" sidebar={<Sidebar title=\"Harbor CRM\" groups={NAV} user={me} />}>\n// <header><SidebarTrigger /> …</header>\n// …the page…\n// </SidebarLayout>\n//\n// Beyond the column: inset and floating looks, a ⌘K search over every\n// item, folding groups, a second column (the app's panel, or a parent's\n// children to drill into), a primary button, and a \"…\" menu per item.\n//\n// README.md beside this file lists every prop.\n\nconst WIDE = \"(min-width: 768px)\";\n\nconst SidebarContext = createContext(null);\n\n/** The layout's state, for a control of your own: { collapsed,\n * setCollapsed, toggle, mobileOpen, setMobileOpen }. Null outside a\n * SidebarLayout. */\nexport function useSidebar() {\n return useContext(SidebarContext);\n}\n\nfunction readStored(key) {\n try {\n const v = window.localStorage.getItem(key);\n return v === \"1\" ? true : v === \"0\" ? false : null;\n } catch {\n return null;\n }\n}\n\nfunction writeStored(key, value) {\n try {\n window.localStorage.setItem(key, value ? \"1\" : \"0\");\n } catch {\n // private window, blocked storage: the choice lasts this visit only\n }\n}\n\nexport function SidebarLayout({\n sidebar,\n children,\n collapsed,\n defaultCollapsed = false,\n onCollapsedChange,\n storageKey,\n shortcut = \"b\",\n variant,\n className,\n}) {\n // The look is the Sidebar's `variant` unless the layout is given one:\n // inset needs the page drawn as a card, so the frame has to know.\n const look = variant ?? (isValidElement(sidebar) ? sidebar.props?.variant : undefined);\n const controlled = collapsed !== undefined;\n const [own, setOwn] = useState(defaultCollapsed);\n const [mobileOpen, setMobileOpen] = useState(false);\n const isCollapsed = controlled ? collapsed : own;\n\n // Stored after mount: the server has no storage, so the first paint is\n // the default and the remembered width follows at once.\n useEffect(() => {\n if (!storageKey || controlled) return;\n const stored = readStored(storageKey);\n if (stored !== null) setOwn(stored);\n }, [storageKey, controlled]);\n\n // Widening past the phone layout closes the sheet: the column is back.\n useEffect(() => {\n const mq = window.matchMedia(WIDE);\n const onChange = () => mq.matches && setMobileOpen(false);\n mq.addEventListener(\"change\", onChange);\n return () => mq.removeEventListener(\"change\", onChange);\n }, []);\n\n function setCollapsed(next) {\n const value = typeof next === \"function\" ? next(isCollapsed) : Boolean(next);\n if (!controlled) setOwn(value);\n if (storageKey) writeStored(storageKey, value);\n onCollapsedChange?.(value);\n }\n\n function toggle() {\n if (window.matchMedia(WIDE).matches) setCollapsed(!isCollapsed);\n else setMobileOpen((o) => !o);\n }\n\n // Cmd/Ctrl + B, as in most editors — never while typing in a field.\n useEffect(() => {\n if (!shortcut) return undefined;\n const onKey = (e) => {\n if (!(e.metaKey || e.ctrlKey) || e.altKey || e.shiftKey || e.key.toLowerCase() !== shortcut) return;\n if (e.target.closest?.(\"input, textarea, select, [contenteditable='true']\")) return;\n e.preventDefault();\n toggle();\n };\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n });\n\n const value = { collapsed: isCollapsed, setCollapsed, toggle, mobileOpen, setMobileOpen };\n return (\n <SidebarContext.Provider value={value}>\n <div className={cx(\"flex w-full text-foreground\", look === \"inset\" ? \"bg-background md:bg-muted/40\" : \"bg-background\", className)}>\n {sidebar}\n <div\n className={cx(\n \"flex min-w-0 flex-1 flex-col\",\n look === \"inset\" && \"bg-background md:my-2 md:mr-2 md:rounded-xl md:border md:border-border md:shadow-sm\",\n )}\n >\n {children}\n </div>\n </div>\n </SidebarContext.Provider>\n );\n}\n\n/** The one button that opens the sidebar on a phone and collapses it to\n * icons on a wider window. Put it in the page's header. */\nexport function SidebarTrigger({ className }) {\n const ctx = useSidebar();\n if (!ctx) return null;\n return (\n <>\n <IconButton\n icon=\"menu\"\n label=\"Open navigation\"\n aria-expanded={ctx.mobileOpen}\n onClick={() => ctx.setMobileOpen(true)}\n className={cx(\"md:hidden\", className)}\n />\n <IconButton\n icon=\"panel-left\"\n label={ctx.collapsed ? \"Expand sidebar\" : \"Collapse sidebar\"}\n aria-expanded={!ctx.collapsed}\n onClick={() => ctx.setCollapsed(!ctx.collapsed)}\n className={cx(\"max-md:hidden\", className)}\n />\n </>\n );\n}\n\n\nconst keyOf = (item) => String(item.key ?? item.href ?? item.label);\nconst hasActive = (item) => Boolean(item.active || item.items?.some(hasActive));\nconst shows = (badge) => badge != null && badge !== \"\";\n\n// The icon rail's active look rides on aria-current, so the state a reader\n// hears is the state the eye sees. (A wide row is a nav Button, which\n// paints its own aria-current; an icon button is square, not a nav row.)\nconst CURRENT = \"aria-[current=page]:bg-accent aria-[current=page]:text-accent-foreground aria-[current=page]:font-semibold\";\n\n// The column's frame, per variant: a bordered column (today), a column on\n// the frame's own ground beside a page drawn as a card (inset), or a\n// rounded card held off the window's edge (floating).\nconst SHELLS = {\n sidebar: \"sticky top-0 max-h-dvh self-stretch border-r border-border bg-muted/40\",\n inset: \"sticky top-0 max-h-dvh self-stretch\",\n floating: \"sticky top-2 m-2 max-h-[calc(100dvh-1rem)] self-stretch overflow-hidden rounded-xl border border-border bg-card shadow-sm\",\n};\n\n// A row's \"…\" shows on hover or focus on a wide window, always where\n// there is no hover (a touch screen), and stays while its menu is open.\nconst REVEAL =\n \"opacity-0 transition-opacity motion-reduce:transition-none group-hover/row:opacity-100 group-focus-within/row:opacity-100 \" +\n \"has-[[aria-expanded=true]]:opacity-100 [@media(hover:none)]:opacity-100\";\n\n// An href-only item chosen from the ⌘K palette: a full page load, with\n// the Makersclaw app's base path when there is one.\nfunction follow(href) {\n if (typeof window === \"undefined\" || !href) return;\n const base = /^[a-z]+:|^\\/\\//i.test(href) ? \"\" : document.documentElement.dataset.clawBasePath ?? \"\";\n window.location.assign(base + href);\n}\n\nfunction focusSoon(id) {\n requestAnimationFrame(() => document.getElementById(id)?.focus());\n}\n\nexport function Sidebar({\n groups,\n items,\n title,\n subtitle,\n logo,\n switcher,\n switcherLabel = \"Switch workspace\",\n switcherPlacement = \"header\",\n user,\n userMenu,\n children,\n label = \"Main\",\n variant = \"sidebar\",\n headerActions,\n search,\n primaryAction,\n footerActions,\n footerActionsLabel = \"Create\",\n drilldown = \"inline\",\n panel,\n className,\n}) {\n const ctx = useSidebar();\n const uid = useId();\n const collapsed = Boolean(ctx?.collapsed);\n const list = (groups ?? (items ? [{ items }] : [])).filter((g) => g && g.items?.length);\n const drillMode = drilldown === \"panel\" || drilldown === \"replace\" ? drilldown : \"inline\";\n const shell = SHELLS[variant] ?? SHELLS.sidebar;\n // `panel` is a node of the app's own, or { title, search, items, … }.\n const panelIsNode = Boolean(panel) && (isValidElement(panel) || typeof panel !== \"object\" || Array.isArray(panel));\n\n // Open parents are held here so a click on a collapsed parent's icon can\n // widen the sidebar with that parent open. A parent holding the active\n // item starts open.\n const [open, setOpen] = useState(() => {\n const out = new Set();\n for (const g of list) for (const it of g.items) if (it.items?.length && hasActive(it)) out.add(keyOf(it));\n return out;\n });\n const setParent = (key, next) =>\n setOpen((s) => {\n const out = new Set(s);\n if (next) out.add(key);\n else out.delete(key);\n return out;\n });\n\n // Folding groups (group.collapsible): the ones closed, by group key.\n const groupKey = (group, gi) => String(group.key ?? group.label ?? gi);\n const [folded, setFolded] = useState(() => new Set(list.flatMap((g, gi) => (g.collapsible && g.defaultOpen === false ? [groupKey(g, gi)] : []))));\n const setGroup = (key, isOpen) =>\n setFolded((s) => {\n const out = new Set(s);\n if (isOpen) out.delete(key);\n else out.add(key);\n return out;\n });\n\n // Drill-in (drilldown=\"panel\" | \"replace\"): the parent whose children\n // are shown. Starts at the one holding the active item.\n const [drill, setDrill] = useState(() => {\n if (drillMode === \"inline\") return null;\n for (const g of list) for (const it of g.items) if (it.items?.length && hasActive(it)) return keyOf(it);\n return null;\n });\n const drillItem = drill == null ? null : list.flatMap((g) => g.items).find((it) => it.items?.length && keyOf(it) === drill) ?? null;\n\n // The phone sheet's second screen: the panel.\n const [sheetPanel, setSheetPanel] = useState(false);\n useEffect(() => {\n if (!ctx?.mobileOpen) setSheetPanel(false);\n }, [ctx?.mobileOpen]);\n\n // ⌘K search over every item.\n const [searching, setSearching] = useState(false);\n const searchOpts = search ? (typeof search === \"object\" ? search : {}) : null;\n const hotkey = searchOpts ? (searchOpts.hotkey === undefined ? \"k\" : searchOpts.hotkey) : null;\n const searchLabel = searchOpts?.label ?? \"Search\";\n\n function choose(item, { sheet } = {}) {\n item.onSelect?.(item);\n // With a panel, choosing on the phone shows the panel (the mails in\n // the folder just chosen) instead of closing the sheet.\n if (sheet && panel) setSheetPanel(true);\n else ctx?.setMobileOpen(false);\n }\n\n // A choice in the user menu also closes the phone sheet — through\n // groups and submenus too.\n const closingSheet = (entries) =>\n (entries ?? []).filter(Boolean).map((item) =>\n Array.isArray(item.items)\n ? { ...item, items: closingSheet(item.items) }\n : item.onSelect\n ? {\n ...item,\n onSelect: () => {\n ctx?.setMobileOpen(false);\n item.onSelect();\n },\n }\n : item,\n );\n\n const mark = logo ?? <Avatar name={title} shape=\"square\" size=\"sm\" />;\n\n function openDrill(key, where) {\n const next = drillMode === \"panel\" && drill === key ? null : key;\n setDrill(next);\n if (next == null) return;\n // Focus follows: to the back row (replace, or any drill on the phone),\n // or the first child in the second column.\n focusSoon(drillMode === \"replace\" || where === \"s\" ? `${uid}-${where}-back` : `${uid}-drill-first`);\n }\n\n function closeDrill(where) {\n const key = drill;\n setDrill(null);\n if (key != null) focusSoon(`${uid}-${where}-parent-${key}`);\n }\n\n // ---- one item, three ways: the wide column, the icon rail, the sheet --\n\n function leaf(item, { sheet, id }) {\n const current = item.active ? \"page\" : undefined;\n const button = (\n <Button\n id={id}\n variant=\"nav\"\n size={sheet ? \"md\" : \"sm\"}\n href={item.disabled ? undefined : item.href}\n onClick={() => choose(item, { sheet })}\n disabled={item.disabled}\n aria-current={current}\n className={item.description ? \"py-1.5\" : undefined}\n >\n {item.hue ? (\n <span className=\"inline-flex size-4 shrink-0 items-center justify-center\">\n <StatusDot hue={item.hue} size=\"md\" />\n </span>\n ) : item.icon ? (\n <Icon name={item.icon} className=\"shrink-0\" />\n ) : null}\n {item.description ? (\n <span className=\"grid min-w-0 flex-1 text-left leading-tight\">\n <span className=\"truncate\">{item.label}</span>\n <span className=\"truncate text-xs text-muted-foreground\">{item.description}</span>\n </span>\n ) : (\n <span className=\"min-w-0 flex-1 truncate text-left\">{item.label}</span>\n )}\n {shows(item.badge) ? <Badge className=\"shrink-0\">{item.badge}</Badge> : null}\n </Button>\n );\n if (!item.actions?.length) return button;\n return (\n <div className=\"group/row flex min-w-0 items-center gap-0.5\">\n {button}\n <span className={cx(\"shrink-0\", !sheet && REVEAL)}>\n <Menu items={guard(item.actions, item)} label={`Actions for ${item.label}`} size={sheet ? \"md\" : \"sm\"} />\n </span>\n </div>\n );\n }\n\n // A parent in drill-in mode: a row that opens its children (a second\n // column, or in place of the list) instead of folding them in.\n function drillRow(item, { sheet, where }) {\n const key = keyOf(item);\n const inPanel = drillMode === \"panel\" && !sheet;\n return (\n <Button\n id={`${uid}-${where}-parent-${key}`}\n variant=\"nav\"\n size={sheet ? \"md\" : \"sm\"}\n onClick={() => openDrill(key, where)}\n disabled={item.disabled}\n aria-current={hasActive(item) ? \"true\" : undefined}\n aria-expanded={inPanel ? drill === key : undefined}\n className={inPanel ? \"aria-expanded:bg-accent\" : undefined}\n >\n {item.icon ? <Icon name={item.icon} className=\"shrink-0\" /> : null}\n <span className=\"min-w-0 flex-1 truncate text-left\">{item.label}</span>\n {shows(item.badge) ? <Badge className=\"shrink-0\">{item.badge}</Badge> : null}\n <Icon name=\"chevron-right\" className=\"shrink-0 text-muted-foreground\" />\n </Button>\n );\n }\n\n function wideItem(item, opts) {\n if (!item.items?.length) return leaf(item, opts);\n if (drillMode !== \"inline\") return drillRow(item, opts);\n const key = keyOf(item);\n return (\n <div>\n <Collapsible\n open={open.has(key)}\n onOpenChange={(next) => setParent(key, next)}\n meta={shows(item.badge) ? item.badge : undefined}\n title={\n <span className=\"flex min-w-0 items-center gap-3\">\n {item.icon ? <Icon name={item.icon} className=\"shrink-0\" /> : null}\n <span className=\"truncate\">{item.label}</span>\n </span>\n }\n className={cx(\n \"[&>summary]:mx-0 [&>div]:pb-1 [&>div]:pt-0.5\",\n opts.sheet ? \"[&>summary]:px-4\" : \"[&>summary]:min-h-9 [&>summary]:px-3 [&>summary]:py-1\",\n )}\n >\n <ul className=\"ml-5 grid gap-0.5 border-l border-border pl-2\">\n {item.items.map((child) => (\n <li key={keyOf(child)}>{leaf(child, opts)}</li>\n ))}\n </ul>\n </Collapsible>\n </div>\n );\n }\n\n function iconItem(item) {\n const parent = item.items?.length > 0;\n const count = shows(item.badge) ? item.badge : null;\n const current = item.active || (parent && hasActive(item)) ? \"page\" : undefined;\n return (\n <Tooltip label={count != null ? `${item.label} (${count})` : item.label} side=\"right\">\n <span className=\"relative inline-flex\">\n <IconButton\n icon={item.icon ?? \"file\"}\n label={item.label}\n size=\"sm\"\n href={parent || item.disabled ? undefined : item.href}\n disabled={item.disabled}\n aria-current={current}\n aria-expanded={parent && drillMode === \"panel\" ? drill === keyOf(item) : undefined}\n onClick={() => {\n if (!parent) return choose(item);\n // Drill-in to a column: the rail stays narrow beside it.\n if (drillMode === \"panel\") return openDrill(keyOf(item), \"r\");\n // Otherwise a parent's children need labels: widen, with it open.\n if (drillMode === \"replace\") setDrill(keyOf(item));\n else setParent(keyOf(item), true);\n ctx?.setCollapsed(false);\n }}\n className={cx(CURRENT, drillMode === \"panel\" && \"aria-expanded:bg-accent\")}\n />\n {count != null ? (\n <span aria-hidden=\"true\" className=\"pointer-events-none absolute right-1 top-1 size-1.5 rounded-full bg-primary\" />\n ) : null}\n </span>\n </Tooltip>\n );\n }\n\n // A drilled parent's children in place of the list: a back row, the\n // parent's name, then its children.\n function replaced(item, { sheet, where }) {\n const headingId = `${uid}-${where}-drill`;\n return (\n <div className=\"grid gap-0.5\">\n <Button id={`${uid}-${where}-back`} variant=\"nav\" size={sheet ? \"md\" : \"sm\"} onClick={() => closeDrill(where)}>\n <Icon name=\"arrow-left\" className=\"shrink-0\" />\n <span className=\"min-w-0 flex-1 truncate text-left\">Back</span>\n </Button>\n <h2 id={headingId} className=\"flex h-8 items-center px-3 text-xs font-medium text-muted-foreground\">\n {item.label}\n </h2>\n <ul aria-labelledby={headingId} className=\"grid gap-0.5\">\n {item.items.map((child) => (\n <li key={keyOf(child)}>{leaf(child, { sheet })}</li>\n ))}\n </ul>\n </div>\n );\n }\n\n function primary({ icons, sheet }) {\n if (!primaryAction) return null;\n const act = () => {\n ctx?.setMobileOpen(false);\n return run(primaryAction.onSelect, primaryAction, primaryAction.label);\n };\n if (icons) {\n return (\n <Tooltip label={primaryAction.label} side=\"right\">\n <IconButton icon={primaryAction.icon ?? \"plus\"} label={primaryAction.label} size=\"sm\" variant=\"secondary\" href={primaryAction.href} onClick={act} />\n </Tooltip>\n );\n }\n return (\n <Button variant=\"secondary\" size={sheet ? \"md\" : \"sm\"} align=\"start\" icon={primaryAction.icon ?? \"plus\"} href={primaryAction.href} onClick={act} className=\"w-full\">\n {primaryAction.label}\n </Button>\n );\n }\n\n function nav({ sheet }) {\n const icons = collapsed && !sheet;\n const where = sheet ? \"s\" : \"r\";\n // Replace mode (and any drill-in on the phone): the drilled parent's\n // children stand in for the list.\n const drilledHere = drillItem && !icons && (drillMode === \"replace\" || (sheet && drillMode === \"panel\"));\n return (\n <nav aria-label={label} className={cx(\"grid gap-3\", icons ? \"justify-items-center py-2\" : \"px-2 py-2\")}>\n {primaryAction && !drilledHere ? primary({ icons, sheet }) : null}\n {drilledHere\n ? replaced(drillItem, { sheet, where })\n : list.map((group, gi) => {\n const headingId = `${uid}-${where}-${gi}`;\n const gk = groupKey(group, gi);\n const rows = (\n <ul\n aria-labelledby={group.label && !icons && !group.collapsible ? headingId : undefined}\n aria-label={group.label && !icons && group.collapsible ? group.label : undefined}\n className={cx(\"grid gap-0.5\", icons && \"justify-items-center\")}\n >\n {group.items.map((item) => (\n <li key={keyOf(item)}>{icons ? iconItem(item) : wideItem(item, { sheet, where })}</li>\n ))}\n </ul>\n );\n if (group.collapsible && group.label && !icons) {\n return (\n <Collapsible\n key={gk}\n open={!folded.has(gk)}\n onOpenChange={(next) => setGroup(gk, next)}\n meta={shows(group.badge) ? group.badge : undefined}\n title={<span className=\"truncate text-xs font-medium text-muted-foreground\">{group.label}</span>}\n className={cx(\"[&>summary]:mx-0 [&>div]:pb-0 [&>div]:pt-0.5\", sheet ? \"[&>summary]:px-4\" : \"[&>summary]:min-h-8 [&>summary]:px-3 [&>summary]:py-1\")}\n >\n {rows}\n </Collapsible>\n );\n }\n return (\n <div key={group.key ?? group.label ?? gi} className=\"grid gap-0.5\">\n {group.label && !icons ? (\n <h2 id={headingId} className=\"flex h-8 items-center px-3 text-xs font-medium text-muted-foreground\">\n {group.label}\n </h2>\n ) : icons && gi > 0 ? (\n <Separator className=\"mb-2 w-6\" />\n ) : null}\n {rows}\n </div>\n );\n })}\n {children && !icons && !drilledHere ? <div className=\"px-2\">{children}</div> : null}\n </nav>\n );\n }\n\n function brand({ icons, side = \"bottom\" }) {\n const words = (\n <span className=\"grid min-w-0 flex-1 text-left leading-tight\">\n <span className=\"truncate text-sm font-semibold\">{title}</span>\n {subtitle ? <span className=\"truncate text-xs font-normal text-muted-foreground\">{subtitle}</span> : null}\n </span>\n );\n if (switcher?.length) {\n return (\n <Menu\n items={switcher}\n label={`${title}, ${switcherLabel}`}\n side={side === \"top\" && icons ? \"right\" : side}\n align={side === \"top\" && icons ? \"end\" : \"start\"}\n className={cx(\"min-h-12\", icons ? \"px-1.5\" : \"w-full px-2\")}\n trigger={\n <>\n {mark}\n {icons ? null : words}\n {icons ? null : <Icon name=\"chevrons-up-down\" className=\"shrink-0 text-muted-foreground\" />}\n </>\n }\n />\n );\n }\n return (\n <div className={cx(\"flex min-h-12 items-center gap-2\", icons ? \"justify-center\" : \"px-2\")}>\n {icons ? <Tooltip label={title} side=\"right\">{mark}</Tooltip> : mark}\n {icons ? null : words}\n </div>\n );\n }\n\n // The top of the column: the brand (or, with the switcher in the foot,\n // only the mark), then headerActions — a bell, a trigger.\n function head({ icons }) {\n const footSwitch = switcherPlacement === \"footer\" && switcher?.length;\n const top = footSwitch ? (\n <div className={cx(\"flex min-h-12 items-center\", icons ? \"justify-center\" : \"px-2\")}>\n {icons ? <Tooltip label={title} side=\"right\">{mark}</Tooltip> : mark}\n </div>\n ) : (\n brand({ icons })\n );\n if (icons || !headerActions) return top;\n return (\n <div className=\"flex items-center gap-1\">\n <div className=\"min-w-0 flex-1\">{top}</div>\n <div className=\"flex shrink-0 items-center gap-1\">{headerActions}</div>\n </div>\n );\n }\n\n function searchRow({ icons, sheet }) {\n if (!searchOpts) return null;\n const openIt = () => {\n ctx?.setMobileOpen(false);\n setSearching(true);\n };\n const keys = hotkey ? `Meta+${String(hotkey).toUpperCase()}` : undefined;\n if (icons) {\n return (\n <Tooltip label={searchLabel} side=\"right\">\n <IconButton icon=\"search\" label={searchLabel} size=\"sm\" onClick={openIt} aria-keyshortcuts={keys} />\n </Tooltip>\n );\n }\n return (\n <Button variant=\"secondary\" size={sheet ? \"md\" : \"sm\"} align=\"start\" onClick={openIt} aria-keyshortcuts={keys} className=\"w-full\">\n <Icon name=\"search\" className=\"shrink-0 text-muted-foreground\" />\n <span className=\"min-w-0 flex-1 truncate text-left font-normal text-muted-foreground\">{searchOpts.placeholder ?? `${searchLabel}…`}</span>\n {hotkey && !sheet ? <KbdGroup keys={[\"⌘\", String(hotkey).toUpperCase()]} /> : null}\n </Button>\n );\n }\n\n // Every item, for the palette: a group's items under its label, a\n // parent's children under the parent's name.\n const searchItems = searchOpts\n ? list.flatMap((group, gi) =>\n group.items.flatMap((item) => {\n const own = (it, parent) => ({\n value: `${gi}:${parent ? `${keyOf(parent)}/` : \"\"}${keyOf(it)}`,\n label: it.label,\n description: parent ? parent.label : it.description,\n icon: it.hue ? <StatusDot hue={it.hue} /> : it.icon,\n group: group.label ?? searchOpts.groupLabel ?? \"Go to\",\n keywords: it.keywords,\n disabled: it.disabled,\n onSelect: () => {\n if (it.onSelect) it.onSelect(it);\n else follow(it.href);\n },\n });\n if (!item.items?.length) return [own(item)];\n return [...(item.onSelect || item.href ? [own(item)] : []), ...item.items.map((child) => own(child, item))];\n }),\n )\n : [];\n\n function person({ icons }) {\n if (!user) return null;\n const face = <Avatar name={user.name} src={user.src} size=\"sm\" />;\n const words = (\n <span className=\"grid min-w-0 flex-1 text-left leading-tight\">\n <span className=\"truncate text-sm font-medium\">{user.name}</span>\n {user.email ? <span className=\"truncate text-xs font-normal text-muted-foreground\">{user.email}</span> : null}\n </span>\n );\n if (!userMenu?.length) {\n return <div className={cx(\"flex min-h-12 items-center gap-2\", icons ? \"justify-center\" : \"px-2\")}>{face}{icons ? null : words}</div>;\n }\n // Opens upward from the foot of the window (beside the rail when it is\n // icons), and flips where there is no room.\n return (\n <Menu\n side={icons ? \"right\" : \"top\"}\n align={icons ? \"end\" : \"start\"}\n label={`${user.name}, account menu`}\n items={closingSheet(userMenu)}\n className={cx(\"min-h-12\", icons ? \"px-1.5\" : \"w-full px-2\")}\n trigger={\n <>\n {face}\n {icons ? null : words}\n {icons ? null : <Icon name=\"chevrons-up-down\" className=\"shrink-0 text-muted-foreground\" />}\n </>\n }\n />\n );\n }\n\n // The foot: the switcher (when it lives here), the person, and the \"+\"\n // menu of footerActions beside them.\n function foot({ icons, sheet }) {\n const footSwitch = switcherPlacement === \"footer\" && switcher?.length && !sheet;\n const plus = footerActions?.length ? (\n <Menu\n items={closingSheet(guard(footerActions))}\n label={footerActionsLabel}\n side={icons ? \"right\" : \"top\"}\n align=\"end\"\n size={sheet ? \"md\" : \"sm\"}\n trigger={<Icon name=\"plus\" />}\n />\n ) : null;\n if (!user && !plus && !footSwitch) return null;\n return (\n <div className=\"grid gap-1\">\n {footSwitch ? brand({ icons, side: \"top\" }) : null}\n {user || plus ? (\n <div className={cx(\"flex items-center gap-1\", icons && \"flex-col\")}>\n {user ? <div className={cx(\"min-w-0\", !icons && \"flex-1\")}>{person({ icons })}</div> : null}\n {plus}\n </div>\n ) : null}\n </div>\n );\n }\n\n // The second column from 768px: a drilled parent's children\n // (drilldown=\"panel\"), else the panel.\n function secondColumn() {\n if (drillMode === \"panel\" && drillItem) {\n const headingId = `${uid}-drill-title`;\n return (\n <div\n className=\"flex min-h-0 flex-col\"\n onKeyDown={(e) => {\n if (e.key === \"Escape\") {\n e.stopPropagation();\n closeDrill(\"r\");\n }\n }}\n >\n <div className=\"flex min-h-14 shrink-0 items-center gap-2 border-b border-border px-3\">\n <h2 id={headingId} className=\"min-w-0 flex-1 truncate text-sm font-semibold\">\n {drillItem.label}\n </h2>\n <IconButton icon=\"x\" label={`Close ${drillItem.label}`} size=\"sm\" onClick={() => closeDrill(\"r\")} />\n </div>\n <ScrollArea label={drillItem.label} className=\"min-h-0 flex-1\" fade>\n <ul aria-labelledby={headingId} className=\"grid gap-0.5 p-2\">\n {drillItem.items.map((child, i) => (\n <li key={keyOf(child)}>{leaf(child, { sheet: false, id: i === 0 ? `${uid}-drill-first` : undefined })}</li>\n ))}\n </ul>\n </ScrollArea>\n </div>\n );\n }\n if (!panel) return null;\n if (panelIsNode) return panel;\n return <PanelBody panel={panel} onChoose={() => ctx?.setMobileOpen(false)} />;\n }\n\n const inLayout = Boolean(ctx);\n const second = secondColumn();\n const hasFoot = Boolean(user || footerActions?.length || (switcherPlacement === \"footer\" && switcher?.length));\n return (\n <>\n <div\n data-sidebar=\"\"\n data-collapsed={collapsed ? \"\" : undefined}\n data-variant={variant !== \"sidebar\" ? variant : undefined}\n className={cx(inLayout ? \"hidden md:flex\" : \"flex\", \"shrink-0\", shell, className)}\n >\n <div\n className={cx(\n \"flex min-h-0 shrink-0 flex-col transition-[width] duration-200 ease-out motion-reduce:transition-none\",\n collapsed ? \"w-14\" : \"w-64\",\n )}\n >\n <div className={cx(\"shrink-0 py-2\", collapsed ? \"flex justify-center\" : \"px-2\")}>{head({ icons: collapsed })}</div>\n {searchOpts ? <div className={cx(\"shrink-0 pb-1\", collapsed ? \"flex justify-center\" : \"px-2\")}>{searchRow({ icons: collapsed })}</div> : null}\n <ScrollArea label={`${label} navigation`} className=\"flex-1\" fade>\n {nav({ sheet: false })}\n </ScrollArea>\n {hasFoot ? (\n <div className={cx(\"shrink-0 border-t border-border py-2\", collapsed ? \"flex justify-center\" : \"px-2\")}>\n {foot({ icons: collapsed })}\n </div>\n ) : null}\n </div>\n {second ? (\n <div className={cx(\"flex w-80 min-h-0 shrink-0 flex-col border-l border-border\", variant === \"sidebar\" && \"bg-background\")}>{second}</div>\n ) : null}\n </div>\n {searchOpts ? (\n <CommandDialog\n open={searching}\n onOpenChange={setSearching}\n hotkey={hotkey || undefined}\n label={searchOpts.label ?? `Search ${title ?? label}`}\n placeholder={searchOpts.placeholder ?? \"Go to…\"}\n empty={searchOpts.empty ?? \"Nothing by that name.\"}\n items={searchItems}\n />\n ) : null}\n {inLayout ? (\n <Sheet\n side=\"start\"\n open={ctx.mobileOpen}\n onClose={() => ctx.setMobileOpen(false)}\n title={title}\n description={subtitle}\n footer={user || footerActions?.length ? <div className=\"w-full\">{foot({ icons: false, sheet: true })}</div> : null}\n >\n {ctx.mobileOpen ? (\n sheetPanel && panel ? (\n <div className=\"-mx-2 -my-2 grid gap-2\">\n <div className=\"px-2 pt-2\">\n <Button variant=\"nav\" size=\"md\" onClick={() => setSheetPanel(false)}>\n <Icon name=\"arrow-left\" className=\"shrink-0\" />\n <span className=\"min-w-0 flex-1 truncate text-left\">Back</span>\n </Button>\n </div>\n {panelIsNode ? panel : <PanelBody panel={panel} sheet onChoose={() => ctx.setMobileOpen(false)} />}\n </div>\n ) : (\n <div className=\"-mx-2 -my-2 grid gap-2\">\n {headerActions ? <div className=\"flex flex-wrap items-center gap-1 px-2\">{headerActions}</div> : null}\n {searchOpts ? <div className=\"px-2\">{searchRow({ icons: false, sheet: true })}</div> : null}\n {switcher?.length ? (\n // The sheet's header already names the workspace: here the\n // switcher is just the way to change it.\n <div className=\"px-2\">\n <Menu\n items={switcher}\n label={switcherLabel}\n align=\"start\"\n className=\"min-h-11 w-full px-2\"\n trigger={\n <>\n <Icon name=\"chevrons-up-down\" className=\"shrink-0 text-muted-foreground\" />\n <span className=\"min-w-0 flex-1 truncate text-left\">{switcherLabel}</span>\n </>\n }\n />\n </div>\n ) : null}\n {panel ? (\n <div className=\"px-2\">\n <Button variant=\"nav\" size=\"md\" onClick={() => setSheetPanel(true)}>\n <Icon name=\"list\" className=\"shrink-0\" />\n <span className=\"min-w-0 flex-1 truncate text-left\">{(panelIsNode ? null : panel.title) ?? \"Open list\"}</span>\n <Icon name=\"chevron-right\" className=\"shrink-0 text-muted-foreground\" />\n </Button>\n </div>\n ) : null}\n {nav({ sheet: true })}\n </div>\n )\n ) : null}\n </Sheet>\n ) : null}\n </>\n );\n}\n"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "registry/blocks/sidebar/sidebar-panel.jsx",
|
|
26
|
+
"type": "registry:component",
|
|
27
|
+
"target": "@components/makersclaw/blocks/sidebar/sidebar-panel.jsx",
|
|
28
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Button, Empty, InputGroup, Item, Notice, ScrollArea, Skeleton, commandScore, toast } from \"../../ui\";\n\n// The Sidebar's second column (README \"panel\"): a title, an optional\n// search field and a list — the mails in a folder, the threads in a\n// channel — or any node of the app's own. Drawn beside the nav from\n// 768px and as the sheet's second screen on a phone (sidebar.jsx decides\n// where). Also the helper that turns an action's `{ ok: false }` into a\n// toast, shared by item menus, the foot's \"+\" menu and primaryAction.\n\n/** Menu items whose onSelect may be async and return `{ ok, error? }`:\n * a refusal or a throw becomes an error toast, so a failed \"Archive\"\n * never looks done. `arg` is what each onSelect is called with. */\nexport function guard(entries, arg) {\n return (entries ?? []).filter(Boolean).map((entry) => {\n if (Array.isArray(entry.items)) return { ...entry, items: guard(entry.items, arg) };\n if (!entry.onSelect) return entry;\n return { ...entry, onSelect: () => run(entry.onSelect, arg, entry.label) };\n });\n}\n\n/** Runs an action and says so when it fails. */\nexport async function run(fn, arg, label) {\n try {\n const result = await fn?.(arg);\n if (result && result.ok === false) toast.error(result.error || `${label ?? \"That\"} didn't go through`);\n return result;\n } catch (err) {\n toast.error(`${label ?? \"That\"} didn't go through`, { description: err?.message });\n return { ok: false };\n }\n}\n\nconst textOf = (it) => [it.title, it.subtitle, it.description, ...(it.keywords ?? [])].filter((x) => typeof x === \"string\").join(\" \");\n\n/** The panel's body: header (title, actions, search), then the list or its\n * loading · error · empty state. `onChoose(item)` runs after the item's\n * own onSelect (the Sidebar closes the phone sheet there). */\nexport function PanelBody({ panel, onChoose, sheet = false }) {\n const [own, setOwn] = useState(\"\");\n const query = panel.query ?? own;\n const setQuery = (q) => {\n if (panel.query === undefined) setOwn(q);\n panel.onQueryChange?.(q);\n };\n const searchOn = Boolean(panel.search);\n const placeholder = (typeof panel.search === \"object\" && panel.search.placeholder) || \"Search\";\n const items = (panel.items ?? []).filter(Boolean);\n const shown = !searchOn || panel.filter === false || !query.trim() ? items : items.filter((it) => commandScore(textOf(it), query) > 0);\n\n function choose(item) {\n item.onSelect?.(item);\n panel.onSelect?.(item);\n onChoose?.(item);\n }\n\n let body;\n if (panel.error) {\n body = (\n <div className=\"p-3\">\n <Notice\n tone=\"danger\"\n title={typeof panel.error === \"string\" ? panel.error : \"This list didn't load\"}\n action={panel.onRetry ? <Button size=\"sm\" onClick={panel.onRetry}>Try again</Button> : undefined}\n />\n </div>\n );\n } else if (panel.loading) {\n body = (\n <ul aria-busy=\"true\" aria-label={`Loading ${panel.title ?? \"list\"}`} className=\"grid\">\n {[0, 1, 2, 3, 4].map((i) => (\n <li key={i} className=\"grid gap-2 border-b border-border px-4 py-3\">\n <Skeleton className=\"w-1/2\" />\n <Skeleton className=\"w-3/4\" />\n <Skeleton className=\"w-full\" />\n </li>\n ))}\n </ul>\n );\n } else if (!shown.length) {\n body = (\n <div className=\"p-4\">\n <Empty\n icon={query.trim() ? \"search\" : \"inbox\"}\n title={query.trim() ? `Nothing matches \"${query.trim()}\"` : panel.empty ?? \"Nothing here yet\"}\n />\n </div>\n );\n } else {\n body = (\n <ul aria-label={panel.title ?? \"List\"} className=\"grid\">\n {shown.map((it, i) => (\n <li key={it.key ?? it.href ?? `${it.title}-${i}`} className=\"border-b border-border\">\n <Item\n title={it.title}\n meta={it.meta}\n media={it.media}\n icon={it.icon}\n href={it.disabled ? undefined : it.href}\n onClick={it.disabled ? undefined : () => choose(it)}\n disabled={it.disabled}\n aria-current={it.active ? \"true\" : undefined}\n description={\n it.subtitle || it.description ? (\n <>\n {it.subtitle ? <span className=\"block truncate font-medium text-foreground\">{it.subtitle}</span> : null}\n {it.description ? <span className=\"line-clamp-2\">{it.description}</span> : null}\n </>\n ) : undefined\n }\n />\n </li>\n ))}\n </ul>\n );\n }\n\n return (\n <div className=\"flex min-h-0 flex-1 flex-col\">\n {panel.title || panel.actions || searchOn ? (\n <div className=\"grid shrink-0 gap-3 border-b border-border p-3\">\n {panel.title || panel.actions ? (\n <div className=\"flex min-h-9 items-center gap-2\">\n {panel.title ? <h2 className=\"min-w-0 flex-1 truncate text-base font-semibold\">{panel.title}</h2> : null}\n {panel.actions ? <div className=\"flex shrink-0 items-center gap-1\">{panel.actions}</div> : null}\n </div>\n ) : null}\n {searchOn ? (\n <InputGroup\n icon=\"search\"\n type=\"search\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={placeholder}\n aria-label={`Search ${panel.title ?? \"the list\"}`}\n />\n ) : null}\n </div>\n ) : null}\n {sheet ? <div className=\"min-h-0 flex-1\">{body}</div> : (\n <ScrollArea label={panel.title ?? \"List\"} className=\"min-h-0 flex-1\" fade>\n {body}\n </ScrollArea>\n )}\n </div>\n );\n}\n"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"path": "registry/blocks/sidebar/README.md",
|
|
32
|
+
"type": "registry:file",
|
|
33
|
+
"target": "@components/makersclaw/blocks/sidebar/README.md",
|
|
34
|
+
"content": "# Sidebar\n\nAn app's navigation down the left edge: the app's name (or a workspace\nswitcher), groups of items with icons, counts and nested items, a slot of\nyour own, and the signed-in person at the foot with their menu. On a wide\nwindow it collapses to icons — every label stays, as a tooltip — and\nremembers that. On a phone it is not a column at all: it is a Sheet that\n`SidebarTrigger` opens (from the leading edge; a bottom sheet under\n640px), and it closes when an item is chosen.\n\n```jsx\nimport { Sidebar, SidebarLayout, SidebarTrigger } from \"../components/blocks/sidebar\"; // a Makersclaw app (\"./blocks/sidebar\" from components/)\nimport { Sidebar, SidebarLayout, SidebarTrigger } from \"@/components/makersclaw/blocks/sidebar\"; // a shadcn project\n```\n\nIt is a client component. In a Makersclaw app it belongs in\n`app/shell.jsx`, around the surfaces — the shell's nav is what it replaces.\n\n## The data\n\n`groups` is a list of `{ label?, items }`. An item:\n\n- `label` — what it says (required).\n- `href` — where it goes; rendered as a link (base path applied). Or\n- `onSelect(item)` — what choosing it does. Both may be given.\n- `icon` — a kit icon name. Needed for the icon rail (a missing one shows\n `file`).\n- `active` — true on the current page: `aria-current=\"page\"` and the\n highlighted look. The parent of an active item is open to begin with.\n- `badge` — a count or a word, shown on the right; a dot in the rail.\n- `items` — nested items (same shape, one level): the item becomes a\n collapsible row. In the rail its icon widens the sidebar with it open.\n With `drilldown` it opens them instead (see Sidebar).\n- `hue` — 1..5: a colour dot in place of the icon (a favourite, a label).\n- `description` — a muted second line (shown on children in drill-in).\n- `actions` — kit `Menu` items: a \"…\" at the row's end, shown on hover\n or focus (always on a touch screen). Each `onSelect(item)` may be async\n and return `{ ok, error? }`; `{ ok: false }` or a throw shows an error\n toast. Not drawn in the icon rail.\n- `keywords` — extra words the ⌘K search matches, never shown.\n- `disabled` · `key` (default `href`, then `label`).\n\nA group is `{ label?, items, collapsible?, defaultOpen?, key? }`.\n`collapsible: true` makes its heading fold it; `defaultOpen` (default\ntrue) is where it starts. The icon rail shows every group unfolded.\n\n## Props\n\n### SidebarLayout\n\n- `sidebar` — the `<Sidebar>` element.\n- `children` — the page beside it. Put `<SidebarTrigger />` in its header.\n- `storageKey` — remember collapsed/expanded in this browser under this\n key. Optional; without it every visit starts at `defaultCollapsed`.\n- `defaultCollapsed` — default `false`.\n- `collapsed` + `onCollapsedChange(next)` — control it yourself instead.\n- `shortcut` — Cmd/Ctrl + this key toggles it; default `\"b\"`, `null` for none.\n- `variant` — `\"inset\"` draws the page as a card on the frame's ground.\n Default: the Sidebar's own `variant`, so you rarely pass it here.\n- `className` — the frame's height and edges (`min-h-dvh` in a shell).\n Never colour.\n\n### Sidebar\n\n- `groups` — see The data. `items` is the one-group shorthand.\n- `title` — the app's or workspace's name, at the top and on the sheet.\n- `subtitle` — a muted line under it (a plan, a team).\n- `logo` — a node beside the title; default a square `Avatar` of `title`.\n- `switcher` — kit `Menu` items (`[{ label, onSelect, icon? }]`): the title\n becomes a menu for changing workspace. `switcherLabel` names it\n (default \"Switch workspace\"); a reader hears \"<title>, <switcherLabel>\",\n so a voice user who says the title still hits it.\n- `user` — `{ name, email?, src? }`, shown at the foot with an `Avatar`.\n- `userMenu` — kit `Menu` items (`[{ label, onSelect, icon?, tone?: \"danger\",\n disabled? }]`, and groups and separators): the person becomes a menu\n button opening these upward (beside the rail when it is icons), flipping\n where there is no room. A choice also closes the phone sheet.\n- `children` — your own content under the groups (a target, a promo, a\n help link). Hidden in the icon rail.\n- `label` — the nav landmark's name, default \"Main\".\n- `className` — layout only. Never colour.\n\nShapes and extras (all optional; leave them out and it is the column\nabove):\n\n- `variant` — `\"sidebar\"` (default: a bordered column) · `\"inset\"` (no\n border; the page beside it becomes a rounded card) · `\"floating\"` (a\n rounded card held off the window's edge). A phone is a sheet either way.\n- `headerActions` — a node at the top right: a notifications `Menu`, a\n button. Hidden in the icon rail; first row of the phone sheet.\n- `switcherPlacement` — `\"header\"` (default) · `\"footer\"`: the switcher\n moves to the foot, over the person, and the top keeps only `logo`.\n- `search` — `true` or `{ placeholder?, hotkey?, label?, empty?,\n groupLabel? }`: a search row under the top that opens the kit\n `CommandDialog` over every item (children listed under their parent's\n name). `hotkey` defaults to `\"k\"` (⌘K / Ctrl+K), `null` for none. A\n palette choice runs the item's `onSelect`; an item with only `href`\n is followed as a page load (the app's base path applied). Give items\n `onSelect` with `router.push` for a client-side jump.\n- `primaryAction` — `{ label, icon?, onSelect?, href? }`: one button above\n the groups (\"New chat\"). `onSelect` may return `{ ok: false, error }`,\n shown as a toast.\n- `footerActions` — kit `Menu` items behind a \"+\" beside the person (New\n deal, Invite). `footerActionsLabel` names it, default \"Create\". Same\n `{ ok }` rule as `actions`.\n- `drilldown` — `\"inline\"` (default: a parent folds open) · `\"panel\"`\n (its children open in a second column with a close button; Esc closes\n it; the icon rail stays narrow beside it) · `\"replace\"` (its children\n replace the list, under a Back row). The parent holding the active\n item starts open. On a phone both are the replace form.\n- `panel` — a second column beside the nav from 768px, 320px wide: your\n own node, or `{ title?, actions?, search?, items, loading?, error?,\n onRetry?, empty?, onSelect?, query?, onQueryChange?, filter? }`.\n - `items`: `[{ key?, title, subtitle?, description?, meta?, icon?,\n media?, active?, href?, onSelect?, disabled? }]` — a mail is sender\n (title), date (meta), subject (subtitle), teaser (description).\n - `search`: `true` or `{ placeholder }`: a field that filters the items\n here; `filter: false` when you filter them yourself from\n `onQueryChange(query)`.\n - `loading` draws skeleton rows, `error` (text) a notice with Try\n again (`onRetry`), and no items the `empty` line.\n - On a phone it is the sheet's second screen: choosing a nav item (or\n the panel's own row at the top) shows it, with a Back row.\n\n### SidebarTrigger\n\nThe header button: opens the sheet on a phone, collapses to icons from\n768px. `className` for placement.\n\n### useSidebar()\n\n`{ collapsed, setCollapsed, toggle, mobileOpen, setMobileOpen }` inside a\n`SidebarLayout`, for a control of your own; `null` outside one.\n\n## Example\n\n```jsx\n// app/shell.jsx — the app's shell, the surfaces as the sidebar's items\n\"use client\";\nimport { usePathname } from \"next/navigation\";\nimport { Sidebar, SidebarLayout, SidebarTrigger } from \"../components/blocks/sidebar\";\n\nconst ICONS = { inbox: \"inbox\", deals: \"dollar\", contacts: \"users\", reports: \"chart\" };\n\nexport default function Shell({ name, surfaces, children }) {\n const pathname = usePathname() ?? \"\";\n const activeKey = decodeURIComponent(/^\\/s\\/([^/?#]+)/.exec(pathname)?.[1] ?? \"\");\n const items = surfaces.map((s) => ({\n key: s.key,\n label: s.label,\n href: `/s/${encodeURIComponent(s.key)}`,\n icon: ICONS[s.key] ?? \"file\",\n active: s.key === activeKey,\n badge: s.built ? undefined : \"planned\",\n }));\n return (\n <SidebarLayout\n storageKey={`${name}.sidebar`}\n className=\"min-h-dvh\"\n sidebar={<Sidebar title={name} items={items} label={`${name} surfaces`} />}\n >\n <header className=\"flex min-h-14 items-center gap-2 border-b border-border px-2 md:hidden\">\n <SidebarTrigger />\n <span className=\"font-semibold\">{name}</span>\n </header>\n <main className=\"min-w-0 flex-1 p-4 sm:p-6\">{children}</main>\n </SidebarLayout>\n );\n}\n```\n\n## What it will not do\n\n- Know the route: say which item is `active` (from `usePathname()`).\n- Nest deeper than one level: a second level is a page of its own.\n- Cover the page on a wide window or dock on the right: floating is a\n card beside the page, not over it; it is on the left from 768px and a\n sheet below that.\n- Hold the panel's data: the list, its loading and its errors are yours.\n- Take a colour: it wears the workspace theme.\n\nA sidebar it cannot be is your own component in `components/`, built from\nthe kit — not an edit to this block. Its files are fixed: `npm run check`\nrefuses a change, and `makersclaw add sidebar` restores them.\n\n## Credits\n\nThe inset, floating, search, folding-group, panel, drill-in and item-menu\nshapes follow blocks.so (https://blocks.so, MIT) — a rewrite on the\nMakersclaw kit, none of its code.\n"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"categories": [
|
|
38
|
+
"sidebar",
|
|
39
|
+
"nav",
|
|
40
|
+
"navigation",
|
|
41
|
+
"shell",
|
|
42
|
+
"layout",
|
|
43
|
+
"menu",
|
|
44
|
+
"drawer",
|
|
45
|
+
"collapse",
|
|
46
|
+
"workspace",
|
|
47
|
+
"switcher",
|
|
48
|
+
"app",
|
|
49
|
+
"rail",
|
|
50
|
+
"inset",
|
|
51
|
+
"floating",
|
|
52
|
+
"search",
|
|
53
|
+
"command",
|
|
54
|
+
"palette",
|
|
55
|
+
"collapsible",
|
|
56
|
+
"panel",
|
|
57
|
+
"drilldown",
|
|
58
|
+
"mail",
|
|
59
|
+
"chat",
|
|
60
|
+
"history"
|
|
61
|
+
],
|
|
62
|
+
"docs": "Installed to components/makersclaw/blocks/sidebar. Import: import { Sidebar, SidebarLayout, SidebarTrigger, useSidebar } from \"@/components/makersclaw/blocks/sidebar\". Props and an example: components/makersclaw/blocks/sidebar/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
63
|
+
"meta": {
|
|
64
|
+
"makersclaw": {
|
|
65
|
+
"version": "0.3.0",
|
|
66
|
+
"useWhen": "An app with more than a handful of places to go (inbox, deals, contacts, reports, settings, projects) that wants a real app shell: compose it in app/shell.jsx around the surfaces. Not for in-page tabs (Tabs) or a short strip of links."
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "signature-pad",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "SignaturePad",
|
|
6
|
+
"description": "A signature field (signature_pad) in the workspace theme: a full-name field, then draw on a paper box (smooth pen strokes, undo the last stroke, clear with Undo) or type the name set as a signature. Sign returns a record: a cropped transparent PNG in dark ink, the typed name, the mode and the time; the signed view shows it with Sign again. Works as a form field too, through a handle (toPNG, getSignature).",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/core.json"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"signature_pad@5.1.4"
|
|
12
|
+
],
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "registry/blocks/signature-pad/index.js",
|
|
16
|
+
"type": "registry:component",
|
|
17
|
+
"target": "@components/makersclaw/blocks/signature-pad/index.js",
|
|
18
|
+
"content": "// The SignaturePad block — `makersclaw add signature-pad`. Fixed: compose\n// it through its props (README.md beside this file). The blocks built on\n// this one (proposal-accept…) import it, never signature_pad directly.\n// The helpers are plain functions and work on the server.\nexport { SignaturePad, useSignaturePad } from \"./signature-pad\";\nexport { signatureRecord, formatSignedAt } from \"./signature\";\n"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"path": "registry/blocks/signature-pad/signature-pad.jsx",
|
|
22
|
+
"type": "registry:component",
|
|
23
|
+
"target": "@components/makersclaw/blocks/signature-pad/signature-pad.jsx",
|
|
24
|
+
"content": "\"use client\";\n\n// SignaturePad — sign by drawing or by typing your name, in the workspace\n// theme (signature_pad underneath for the drawing). A full-name field, a\n// paper box to draw on (undo the last stroke, clear with Undo), or the\n// name set in the theme's display face; then \"Sign\". What comes out is a\n// PNG (transparent, dark ink, cropped to the signature) and the typed\n// name, with when and how it was made.\n//\n// The pad is paper: whichever of the theme's background and foreground is\n// lighter, inked with the other (colors.js). On a dark theme that is a\n// light sheet — the way a signature looks on a page — and the PNG is what\n// you see.\n//\n// Client only in effect, not in import: the pad is created in an effect.\n// The server (and the first paint) shows the same frame, faded in once\n// the ink is known.\nimport { useCallback, useEffect, useId, useImperativeHandle, useRef, useState } from \"react\";\nimport { Button, Field, Icon, IconButton, Input, Notice, ToggleGroup, Tooltip, cx, toast } from \"../../ui\";\nimport { SignaturePadEngine } from \"./pad\";\nimport { paperAndInk, readTokens, watchTheme } from \"./colors\";\nimport { formatSignedAt, missing, pointCount, signatureRecord, strokeBounds, withInk, withoutLast } from \"./signature\";\n\n/** A ref for the pad's handle: `const pad = useSignaturePad()`, then\n * `<SignaturePad padRef={pad} />` and `await pad.current?.toPNG()`. */\nexport function useSignaturePad() {\n return useRef(null);\n}\n\nconst MODES = [\n { value: \"draw\", label: \"Draw\", icon: \"pencil\" },\n { value: \"type\", label: \"Type\", icon: \"type\" },\n];\n\n// Class names written out whole (Tailwind reads them literally), per paper.\nconst PAPER = {\n background: { box: \"bg-background\", hint: \"text-muted-foreground\", line: \"bg-border\", ink: \"text-foreground\", mark: \"text-muted-foreground\" },\n foreground: { box: \"bg-foreground\", hint: \"text-background/60\", line: \"bg-background/25\", ink: \"text-background\", mark: \"text-background/50\" },\n};\n\nexport function SignaturePad({\n signer = \"\",\n label = \"Signature\",\n nameLabel = \"Full name\",\n modes = [\"draw\", \"type\"],\n defaultMode,\n requireName = true,\n value,\n onChange,\n onSubmit,\n submitLabel = \"Sign\",\n note,\n disabled = false,\n height = 176,\n bare = false,\n padRef,\n className,\n}) {\n const id = useId();\n const rootRef = useRef(null);\n const boxRef = useRef(null);\n const canvasRef = useRef(null);\n const typedRef = useRef(null);\n const engine = useRef(null);\n const strokes = useRef([]); // survives the canvas going away (signed view, mode switch)\n\n const allowed = MODES.filter((m) => modes.includes(m.value));\n const [mode, setMode] = useState(defaultMode && modes.includes(defaultMode) ? defaultMode : allowed[0]?.value ?? \"draw\");\n const [name, setName] = useState(signer);\n const nameRef = useRef(name);\n nameRef.current = name;\n const [drawn, setDrawn] = useState(0);\n const [look, setLook] = useState({ paper: \"background\", ink: \"foreground\", inkColour: null, known: false });\n const [errors, setErrors] = useState(null);\n const [signed, setSigned] = useState(value?.png ? value : null);\n const [saving, setSaving] = useState(false);\n const [failed, setFailed] = useState(null);\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n\n // A new `value` from the parent (a saved signature) shows as signed.\n useEffect(() => {\n if (value?.png) setSigned(value);\n }, [value]);\n\n const cbs = useRef({});\n cbs.current = { onChange };\n const tell = useCallback((next) => cbs.current.onChange?.(next), []);\n\n // ---- the ink, from the theme ----------------------------------------------\n\n useEffect(() => {\n return watchTheme(rootRef.current, () => {\n const next = paperAndInk(readTokens(rootRef.current, [\"background\", \"foreground\"]));\n setLook((prev) => (prev.known && prev.inkColour === next.inkColour && prev.paper === next.paper ? prev : { ...next, known: true }));\n });\n }, []);\n\n // ---- the drawing engine ---------------------------------------------------\n\n const drawing = mode === \"draw\" && !signed;\n\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!drawing || !canvas || !look.inkColour) return;\n const pad = new SignaturePadEngine(canvas, { penColor: look.inkColour, minWidth: 0.8, maxWidth: 2.6, velocityFilterWeight: 0.6 });\n engine.current = pad;\n\n // The canvas holds device pixels; strokes are kept in CSS pixels and\n // redrawn at every size change (a phone rotating, a panel resizing).\n const fit = () => {\n const box = boxRef.current?.getBoundingClientRect();\n if (!box || !box.width) return;\n const ratio = Math.max(window.devicePixelRatio || 1, 1);\n canvas.width = Math.round(box.width * ratio);\n canvas.height = Math.round(box.height * ratio);\n canvas.getContext(\"2d\")?.scale(ratio, ratio);\n pad.fromData(withInk(strokes.current, look.inkColour));\n };\n fit();\n const ro = typeof ResizeObserver !== \"undefined\" ? new ResizeObserver(fit) : null;\n ro?.observe(boxRef.current);\n\n const end = () => {\n strokes.current = pad.toData();\n const n = pointCount(strokes.current);\n setDrawn(n);\n setErrors((e) => (e?.pad ? { ...e, pad: undefined } : e));\n tell({ mode: \"draw\", name: nameRef.current, empty: n === 0 });\n };\n pad.addEventListener(\"endStroke\", end);\n if (disabled) pad.off();\n return () => {\n pad.removeEventListener(\"endStroke\", end);\n pad.off();\n ro?.disconnect();\n engine.current = null;\n };\n }, [drawing, look.inkColour, disabled, tell]);\n\n const undo = useCallback(() => {\n strokes.current = withoutLast(strokes.current);\n engine.current?.fromData(withInk(strokes.current, look.inkColour));\n const n = pointCount(strokes.current);\n setDrawn(n);\n tell({ mode: \"draw\", name, empty: n === 0 });\n }, [look.inkColour, name, tell]);\n\n const clear = useCallback(\n (quiet) => {\n const before = strokes.current;\n if (!before.length) return;\n strokes.current = [];\n engine.current?.clear();\n setDrawn(0);\n tell({ mode: \"draw\", name, empty: true });\n if (quiet === true) return;\n toast(\"Signature cleared\", {\n action: {\n label: \"Undo\",\n onClick: () => {\n strokes.current = before;\n engine.current?.fromData(withInk(before, look.inkColour));\n setDrawn(pointCount(before));\n },\n },\n });\n },\n [look.inkColour, name, tell],\n );\n\n // ---- the picture ----------------------------------------------------------\n\n /** The signature as a PNG data URL (transparent, inked, cropped), or null. */\n const toPNG = useCallback(async () => {\n if (mode === \"type\") {\n const text = name.trim();\n const el = typedRef.current;\n if (!text || typeof document === \"undefined\") return null;\n try {\n await document.fonts?.ready;\n } catch {}\n const family = el ? getComputedStyle(el).fontFamily : \"serif\";\n const size = 64;\n const scale = 2;\n const c = document.createElement(\"canvas\");\n const ctx = c.getContext(\"2d\");\n const font = `italic 400 ${size}px ${family}`;\n ctx.font = font;\n const m = ctx.measureText(text);\n const w = Math.ceil(m.width + size * 0.6);\n const h = Math.ceil(size * 1.5);\n c.width = w * scale;\n c.height = h * scale;\n ctx.scale(scale, scale);\n ctx.font = font;\n ctx.fillStyle = look.inkColour ?? getComputedStyle(el ?? document.body).color;\n ctx.textBaseline = \"alphabetic\";\n ctx.fillText(text, size * 0.3, size * 1.05);\n return c.toDataURL(\"image/png\");\n }\n const canvas = canvasRef.current;\n const box = boxRef.current?.getBoundingClientRect();\n const b = strokeBounds(strokes.current, 6, box ? { width: box.width, height: box.height } : undefined);\n if (!canvas || !b) return null;\n const ratio = canvas.width / (box?.width || canvas.width);\n const out = document.createElement(\"canvas\");\n out.width = Math.round(b.width * ratio);\n out.height = Math.round(b.height * ratio);\n out.getContext(\"2d\").drawImage(canvas, b.x * ratio, b.y * ratio, b.width * ratio, b.height * ratio, 0, 0, out.width, out.height);\n return out.toDataURL(\"image/png\");\n }, [mode, name, look.inkColour]);\n\n // ---- sign -----------------------------------------------------------------\n\n const submit = useCallback(async () => {\n if (!onSubmit || saving || disabled) return;\n const gaps = missing({ mode, drawn, name, requireName });\n setErrors(gaps);\n if (gaps) {\n if (gaps.name) document.getElementById(`${id}-name`)?.focus();\n return;\n }\n const png = await toPNG();\n if (!png) return setErrors({ pad: \"Draw your signature in the box.\" });\n const record = signatureRecord({ png, name, mode });\n // Optimistic: it shows as signed at once; a failed save puts the pad\n // back as it was, with the reason.\n setFailed(null);\n setSigned(record);\n setSaving(true);\n let res;\n try {\n res = await onSubmit(record);\n } catch (e) {\n res = { ok: false, error: e?.message };\n }\n setSaving(false);\n if (!res?.ok) {\n setSigned(null);\n setFailed(res?.error || \"The signature wasn't saved. Try again.\");\n }\n }, [onSubmit, saving, disabled, mode, drawn, name, requireName, toPNG, id]);\n\n const signAgain = useCallback(() => {\n setSigned(null);\n setFailed(null);\n strokes.current = [];\n setDrawn(0);\n }, []);\n\n useImperativeHandle(\n padRef,\n () => ({\n toPNG,\n clear: () => clear(true),\n undo,\n isEmpty: () => (mode === \"type\" ? !name.trim() : pointCount(strokes.current) === 0),\n getName: () => name.trim(),\n getMode: () => mode,\n /** { png, name, mode, signedAt } now, or null when nothing is there. */\n getSignature: async () => {\n const png = await toPNG();\n return png ? signatureRecord({ png, name, mode }) : null;\n },\n submit,\n }),\n [toPNG, clear, undo, mode, name, submit],\n );\n\n // ---- render ---------------------------------------------------------------\n\n const paper = PAPER[look.paper] ?? PAPER.background;\n const frame = bare ? \"\" : \"rounded-lg border border-border bg-card p-4 sm:p-5\";\n const nameId = `${id}-name`;\n const hintId = `${id}-hint`;\n const typed = name.trim();\n\n if (signed) {\n return (\n <div ref={rootRef} className={cx(frame, \"text-card-foreground\", className)}>\n <div className=\"flex items-center justify-between gap-3\">\n <p className=\"text-sm font-medium\">{label}</p>\n {saving ? (\n <span className=\"text-xs text-muted-foreground\">Saving…</span>\n ) : (\n <span className=\"inline-flex items-center gap-1.5 text-xs font-medium text-success\">\n <Icon name=\"circle-check\" size={14} /> Signed\n </span>\n )}\n </div>\n <div className={cx(\"mt-3 grid place-items-center rounded-md border border-border px-4 py-5 transition-opacity duration-150 motion-reduce:transition-none\", paper.box, look.known ? \"opacity-100\" : \"opacity-0\")} style={{ minHeight: Math.round(height * 0.7) }}>\n {/* raw: the signature on its paper — a transparent PNG over the paper token; the kit Image paints a muted fill and a skeleton under it */}\n <img src={signed.png} alt={`Signature of ${signed.name || \"the signer\"}`} className=\"max-h-28 max-w-full object-contain\" />\n </div>\n <div className=\"mt-3 flex flex-wrap items-center justify-between gap-2\">\n <p className=\"min-w-0 text-sm text-muted-foreground\">\n <span className=\"font-medium text-foreground\">{signed.name || \"Signed\"}</span>\n {mounted && signed.signedAt ? <span> · {formatSignedAt(signed.signedAt)}</span> : null}\n {signed.mode === \"type\" ? <span> · typed</span> : null}\n </p>\n {!disabled && !saving ? (\n <Button variant=\"quiet\" size=\"sm\" icon=\"refresh\" onClick={signAgain}>\n Sign again\n </Button>\n ) : null}\n </div>\n </div>\n );\n }\n\n return (\n <div ref={rootRef} className={cx(frame, \"text-card-foreground\", className)}>\n <div className=\"flex items-center justify-between gap-3\">\n <p className=\"text-sm font-medium\" id={`${id}-label`}>\n {label}\n </p>\n {allowed.length > 1 ? (\n <ToggleGroup\n size=\"sm\"\n aria-label=\"How to sign\"\n value={mode}\n onChange={(m) => {\n setMode(m);\n setErrors(null);\n tell({ mode: m, name, empty: m === \"type\" ? !typed : pointCount(strokes.current) === 0 });\n }}\n disabled={disabled}\n items={allowed}\n />\n ) : null}\n </div>\n\n <Field id={nameId} label={nameLabel} error={errors?.name} className=\"mt-3\">\n <Input\n id={nameId}\n value={name}\n disabled={disabled}\n autoComplete=\"name\"\n placeholder=\"Your full name\"\n invalid={Boolean(errors?.name)}\n aria-describedby={errors?.name ? `${nameId}-error` : undefined}\n onChange={(e) => {\n const v = e.target.value;\n setName(v);\n if (errors?.name && v.trim()) setErrors((x) => ({ ...x, name: undefined }));\n tell({ mode, name: v, empty: mode === \"type\" ? !v.trim() : pointCount(strokes.current) === 0 });\n }}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && mode === \"type\" && onSubmit) {\n e.preventDefault();\n submit();\n }\n }}\n />\n </Field>\n\n {/* The paper. */}\n <div\n ref={boxRef}\n className={cx(\n \"relative mt-3 overflow-hidden rounded-md border transition-[opacity,border-color] duration-150 motion-reduce:transition-none\",\n paper.box,\n errors?.pad ? \"border-destructive\" : \"border-border\",\n look.known ? \"opacity-100\" : \"opacity-0\",\n disabled && \"opacity-60\",\n )}\n style={{ height }}\n >\n {/* The line to sign on, and its cross. */}\n <span aria-hidden=\"true\" className={cx(\"pointer-events-none absolute right-6 left-6 h-px\", paper.line)} style={{ bottom: Math.round(height * 0.26) }} />\n <span aria-hidden=\"true\" className={cx(\"pointer-events-none absolute left-6 text-sm select-none\", paper.mark)} style={{ bottom: Math.round(height * 0.26) + 4 }}>\n ×\n </span>\n\n {mode === \"draw\" ? (\n <>\n {!drawn ? (\n <p aria-hidden=\"true\" className={cx(\"pointer-events-none absolute inset-x-0 top-1/2 -translate-y-1/2 px-6 text-center text-sm select-none\", paper.hint)}>\n Sign here with your mouse, pen or finger\n </p>\n ) : null}\n {/* raw: the drawing surface — signature_pad draws on a canvas; no kit part is one */}\n <canvas\n ref={canvasRef}\n role=\"img\"\n aria-labelledby={`${id}-label`}\n aria-describedby={hintId}\n className={cx(\"absolute inset-0 block size-full touch-none\", disabled ? \"cursor-not-allowed\" : \"cursor-crosshair\")}\n />\n {drawn && !disabled ? (\n <div className=\"absolute top-2 right-2 flex gap-1\">\n <Tooltip label=\"Undo last stroke\">\n <IconButton icon=\"undo\" label=\"Undo last stroke\" size=\"sm\" variant=\"secondary\" onClick={undo} />\n </Tooltip>\n <Tooltip label=\"Clear\">\n <IconButton icon=\"eraser\" label=\"Clear the signature\" size=\"sm\" variant=\"secondary\" onClick={() => clear()} />\n </Tooltip>\n </div>\n ) : null}\n </>\n ) : (\n <div className=\"absolute inset-x-6 flex items-end justify-center\" style={{ bottom: Math.round(height * 0.26) + 6, top: 12 }}>\n <p ref={typedRef} className={cx(\"truncate pb-1 font-display text-4xl leading-tight italic sm:text-5xl\", typed ? paper.ink : paper.hint)} aria-live=\"polite\">\n {typed || <span className=\"text-base not-italic\">Type your name above</span>}\n </p>\n </div>\n )}\n </div>\n\n <p id={hintId} className={cx(\"mt-1.5 text-xs\", errors?.pad ? \"text-destructive\" : \"text-muted-foreground\")} role={errors?.pad ? \"alert\" : undefined}>\n {errors?.pad ?? (mode === \"draw\" ? (allowed.some((m) => m.value === \"type\") ? \"Prefer the keyboard? Switch to Type.\" : \"Draw inside the box.\") : \"Your name, set as a signature.\")}\n </p>\n\n {failed ? (\n <Notice tone=\"danger\" title=\"Not signed yet\" className=\"mt-3\" onDismiss={() => setFailed(null)}>\n {failed}\n </Notice>\n ) : null}\n\n {note || onSubmit ? (\n <div className=\"mt-4 flex flex-col-reverse gap-3 sm:flex-row sm:items-center sm:justify-between\">\n {note ? <p className=\"text-xs text-muted-foreground sm:max-w-md\">{note}</p> : <span />}\n {onSubmit ? (\n <Button variant=\"primary\" icon=\"pencil\" onClick={submit} disabled={disabled} className=\"w-full sm:w-auto\">\n {submitLabel}\n </Button>\n ) : null}\n </div>\n ) : null}\n </div>\n );\n}\n\n"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"path": "registry/blocks/signature-pad/pad.js",
|
|
28
|
+
"type": "registry:component",
|
|
29
|
+
"target": "@components/makersclaw/blocks/signature-pad/pad.js",
|
|
30
|
+
"content": "// The one door to signature_pad (MIT). Every file in this block reaches\n// the library through here, and the blocks built on this one\n// (proposal-accept…) reach it through SignaturePad — never the package.\n//\n// No shim is needed: the package loads on the server without touching\n// `window` (checked in node); the pad is only created in an effect.\nexport { default as SignaturePadEngine } from \"signature_pad\";\n"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"path": "registry/blocks/signature-pad/colors.js",
|
|
34
|
+
"type": "registry:component",
|
|
35
|
+
"target": "@components/makersclaw/blocks/signature-pad/colors.js",
|
|
36
|
+
"content": "// The pad's colours are the workspace's tokens.\n//\n// signature_pad draws on a <canvas>, and a canvas needs a concrete colour —\n// it cannot read var(--color-foreground). So the ink is read from the CSS\n// variables in effect where the pad sits, at run time, and read again when\n// the theme or the scheme changes (the pad watches for that). No colour\n// value is written in this file: every one comes from the page. Same\n// method as sketch-canvas's palette.js and audio-player's colors.js.\n//\n// The pad is PAPER: whichever of the theme's background and foreground is\n// lighter, with the other one as ink. On a light theme that is the page\n// itself; on a dark theme it is a light sheet with dark ink. Either way\n// what you see is what the PNG holds — dark ink on a transparent ground,\n// which reads on a document, a PDF, an email.\n//\n// Everything but readTokens() and watchTheme() is a plain function, so it\n// runs in node.\n\nconst byte = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, \"0\");\n\n/** [r, g, b, a(0-255)] → \"#rrggbb\", or \"#rrggbbaa\" when not opaque. */\nexport function toHex([r, g, b, a = 255]) {\n return \"#\" + byte(r) + byte(g) + byte(b) + (a >= 255 ? \"\" : byte(a));\n}\n\n/** Relative luminance (WCAG) of [r, g, b], 0 (black) to 1 (white). */\nexport function luminance([r, g, b]) {\n const lin = (c) => {\n const s = c / 255;\n return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);\n };\n return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);\n}\n\n/** Which token is the paper and which the ink, from the two read off the\n * page. Unknown (the server, a missing slot): the page is the paper. */\nexport function paperAndInk(slots) {\n const bg = slots?.background;\n const fg = slots?.foreground;\n if (!bg || !fg) return { paper: \"background\", ink: \"foreground\", inkColour: fg ? toHex(fg) : null };\n return luminance(fg) > luminance(bg) ? { paper: \"foreground\", ink: \"background\", inkColour: toHex(bg) } : { paper: \"background\", ink: \"foreground\", inkColour: toHex(fg) };\n}\n\n// ---- the browser half -------------------------------------------------------\n\n/** Read `slots` (token names: \"primary\", \"muted-foreground\"…) from the CSS\n * variables in effect at `root`, as [r, g, b, a] bytes. A probe takes\n * `color: var(--color-<slot>)`; the browser resolves var(), color-mix()\n * and light-dark() into its computed colour, and a 1×1 canvas turns that\n * into bytes whatever syntax it came back in. */\nexport function readTokens(root, slots) {\n const out = {};\n if (typeof document === \"undefined\" || !root) return out;\n const probe = document.createElement(\"span\");\n probe.setAttribute(\"aria-hidden\", \"true\");\n probe.style.position = \"absolute\";\n probe.style.visibility = \"hidden\";\n probe.style.pointerEvents = \"none\";\n root.appendChild(probe);\n const canvas = document.createElement(\"canvas\");\n canvas.width = 1;\n canvas.height = 1;\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n try {\n for (const slot of slots) {\n probe.style.color = \"\";\n probe.style.color = `var(--color-${slot})`;\n const computed = getComputedStyle(probe).color;\n if (!computed || !ctx) continue;\n ctx.clearRect(0, 0, 1, 1);\n ctx.fillStyle = \"transparent\";\n ctx.fillStyle = computed;\n ctx.fillRect(0, 0, 1, 1);\n const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;\n if (a === 0) continue;\n out[slot] = [r, g, b, a];\n }\n } finally {\n probe.remove();\n }\n return out;\n}\n\n/** Call `fn` (soon, once) whenever something that sets the tokens may have\n * changed: a data-theme / class / style on <html> or the nearest themed\n * ancestor, a stylesheet added to <head>, the OS scheme. Returns the\n * cleanup. */\nexport function watchTheme(root, fn) {\n if (typeof document === \"undefined\") return () => {};\n let t = 0;\n const soon = () => {\n clearTimeout(t);\n t = setTimeout(fn, 30);\n };\n const obs = new MutationObserver((records) => {\n for (const r of records) for (const n of r.addedNodes ?? []) if (n.tagName === \"LINK\") n.addEventListener(\"load\", soon, { once: true });\n soon();\n });\n const attrs = { attributes: true, attributeFilter: [\"data-theme\", \"class\", \"style\"] };\n obs.observe(document.documentElement, attrs);\n obs.observe(document.head, { childList: true });\n const scoped = root?.closest?.(\"[data-theme]\");\n if (scoped && scoped !== document.documentElement) {\n obs.observe(scoped, attrs);\n obs.observe(scoped, { childList: true });\n }\n const mq = typeof matchMedia !== \"undefined\" ? matchMedia(\"(prefers-color-scheme: dark)\") : null;\n mq?.addEventListener(\"change\", soon);\n soon();\n return () => {\n clearTimeout(t);\n obs.disconnect();\n mq?.removeEventListener(\"change\", soon);\n };\n}\n"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"path": "registry/blocks/signature-pad/signature.js",
|
|
40
|
+
"type": "registry:component",
|
|
41
|
+
"target": "@components/makersclaw/blocks/signature-pad/signature.js",
|
|
42
|
+
"content": "// Plain helpers for a signature: its strokes, what is missing before it\n// can be signed, the record it becomes. No DOM, so they run anywhere.\n//\n// Strokes are signature_pad's point groups, in CSS pixels of the pad:\n// [{ points: [{ x, y, pressure, time }], penColor, minWidth, maxWidth, … }]\n\n/** The box around every point, grown by `pad` px on each side (a stroke\n * has width); clipped to 0,0 and, when given, to the pad's size. Null\n * when there is nothing drawn. */\nexport function strokeBounds(groups, pad = 6, size) {\n let x0 = Infinity;\n let y0 = Infinity;\n let x1 = -Infinity;\n let y1 = -Infinity;\n for (const g of groups ?? []) {\n for (const p of g?.points ?? []) {\n if (p.x < x0) x0 = p.x;\n if (p.y < y0) y0 = p.y;\n if (p.x > x1) x1 = p.x;\n if (p.y > y1) y1 = p.y;\n }\n }\n if (!Number.isFinite(x0)) return null;\n const maxX = size?.width ?? Infinity;\n const maxY = size?.height ?? Infinity;\n const x = Math.max(0, Math.floor(x0 - pad));\n const y = Math.max(0, Math.floor(y0 - pad));\n const right = Math.min(maxX, Math.ceil(x1 + pad));\n const bottom = Math.min(maxY, Math.ceil(y1 + pad));\n return { x, y, width: Math.max(1, right - x), height: Math.max(1, bottom - y) };\n}\n\n/** Every stroke inked in `colour` (a theme change repaints the pad). */\nexport const withInk = (groups, colour) => (colour ? (groups ?? []).map((g) => ({ ...g, penColor: colour })) : groups ?? []);\n\n/** The strokes without the last one. */\nexport const withoutLast = (groups) => (groups ?? []).slice(0, -1);\n\n/** How many points were drawn: a tap is 1, a real signature dozens. */\nexport const pointCount = (groups) => (groups ?? []).reduce((n, g) => n + (g?.points?.length ?? 0), 0);\n\n/** What stops this from being signed, as the words to show, or null.\n * mode \"draw\" | \"type\"\n * drawn how many points are on the pad\n * name the typed full name\n * requireName a name is needed in draw mode too */\nexport function missing({ mode, drawn, name, requireName = true }) {\n const hasName = Boolean(String(name ?? \"\").trim());\n if (mode === \"type\") return hasName ? null : { name: \"Type your full name to sign.\" };\n const out = {};\n if (requireName && !hasName) out.name = \"Add your full name.\";\n if (!drawn) out.pad = \"Draw your signature in the box.\";\n else if (drawn < 4) out.pad = \"That looks like a dot — draw your signature in the box.\";\n return Object.keys(out).length ? out : null;\n}\n\n/** The record a signature becomes (what onSubmit receives, what `value`\n * shows): the picture, the name, how it was made, when. */\nexport function signatureRecord({ png, name, mode, signedAt }) {\n return { png, name: String(name ?? \"\").trim(), mode, signedAt: signedAt ?? new Date().toISOString() };\n}\n\n/** \"Sep 23, 2026, 2:04 PM\" in the reader's own locale. */\nexport function formatSignedAt(iso, locale) {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n return d.toLocaleString(locale, { month: \"short\", day: \"numeric\", year: \"numeric\", hour: \"numeric\", minute: \"2-digit\" });\n}\n"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"path": "registry/blocks/signature-pad/README.md",
|
|
46
|
+
"type": "registry:file",
|
|
47
|
+
"target": "@components/makersclaw/blocks/signature-pad/README.md",
|
|
48
|
+
"content": "# SignaturePad\n\nA signature field in the workspace theme. The signer adds their full\nname, then either **draws** on a paper box (smooth pen strokes that\nfollow speed and pressure; undo the last stroke; clear, with Undo in a\ntoast) or **types** — their name is set as a signature in the theme's\ndisplay face. \"Sign\" saves it and the field turns into the signed view:\nthe signature, the name and the time, with \"Sign again\". Nothing drawn,\nno name, a dot: it says what is missing, next to where it is missing.\n\nWhat comes out is a record: a PNG (transparent, cropped to the\nsignature, in dark ink so it reads on a document, a PDF or an email), the\ntyped name, how it was made and when.\n\n**The pad is paper.** It is whichever of the theme's background and\nforeground is lighter, inked with the other: on a light theme, the page\nitself; on a dark theme, a light sheet with dark ink. So what the signer\nsees is exactly what the PNG holds.\n\nIt is [signature_pad](https://github.com/szimek/signature_pad) (MIT)\nunderneath, for the drawing. The field, the modes, the states and the\nrecord are ours, in the kit. The blocks built on this one —\n`proposal-accept` — import it, never `signature_pad`.\n\n```jsx\nimport { SignaturePad, useSignaturePad } from \"../components/blocks/signature-pad\"; // a Makersclaw app (\"./blocks/signature-pad\" from components/)\nimport { SignaturePad, useSignaturePad } from \"@/components/makersclaw/blocks/signature-pad\"; // a shadcn project\n```\n\n`SignaturePad` is a client component: render it from a `\"use client\"`\ncomponent. `signatureRecord` and `formatSignedAt` are plain functions.\n\n`makersclaw add signature-pad` adds `signature_pad` (pinned, exact) to\npackage.json: run `npm install` after. It loads with the page (no\n`import()`); the pad starts in the browser, and fades in once it has\nread the theme's ink.\n\n**It records an acceptance — it is not a certified e-signature.** There\nis no identity check and no audit trail beyond what you store. Say so\nwhere people sign (`note`), and store the record with who was signed in,\nwhen, and what they were shown.\n\n## The data\n\nThe record `onSubmit` receives, `getSignature()` returns and `value`\nshows:\n\n```js\n{\n png: \"data:image/png;base64,…\", // transparent, dark ink, cropped; a few KB\n name: \"Maya Chen\", // the typed full name, trimmed\n mode: \"draw\", // \"draw\" | \"type\"\n signedAt: \"2026-09-23T14:04:11.512Z\",\n}\n```\n\nStore it as it is — one `jsonb` column holds the whole record (the PNG\nis a few KB) — beside the signed-in user and the thing that was signed.\n\n## Props\n\n- `onSubmit(record)` — shows the Sign button (`submitLabel`). It shows as\n signed at once while you save; return `{ ok: true }`, or `{ ok: false,\n error }` (or throw) and the pad comes back as it was, strokes and name\n kept, with a notice that says why. Without it there is no button: read\n the signature through the handle (a field inside your own form).\n- `signer` — the name to start with (\"Maya Chen\"), default empty.\n- `value` — a saved record: the field opens signed, showing it. \"Sign\n again\" (unless `disabled`) goes back to an empty pad.\n- `onChange({ mode, name, empty })` — after each stroke, undo, clear,\n keystroke in the name and mode switch. No PNG (that is `toPNG()`).\n- `label` — the field's label, default \"Signature\" (\"Received by\",\n \"Customer signature\").\n- `nameLabel` — the name field's label, default \"Full name\".\n- `modes` — `[\"draw\", \"type\"]` (the default), or one of them.\n- `defaultMode` — the mode it opens in, default the first of `modes`.\n- `requireName` — a name is needed to sign by drawing too. Default\n `true`; typing always needs one.\n- `submitLabel` — default \"Sign\" (\"Accept and sign\", \"Approve\").\n- `note` — a line beside the button: what signing means.\n- `disabled` — nothing can be drawn, typed or signed. Default `false`.\n- `height` — the paper's height in px, default `176`.\n- `bare` — no card around it (no border, background or padding), for a\n field inside your own card or form. Default `false`.\n- `padRef` — a ref from `useSignaturePad()` for the handle below.\n- `className` — on the outer element.\n\n### The handle\n\n```jsx\nconst pad = useSignaturePad();\n<SignaturePad padRef={pad} />;\nconst record = await pad.current?.getSignature(); // null when nothing is there\n```\n\n`getSignature()` → the record or `null`; `toPNG()` → the PNG data URL\nor `null`; `getName()`, `getMode()`, `isEmpty()`; `undo()`, `clear()`\n(no toast); `submit()` (as the button, when `onSubmit` is given).\n\n### Keys and access\n\nDrawing needs a pointer (mouse, pen, finger). **Type is the keyboard\npath**: Tab to the Draw / Type switch, arrow to Type, and the name field\nis the signature; Enter in it signs (when `onSubmit` is given). The hint\nunder the paper says so. Undo and clear are buttons with names.\n\n## Example\n\nAccepting a quote in a Makersclaw app.\n\n```jsx\n\"use client\";\nimport { updateRow } from \"../lib/actions\";\nimport { SignaturePad } from \"./blocks/signature-pad\";\n\nexport default function AcceptQuote({ quote, me }) {\n return (\n <SignaturePad\n signer={me.name}\n value={quote.signature ?? undefined}\n submitLabel=\"Accept and sign\"\n note=\"Signing records that you accept this quote. It is not a certified e-signature.\"\n onSubmit={(signature) =>\n updateRow({ source: \"public.quotes\", id: quote.id, patch: { status: \"accepted\", signature, signed_by: me.email }, detail: \"accepted\" })\n }\n />\n );\n}\n```\n\n## What it will not do\n\n- Verify who signed, send the document for signature, or keep an audit\n trail: it is a field, not an e-signature service.\n- Pick a colour or pen width: ink is the theme's, width follows speed.\n- Upload the PNG: it returns a data URL; storing it is yours.\n- Take an image of a signature to upload (use `file-upload`).\n"
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"categories": [
|
|
52
|
+
"signature",
|
|
53
|
+
"sign",
|
|
54
|
+
"e-sign",
|
|
55
|
+
"draw",
|
|
56
|
+
"type",
|
|
57
|
+
"accept",
|
|
58
|
+
"approval",
|
|
59
|
+
"consent",
|
|
60
|
+
"waiver",
|
|
61
|
+
"sign-off",
|
|
62
|
+
"png",
|
|
63
|
+
"form"
|
|
64
|
+
],
|
|
65
|
+
"docs": "Installed to components/makersclaw/blocks/signature-pad. Import: import { SignaturePad, useSignaturePad, signatureRecord, formatSignedAt } from \"@/components/makersclaw/blocks/signature-pad\". Props and an example: components/makersclaw/blocks/signature-pad/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
66
|
+
"meta": {
|
|
67
|
+
"makersclaw": {
|
|
68
|
+
"version": "0.3.0",
|
|
69
|
+
"useWhen": "A screen needs someone to sign: accepting a proposal or a quote, a delivery or work-done sign-off, a waiver, a consent form, an approval on a record. The proposal-accept block is built on it. It records an acceptance with a picture of the signature — it is not a certified legal e-signature (no identity check, no audit trail beyond what you store)."
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|