makersclaw 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +41 -1
- package/THIRD-PARTY-NOTICES.md +180 -0
- package/package.json +15 -8
- package/r/annotate.json +62 -0
- package/r/approval-request.json +52 -0
- package/r/audio-player.json +74 -0
- package/r/auth-form.json +55 -0
- package/r/board.json +55 -0
- package/r/calendar-view.json +105 -0
- package/r/cart.json +62 -0
- package/r/chart.json +83 -0
- package/r/chat.json +95 -0
- package/r/code-editor.json +94 -0
- package/r/comment-box.json +64 -0
- package/r/contact-section.json +53 -0
- package/r/core.json +341 -0
- package/r/csv-import.json +63 -0
- package/r/data-table.json +69 -0
- package/r/diagram.json +74 -0
- package/r/doc-editor.json +82 -0
- package/r/doc-page.json +61 -0
- package/r/email-render.json +73 -0
- package/r/faq.json +50 -0
- package/r/file-upload.json +54 -0
- package/r/flow-canvas.json +93 -0
- package/r/footer.json +52 -0
- package/r/gantt.json +70 -0
- package/r/job-panel.json +56 -0
- package/r/json-editor.json +66 -0
- package/r/map.json +89 -0
- package/r/menubar.json +52 -0
- package/r/mind-map.json +67 -0
- package/r/navigation-menu.json +52 -0
- package/r/order-detail.json +54 -0
- package/r/org-chart.json +66 -0
- package/r/page-builder.json +105 -0
- package/r/payment-methods.json +51 -0
- package/r/pdf-document.json +79 -0
- package/r/pdf-viewer.json +88 -0
- package/r/product-detail.json +76 -0
- package/r/product-grid.json +65 -0
- package/r/record-form.json +67 -0
- package/r/record-notes.json +55 -0
- package/r/registry.json +3612 -0
- package/r/search-dialog.json +51 -0
- package/r/settings-list.json +66 -0
- package/r/setup-checklist.json +51 -0
- package/r/share-dialog.json +52 -0
- package/r/sidebar.json +69 -0
- package/r/signature-pad.json +72 -0
- package/r/sketch-canvas.json +99 -0
- package/r/spreadsheet.json +90 -0
- package/r/stat-cards.json +66 -0
- package/r/task-list.json +63 -0
- package/r/task-model.json +98 -0
- package/r/testimonials.json +52 -0
- package/r/transaction-list.json +53 -0
- package/r/whiteboard.json +66 -0
- package/registry/blocks/annotate/README.md +179 -0
- package/registry/blocks/annotate/annotate-data.js +250 -0
- package/registry/blocks/annotate/annotate.jsx +646 -0
- package/registry/blocks/annotate/block.json +10 -0
- package/registry/blocks/annotate/demo.jsx +135 -0
- package/registry/blocks/annotate/index.js +15 -0
- package/registry/blocks/approval-request/README.md +116 -0
- package/registry/blocks/approval-request/approval-request.jsx +386 -0
- package/registry/blocks/approval-request/block.json +10 -0
- package/registry/blocks/approval-request/demo.jsx +127 -0
- package/registry/blocks/approval-request/index.js +4 -0
- package/registry/blocks/audio-player/README.md +167 -0
- package/registry/blocks/audio-player/audio-player.jsx +623 -0
- package/registry/blocks/audio-player/block.json +14 -0
- package/registry/blocks/audio-player/colors.js +109 -0
- package/registry/blocks/audio-player/demo.jsx +209 -0
- package/registry/blocks/audio-player/index.js +6 -0
- package/registry/blocks/audio-player/time.js +67 -0
- package/registry/blocks/audio-player/wavesurfer.js +9 -0
- package/registry/blocks/auth-form/README.md +126 -0
- package/registry/blocks/auth-form/auth-form.jsx +568 -0
- package/registry/blocks/auth-form/block.json +10 -0
- package/registry/blocks/auth-form/demo.jsx +131 -0
- package/registry/blocks/auth-form/index.js +3 -0
- package/registry/blocks/board/README.md +64 -13
- package/registry/blocks/board/block.json +2 -2
- package/registry/blocks/board/board.jsx +237 -94
- package/registry/blocks/board/demo.jsx +103 -24
- package/registry/blocks/calendar-view/README.md +180 -0
- package/registry/blocks/calendar-view/agenda.jsx +147 -0
- package/registry/blocks/calendar-view/block.json +76 -0
- package/registry/blocks/calendar-view/calendar-model.js +442 -0
- package/registry/blocks/calendar-view/calendar.jsx +627 -0
- package/registry/blocks/calendar-view/demo.jsx +195 -0
- package/registry/blocks/calendar-view/draft.jsx +49 -0
- package/registry/blocks/calendar-view/index.js +39 -0
- package/registry/blocks/calendar-view/month.jsx +346 -0
- package/registry/blocks/calendar-view/paint.js +69 -0
- package/registry/blocks/calendar-view/time-grid.jsx +527 -0
- package/registry/blocks/calendar-view/tray.jsx +87 -0
- package/registry/blocks/cart/README.md +146 -0
- package/registry/blocks/cart/block.json +10 -0
- package/registry/blocks/cart/cart.jsx +597 -0
- package/registry/blocks/cart/demo.jsx +98 -0
- package/registry/blocks/cart/index.js +4 -0
- package/registry/blocks/cart/money.js +28 -0
- package/registry/blocks/chart/README.md +316 -0
- package/registry/blocks/chart/block.json +10 -0
- package/registry/blocks/chart/cartesian.jsx +642 -0
- package/registry/blocks/chart/core.jsx +262 -0
- package/registry/blocks/chart/demo.jsx +357 -0
- package/registry/blocks/chart/heatmap.jsx +336 -0
- package/registry/blocks/chart/index.js +8 -0
- package/registry/blocks/chart/pie.jsx +245 -0
- package/registry/blocks/chart/sparkline.jsx +88 -0
- package/registry/blocks/chat/README.md +273 -0
- package/registry/blocks/chat/attachment.jsx +95 -0
- package/registry/blocks/chat/block.json +10 -0
- package/registry/blocks/chat/chat.jsx +132 -0
- package/registry/blocks/chat/composer.jsx +364 -0
- package/registry/blocks/chat/demo.jsx +578 -0
- package/registry/blocks/chat/header.jsx +34 -0
- package/registry/blocks/chat/index.js +9 -0
- package/registry/blocks/chat/message.jsx +450 -0
- package/registry/blocks/chat/scroller.jsx +247 -0
- package/registry/blocks/chat/text.jsx +104 -0
- package/registry/blocks/code-editor/README.md +210 -0
- package/registry/blocks/code-editor/block.json +22 -0
- package/registry/blocks/code-editor/code-editor.jsx +534 -0
- package/registry/blocks/code-editor/codemirror.js +38 -0
- package/registry/blocks/code-editor/demo.jsx +205 -0
- package/registry/blocks/code-editor/index.js +8 -0
- package/registry/blocks/code-editor/json.js +317 -0
- package/registry/blocks/code-editor/sql.js +136 -0
- package/registry/blocks/code-editor/text.js +101 -0
- package/registry/blocks/code-editor/theme.js +186 -0
- package/registry/blocks/comment-box/README.md +123 -0
- package/registry/blocks/comment-box/block.json +10 -0
- package/registry/blocks/comment-box/comment-body.jsx +217 -0
- package/registry/blocks/comment-box/comment-box.jsx +198 -0
- package/registry/blocks/comment-box/comment-time.jsx +77 -0
- package/registry/blocks/comment-box/demo.jsx +141 -0
- package/registry/blocks/comment-box/index.js +8 -0
- package/registry/blocks/contact-section/README.md +152 -0
- package/registry/blocks/contact-section/block.json +11 -0
- package/registry/blocks/contact-section/contact-section.jsx +509 -0
- package/registry/blocks/contact-section/demo.jsx +93 -0
- package/registry/blocks/contact-section/index.js +4 -0
- package/registry/blocks/csv-import/README.md +133 -0
- package/registry/blocks/csv-import/block.json +13 -0
- package/registry/blocks/csv-import/csv-import.jsx +479 -0
- package/registry/blocks/csv-import/demo.jsx +106 -0
- package/registry/blocks/csv-import/import.js +200 -0
- package/registry/blocks/csv-import/index.js +7 -0
- package/registry/blocks/data-table/README.md +386 -0
- package/registry/blocks/data-table/block.json +10 -0
- package/registry/blocks/data-table/data-table.jsx +1164 -0
- package/registry/blocks/data-table/demo.jsx +588 -0
- package/registry/blocks/data-table/index.js +5 -0
- package/registry/blocks/data-table/model.js +169 -0
- package/registry/blocks/diagram/README.md +108 -0
- package/registry/blocks/diagram/block.json +13 -0
- package/registry/blocks/diagram/demo.jsx +113 -0
- package/registry/blocks/diagram/diagram.jsx +481 -0
- package/registry/blocks/diagram/index.js +7 -0
- package/registry/blocks/diagram/mermaid.js +52 -0
- package/registry/blocks/diagram/palette.js +313 -0
- package/registry/blocks/diagram/source.js +98 -0
- package/registry/blocks/doc-editor/README.md +215 -0
- package/registry/blocks/doc-editor/block.json +15 -0
- package/registry/blocks/doc-editor/demo.jsx +225 -0
- package/registry/blocks/doc-editor/doc-editor.jsx +291 -0
- package/registry/blocks/doc-editor/doc.js +309 -0
- package/registry/blocks/doc-editor/index.js +7 -0
- package/registry/blocks/doc-editor/schema.jsx +56 -0
- package/registry/blocks/doc-editor/shadcn-classes.js +110 -0
- package/registry/blocks/doc-editor/theme.js +82 -0
- package/registry/blocks/doc-page/README.md +119 -0
- package/registry/blocks/doc-page/autosave.js +104 -0
- package/registry/blocks/doc-page/block.json +10 -0
- package/registry/blocks/doc-page/demo.jsx +137 -0
- package/registry/blocks/doc-page/doc-page.jsx +276 -0
- package/registry/blocks/doc-page/index.js +4 -0
- package/registry/blocks/email-render/README.md +151 -0
- package/registry/blocks/email-render/block.json +14 -0
- package/registry/blocks/email-render/demo.jsx +153 -0
- package/registry/blocks/email-render/doc-email.jsx +210 -0
- package/registry/blocks/email-render/email.jsx +221 -0
- package/registry/blocks/email-render/index.js +10 -0
- package/registry/blocks/email-render/render.js +50 -0
- package/registry/blocks/email-render/theme.js +215 -0
- package/registry/blocks/faq/README.md +101 -0
- package/registry/blocks/faq/block.json +11 -0
- package/registry/blocks/faq/demo.jsx +89 -0
- package/registry/blocks/faq/faq.jsx +220 -0
- package/registry/blocks/faq/index.js +4 -0
- package/registry/blocks/file-upload/README.md +138 -0
- package/registry/blocks/file-upload/block.json +10 -0
- package/registry/blocks/file-upload/demo.jsx +204 -0
- package/registry/blocks/file-upload/file-upload.jsx +635 -0
- package/registry/blocks/file-upload/index.js +4 -0
- package/registry/blocks/flow-canvas/README.md +420 -0
- package/registry/blocks/flow-canvas/block.json +13 -0
- package/registry/blocks/flow-canvas/board.js +819 -0
- package/registry/blocks/flow-canvas/chrome.jsx +649 -0
- package/registry/blocks/flow-canvas/context.jsx +35 -0
- package/registry/blocks/flow-canvas/demo.jsx +249 -0
- package/registry/blocks/flow-canvas/edges.jsx +107 -0
- package/registry/blocks/flow-canvas/flow-canvas.jsx +1329 -0
- package/registry/blocks/flow-canvas/index.js +41 -0
- package/registry/blocks/flow-canvas/nodes.jsx +392 -0
- package/registry/blocks/flow-canvas/theme.js +174 -0
- package/registry/blocks/footer/README.md +133 -0
- package/registry/blocks/footer/block.json +11 -0
- package/registry/blocks/footer/demo.jsx +119 -0
- package/registry/blocks/footer/footer.jsx +406 -0
- package/registry/blocks/footer/index.js +4 -0
- package/registry/blocks/gantt/README.md +220 -0
- package/registry/blocks/gantt/block.json +10 -0
- package/registry/blocks/gantt/demo.jsx +148 -0
- package/registry/blocks/gantt/gantt.jsx +923 -0
- package/registry/blocks/gantt/index.js +7 -0
- package/registry/blocks/gantt/rows.jsx +468 -0
- package/registry/blocks/gantt/schedule.js +278 -0
- package/registry/blocks/job-panel/README.md +86 -2
- package/registry/blocks/job-panel/block.json +3 -3
- package/registry/blocks/job-panel/demo.jsx +230 -3
- package/registry/blocks/job-panel/job-panel.jsx +169 -3
- package/registry/blocks/json-editor/README.md +133 -0
- package/registry/blocks/json-editor/block.json +10 -0
- package/registry/blocks/json-editor/demo.jsx +92 -0
- package/registry/blocks/json-editor/index.js +7 -0
- package/registry/blocks/json-editor/json-editor.jsx +305 -0
- package/registry/blocks/json-editor/json-tree.jsx +393 -0
- package/registry/blocks/json-editor/tree.js +225 -0
- package/registry/blocks/map/README.md +221 -0
- package/registry/blocks/map/block.json +14 -0
- package/registry/blocks/map/demo.jsx +201 -0
- package/registry/blocks/map/geo.js +198 -0
- package/registry/blocks/map/index.js +8 -0
- package/registry/blocks/map/list.jsx +128 -0
- package/registry/blocks/map/map-view.jsx +715 -0
- package/registry/blocks/map/maplibre.js +30 -0
- package/registry/blocks/map/marks.jsx +146 -0
- package/registry/blocks/map/theme.js +142 -0
- package/registry/blocks/menubar/README.md +109 -0
- package/registry/blocks/menubar/block.json +10 -0
- package/registry/blocks/menubar/demo.jsx +128 -0
- package/registry/blocks/menubar/index.js +4 -0
- package/registry/blocks/menubar/menubar.jsx +199 -0
- package/registry/blocks/mind-map/README.md +206 -0
- package/registry/blocks/mind-map/block.json +10 -0
- package/registry/blocks/mind-map/demo.jsx +103 -0
- package/registry/blocks/mind-map/index.js +31 -0
- package/registry/blocks/mind-map/mind-map.jsx +1125 -0
- package/registry/blocks/mind-map/topic.jsx +143 -0
- package/registry/blocks/mind-map/tree.js +645 -0
- package/registry/blocks/navigation-menu/README.md +124 -0
- package/registry/blocks/navigation-menu/block.json +10 -0
- package/registry/blocks/navigation-menu/demo.jsx +84 -0
- package/registry/blocks/navigation-menu/index.js +4 -0
- package/registry/blocks/navigation-menu/navigation-menu.jsx +448 -0
- package/registry/blocks/order-detail/README.md +109 -0
- package/registry/blocks/order-detail/block.json +10 -0
- package/registry/blocks/order-detail/demo.jsx +88 -0
- package/registry/blocks/order-detail/index.js +4 -0
- package/registry/blocks/order-detail/order-detail.jsx +354 -0
- package/registry/blocks/org-chart/README.md +145 -0
- package/registry/blocks/org-chart/block.json +10 -0
- package/registry/blocks/org-chart/demo.jsx +94 -0
- package/registry/blocks/org-chart/index.js +7 -0
- package/registry/blocks/org-chart/org-chart.jsx +631 -0
- package/registry/blocks/org-chart/org.js +245 -0
- package/registry/blocks/org-chart/person.jsx +96 -0
- package/registry/blocks/page-builder/README.md +257 -0
- package/registry/blocks/page-builder/block.json +14 -0
- package/registry/blocks/page-builder/demo.jsx +145 -0
- package/registry/blocks/page-builder/grapes.js +18 -0
- package/registry/blocks/page-builder/index.js +11 -0
- package/registry/blocks/page-builder/page-builder.jsx +704 -0
- package/registry/blocks/page-builder/page-view.jsx +33 -0
- package/registry/blocks/page-builder/page.js +118 -0
- package/registry/blocks/page-builder/panels.jsx +588 -0
- package/registry/blocks/page-builder/sections.js +209 -0
- package/registry/blocks/page-builder/theme.js +87 -0
- package/registry/blocks/page-builder/thumbs.jsx +189 -0
- package/registry/blocks/page-builder/tokens.js +329 -0
- package/registry/blocks/payment-methods/README.md +104 -0
- package/registry/blocks/payment-methods/block.json +10 -0
- package/registry/blocks/payment-methods/demo.jsx +71 -0
- package/registry/blocks/payment-methods/index.js +4 -0
- package/registry/blocks/payment-methods/payment-methods.jsx +302 -0
- package/registry/blocks/pdf-document/README.md +213 -0
- package/registry/blocks/pdf-document/block.json +13 -0
- package/registry/blocks/pdf-document/demo.jsx +136 -0
- package/registry/blocks/pdf-document/index.js +33 -0
- package/registry/blocks/pdf-document/money.js +83 -0
- package/registry/blocks/pdf-document/pdf-document.jsx +144 -0
- package/registry/blocks/pdf-document/primitives.jsx +271 -0
- package/registry/blocks/pdf-document/quote.jsx +179 -0
- package/registry/blocks/pdf-document/theme.js +193 -0
- package/registry/blocks/pdf-viewer/README.md +191 -0
- package/registry/blocks/pdf-viewer/block.json +14 -0
- package/registry/blocks/pdf-viewer/demo.jsx +252 -0
- package/registry/blocks/pdf-viewer/helpers.js +270 -0
- package/registry/blocks/pdf-viewer/index.js +8 -0
- package/registry/blocks/pdf-viewer/notes.jsx +195 -0
- package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1368 -0
- package/registry/blocks/pdf-viewer/pdfjs.js +37 -0
- package/registry/blocks/pdf-viewer/ssr-shim-end.js +6 -0
- package/registry/blocks/pdf-viewer/ssr-shim.js +44 -0
- package/registry/blocks/product-detail/README.md +217 -0
- package/registry/blocks/product-detail/block.json +10 -0
- package/registry/blocks/product-detail/demo.jsx +233 -0
- package/registry/blocks/product-detail/index.js +7 -0
- package/registry/blocks/product-detail/price.jsx +115 -0
- package/registry/blocks/product-detail/product-detail.jsx +348 -0
- package/registry/blocks/product-detail/product-media.jsx +102 -0
- package/registry/blocks/product-detail/product-options.jsx +144 -0
- package/registry/blocks/product-grid/README.md +162 -0
- package/registry/blocks/product-grid/block.json +10 -0
- package/registry/blocks/product-grid/demo.jsx +255 -0
- package/registry/blocks/product-grid/index.js +5 -0
- package/registry/blocks/product-grid/product-card.jsx +232 -0
- package/registry/blocks/product-grid/product-grid.jsx +235 -0
- package/registry/blocks/record-form/README.md +196 -0
- package/registry/blocks/record-form/block.json +10 -0
- package/registry/blocks/record-form/demo.jsx +525 -0
- package/registry/blocks/record-form/index.js +4 -0
- package/registry/blocks/record-form/record-fields.jsx +457 -0
- package/registry/blocks/record-form/record-form.jsx +415 -0
- package/registry/blocks/record-form/rules.js +81 -0
- package/registry/blocks/record-notes/README.md +139 -0
- package/registry/blocks/record-notes/block.json +10 -0
- package/registry/blocks/record-notes/demo.jsx +179 -0
- package/registry/blocks/record-notes/index.js +4 -0
- package/registry/blocks/record-notes/record-notes.jsx +266 -0
- package/registry/blocks/search-dialog/README.md +108 -0
- package/registry/blocks/search-dialog/block.json +10 -0
- package/registry/blocks/search-dialog/demo.jsx +116 -0
- package/registry/blocks/search-dialog/index.js +3 -0
- package/registry/blocks/search-dialog/search-dialog.jsx +235 -0
- package/registry/blocks/settings-list/README.md +171 -0
- package/registry/blocks/settings-list/block.json +10 -0
- package/registry/blocks/settings-list/demo.jsx +333 -0
- package/registry/blocks/settings-list/index.js +4 -0
- package/registry/blocks/settings-list/row-state.jsx +93 -0
- package/registry/blocks/settings-list/settings-list.jsx +349 -0
- package/registry/blocks/settings-list/settings-matrix.jsx +118 -0
- package/registry/blocks/setup-checklist/README.md +142 -0
- package/registry/blocks/setup-checklist/block.json +10 -0
- package/registry/blocks/setup-checklist/demo.jsx +159 -0
- package/registry/blocks/setup-checklist/index.js +4 -0
- package/registry/blocks/setup-checklist/setup-checklist.jsx +446 -0
- package/registry/blocks/share-dialog/README.md +113 -0
- package/registry/blocks/share-dialog/block.json +10 -0
- package/registry/blocks/share-dialog/demo.jsx +140 -0
- package/registry/blocks/share-dialog/index.js +3 -0
- package/registry/blocks/share-dialog/share-dialog.jsx +384 -0
- package/registry/blocks/sidebar/README.md +190 -0
- package/registry/blocks/sidebar/block.json +43 -0
- package/registry/blocks/sidebar/demo.jsx +577 -0
- package/registry/blocks/sidebar/index.js +4 -0
- package/registry/blocks/sidebar/sidebar-panel.jsx +149 -0
- package/registry/blocks/sidebar/sidebar.jsx +864 -0
- package/registry/blocks/signature-pad/README.md +137 -0
- package/registry/blocks/signature-pad/block.json +13 -0
- package/registry/blocks/signature-pad/colors.js +115 -0
- package/registry/blocks/signature-pad/demo.jsx +94 -0
- package/registry/blocks/signature-pad/index.js +6 -0
- package/registry/blocks/signature-pad/pad.js +7 -0
- package/registry/blocks/signature-pad/signature-pad.jsx +434 -0
- package/registry/blocks/signature-pad/signature.js +68 -0
- package/registry/blocks/sketch-canvas/README.md +280 -0
- package/registry/blocks/sketch-canvas/block.json +13 -0
- package/registry/blocks/sketch-canvas/demo.jsx +159 -0
- package/registry/blocks/sketch-canvas/excalidraw.js +40 -0
- package/registry/blocks/sketch-canvas/index.js +9 -0
- package/registry/blocks/sketch-canvas/palette.js +226 -0
- package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1188 -0
- package/registry/blocks/sketch-canvas/sketch.js +179 -0
- package/registry/blocks/sketch-canvas/ssr-shim-end.js +6 -0
- package/registry/blocks/sketch-canvas/ssr-shim.js +45 -0
- package/registry/blocks/sketch-canvas/stamps.js +104 -0
- package/registry/blocks/sketch-canvas/theme.js +131 -0
- package/registry/blocks/spreadsheet/README.md +307 -0
- package/registry/blocks/spreadsheet/block.json +13 -0
- package/registry/blocks/spreadsheet/cells.jsx +455 -0
- package/registry/blocks/spreadsheet/chrome.jsx +325 -0
- package/registry/blocks/spreadsheet/demo.jsx +177 -0
- package/registry/blocks/spreadsheet/grid.js +387 -0
- package/registry/blocks/spreadsheet/index.js +34 -0
- package/registry/blocks/spreadsheet/spreadsheet.jsx +1732 -0
- package/registry/blocks/spreadsheet/types.js +473 -0
- package/registry/blocks/stat-cards/README.md +173 -0
- package/registry/blocks/stat-cards/block.json +10 -0
- package/registry/blocks/stat-cards/demo.jsx +257 -0
- package/registry/blocks/stat-cards/index.js +4 -0
- package/registry/blocks/stat-cards/stat-cards.jsx +307 -0
- package/registry/blocks/stat-cards/usage.jsx +243 -0
- package/registry/blocks/task-list/README.md +176 -0
- package/registry/blocks/task-list/block.json +10 -0
- package/registry/blocks/task-list/demo.jsx +134 -0
- package/registry/blocks/task-list/index.js +5 -0
- package/registry/blocks/task-list/task-list.jsx +907 -0
- package/registry/blocks/task-model/README.md +455 -0
- package/registry/blocks/task-model/block.json +167 -0
- package/registry/blocks/task-model/changes.js +89 -0
- package/registry/blocks/task-model/dates.js +371 -0
- package/registry/blocks/task-model/demo.jsx +215 -0
- package/registry/blocks/task-model/dnd.jsx +240 -0
- package/registry/blocks/task-model/index.js +97 -0
- package/registry/blocks/task-model/model.js +919 -0
- package/registry/blocks/task-model/parts.jsx +449 -0
- package/registry/blocks/task-model/toolbar.jsx +271 -0
- package/registry/blocks/testimonials/README.md +139 -0
- package/registry/blocks/testimonials/block.json +11 -0
- package/registry/blocks/testimonials/demo.jsx +137 -0
- package/registry/blocks/testimonials/index.js +4 -0
- package/registry/blocks/testimonials/testimonials.jsx +430 -0
- package/registry/blocks/transaction-list/README.md +126 -0
- package/registry/blocks/transaction-list/block.json +10 -0
- package/registry/blocks/transaction-list/demo.jsx +79 -0
- package/registry/blocks/transaction-list/index.js +4 -0
- package/registry/blocks/transaction-list/transaction-list.jsx +383 -0
- package/registry/blocks/whiteboard/README.md +179 -0
- package/registry/blocks/whiteboard/block.json +10 -0
- package/registry/blocks/whiteboard/demo.jsx +169 -0
- package/registry/blocks/whiteboard/index.js +6 -0
- package/registry/blocks/whiteboard/templates.js +150 -0
- package/registry/blocks/whiteboard/whiteboard.jsx +721 -0
- package/src/install.mjs +18 -0
- package/src/shadcn.mjs +171 -0
- package/templates/next/files/README.md +145 -11
- package/templates/next/files/app/globals.css +55 -4
- package/templates/next/files/check/run.mjs +3 -3
- package/templates/next/files/components/parts/fixture.jsx +1 -1
- package/templates/next/files/components/parts/sample.js +15 -0
- package/templates/next/files/components/ui/avatar.jsx +141 -0
- package/templates/next/files/components/ui/breadcrumb.jsx +149 -0
- package/templates/next/files/components/ui/button-group.jsx +70 -0
- package/templates/next/files/components/ui/button.jsx +132 -12
- package/templates/next/files/components/ui/calendar.jsx +609 -0
- package/templates/next/files/components/ui/carousel.jsx +281 -0
- package/templates/next/files/components/ui/collapsible.jsx +208 -0
- package/templates/next/files/components/ui/combobox.jsx +608 -0
- package/templates/next/files/components/ui/command.jsx +577 -0
- package/templates/next/files/components/ui/context-menu.jsx +191 -0
- package/templates/next/files/components/ui/date-picker.jsx +360 -0
- package/templates/next/files/components/ui/distribution.jsx +117 -0
- package/templates/next/files/components/ui/feedback.jsx +143 -0
- package/templates/next/files/components/ui/field.jsx +115 -3
- package/templates/next/files/components/ui/file-input.jsx +234 -0
- package/templates/next/files/components/ui/icon.jsx +130 -8
- package/templates/next/files/components/ui/image.jsx +143 -0
- package/templates/next/files/components/ui/index.js +47 -2
- package/templates/next/files/components/ui/input-group.jsx +213 -0
- package/templates/next/files/components/ui/input-otp.jsx +170 -0
- package/templates/next/files/components/ui/item.jsx +206 -0
- package/templates/next/files/components/ui/menu.jsx +454 -82
- package/templates/next/files/components/ui/notice.jsx +126 -14
- package/templates/next/files/components/ui/pagination.jsx +156 -0
- package/templates/next/files/components/ui/popover.jsx +284 -0
- package/templates/next/files/components/ui/radio.jsx +193 -0
- package/templates/next/files/components/ui/rating.jsx +131 -0
- package/templates/next/files/components/ui/resizable.jsx +338 -0
- package/templates/next/files/components/ui/scroll-area.jsx +123 -0
- package/templates/next/files/components/ui/separator.jsx +119 -0
- package/templates/next/files/components/ui/sheet.jsx +96 -11
- package/templates/next/files/components/ui/slider.jsx +174 -0
- package/templates/next/files/components/ui/stat.jsx +574 -0
- package/templates/next/files/components/ui/steps.jsx +338 -0
- package/templates/next/files/components/ui/swatch.jsx +33 -0
- package/templates/next/files/components/ui/table.jsx +130 -0
- package/templates/next/files/components/ui/toast.jsx +440 -0
- package/templates/next/files/components/ui/toggle.jsx +195 -0
- package/templates/next/files/components/ui/tooltip.jsx +270 -0
- package/templates/next/files/lib/sample.js +4 -12
- package/templates/next/template.json +264 -8
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "page-builder",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "PageBuilder",
|
|
6
|
+
"description": "A visual page editor (GrapesJS core) in the workspace theme: our own clean panels instead of GrapesJS's chrome — an Add panel of section and basic blocks (click or drag), a Layers tree, a Design panel limited to theme-token choices (colours, type, spacing, width, corners, borders), a device toggle (desktop, tablet, phone), undo/redo, preview, and a friendly empty state with starter templates. Pages style with CSS variables, so a saved page repaints with the theme. Saves out as { html, css, project }; PageView shows a saved page in an app.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.3.0/r/core.json"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [
|
|
11
|
+
"grapesjs@0.22.16",
|
|
12
|
+
"@grapesjs/react@2.0.0"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "registry/blocks/page-builder/index.js",
|
|
17
|
+
"type": "registry:component",
|
|
18
|
+
"target": "@components/makersclaw/blocks/page-builder/index.js",
|
|
19
|
+
"content": "// The PageBuilder block — `makersclaw add page-builder`. Fixed: compose it\n// through its props (README.md beside this file). The page helpers are\n// plain functions and work anywhere, the server included; PageView shows a\n// saved page in the theme. The blocks built on this one (landing-builder…)\n// import it, never grapesjs or @grapesjs/react directly.\nexport { PageBuilder, usePageBuilder, readPage, DEVICES } from \"./page-builder\";\nexport { PageView, standaloneDocument } from \"./page-view\";\nexport { BlockThumb, PageThumb, THUMB_NAMES } from \"./thumbs\";\nexport { BASE_CSS, SECTION_BLOCKS, BASIC_BLOCKS, STARTER_TEMPLATES } from \"./sections\";\nexport { STYLE_GROUPS, COLOR_TOKENS, groupsFor, currentOption, patchFor, tokenCss, fontFaceCss, fontLinks } from \"./tokens\";\nexport { PAGE_BASE_CSS, emptyPage, isPageEmpty, cleanHtml, cleanCss, normalizeBlock, templatePage, pageDocument, projectSections } from \"./page\";\n"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"path": "registry/blocks/page-builder/page-builder.jsx",
|
|
23
|
+
"type": "registry:component",
|
|
24
|
+
"target": "@components/makersclaw/blocks/page-builder/page-builder.jsx",
|
|
25
|
+
"content": "\"use client\";\n\n// PageBuilder — a visual page editor (GrapesJS core) in the workspace\n// theme, behind one set of props.\n//\n// What is ours and what is GrapesJS's: GrapesJS keeps the page model, the\n// canvas (an iframe), selection, drag and drop, text editing, undo and the\n// HTML/CSS export. Everything around the canvas is ours, in the kit: the\n// top bar (title, save state, device toggle, undo/redo, preview, Save),\n// the Add / Layers panel, the Design panel (token choices only), the empty\n// state with templates, and a bottom bar with sheets on a narrow screen.\n// GrapesJS runs with <Canvas> from @grapesjs/react, so it creates none of\n// its own panels; theme.js repaints the few things it still draws.\n//\n// Client only, one static bundle: the modules load with the page (they are\n// safe on the server — see grapes.js); the editor is created in an effect\n// after the mount guard. Until then — on the server and in the first client\n// paint — the same frame shows with a skeleton.\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { Button, IconButton, Menu, Sheet, Skeleton, Tabs, TabPanel, ToggleGroup, Tooltip, cx, toast } from \"../../ui\";\nimport { BlocksProvider, Canvas, GjsEditor, grapesjs, useEditorMaybe } from \"./grapes\";\nimport { cleanCss, cleanHtml, normalizeBlock, templatePage } from \"./page\";\nimport { AddPanel, DesignPanel, LayersPanel, TemplateGrid, nameOf, topLevel, useLatest } from \"./panels\";\nimport { BASE_CSS, BASIC_BLOCKS, SECTION_BLOCKS, STARTER_TEMPLATES } from \"./sections\";\nimport { FRAME_CSS, GJS_THEME, GJS_VARS } from \"./theme\";\nimport { KEEP_TOKENS, STYLE_GROUPS, fontFaceCss, fontLinks, tokenCss } from \"./tokens\";\n\n/** A ref for the builder's handle: `const page = usePageBuilder()`, then\n * `<PageBuilder builderRef={page} />` and `page.current?.getPage()`. */\nexport function usePageBuilder() {\n return useRef(null);\n}\n\nexport const DEVICES = [\n { id: \"desktop\", label: \"Desktop\", icon: \"monitor\", width: \"\" },\n { id: \"tablet\", label: \"Tablet\", icon: \"tablet\", width: \"768px\", widthMedia: \"992px\" },\n { id: \"mobile\", label: \"Phone\", icon: \"smartphone\", width: \"390px\", widthMedia: \"480px\" },\n];\n\n/** The page as it goes out: { html, css, project }. */\nexport function readPage(editor) {\n if (!editor) return { html: \"\", css: \"\", project: null };\n return {\n html: cleanHtml(editor.getHtml()),\n css: cleanCss(editor.getCss({ avoidProtected: true }) ?? \"\"),\n project: editor.getProjectData(),\n };\n}\n\nfunction builtInBlocks(builtIns) {\n if (builtIns === false || builtIns === \"none\") return [];\n if (builtIns === \"basics\") return BASIC_BLOCKS;\n if (builtIns === \"sections\") return SECTION_BLOCKS;\n return [...SECTION_BLOCKS, ...BASIC_BLOCKS];\n}\n\nexport function PageBuilder(props) {\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n return mounted ? <Builder {...props} /> : <Placeholder {...props} />;\n}\n\nconst ROOT = cx(\n \"@container relative isolate flex min-h-0 flex-col overflow-hidden rounded-lg border border-border bg-background text-foreground\",\n KEEP_TOKENS,\n);\n\nfunction Placeholder({ title = \"Untitled page\", height = 720, className }) {\n return (\n <div className={cx(ROOT, className)} style={{ height }} aria-busy=\"true\">\n <div className=\"flex h-14 shrink-0 items-center gap-3 border-b border-border px-4\">\n <span className=\"truncate text-sm font-medium\">{title}</span>\n <span className=\"ml-auto\">\n <Skeleton className=\"h-9 w-20\" />\n </span>\n </div>\n <div className=\"flex min-h-0 flex-1\">\n <div className=\"hidden w-72 shrink-0 border-r border-border p-4 @5xl:block\">\n <Skeleton className=\"mb-4 h-9 w-full\" />\n <div className=\"grid grid-cols-2 gap-2\">\n {[0, 1, 2, 3, 4, 5].map((i) => (\n <Skeleton key={i} shape=\"block\" className=\"aspect-[16/10] w-full\" />\n ))}\n </div>\n </div>\n <div className=\"grid min-w-0 flex-1 place-items-center bg-muted p-6\">\n <span className=\"sr-only\">Loading the page editor…</span>\n <div className=\"grid w-full max-w-xl gap-3\" aria-hidden=\"true\">\n <Skeleton className=\"mx-auto h-4 w-40\" />\n <Skeleton className=\"mx-auto h-8 w-4/5\" />\n <Skeleton className=\"mx-auto h-4 w-3/5\" />\n </div>\n </div>\n </div>\n </div>\n );\n}\n\nfunction Builder(props) {\n const { page, height = 720, className } = props;\n const rootRef = useRef(null);\n // Read once: GrapesJS takes its options when the editor is created. To\n // open another page, change the component's `key` (or use the handle).\n const [options] = useState(() => {\n const opts = {\n height: \"100%\",\n width: \"100%\",\n storageManager: false,\n undoManager: { trackSelection: false },\n telemetry: false,\n noticeOnUnload: false,\n cssIcons: \"\",\n jsInHtml: false,\n avoidInlineStyle: true,\n selectorManager: { componentFirst: true },\n richTextEditor: { actions: [\"bold\", \"italic\", \"link\"] },\n deviceManager: {\n default: \"desktop\",\n devices: DEVICES.map((d) => ({ id: d.id, name: d.label, width: d.width, widthMedia: d.widthMedia ?? \"\" })),\n },\n canvas: { frameStyle: \"\", styles: fontLinks() },\n protectedCss: \"* { box-sizing: border-box; } body { margin: 0; }\",\n parser: { optionsHtml: { allowScripts: false, allowUnsafeAttr: false, allowUnsafeAttrValue: false } },\n };\n if (page?.project) opts.projectData = page.project;\n else {\n opts.components = cleanHtml(page?.html ?? \"\");\n opts.style = cleanCss(page?.css ?? \"\");\n }\n return opts;\n });\n\n return (\n <div ref={rootRef} className={cx(ROOT, GJS_VARS, GJS_THEME, className)} style={{ height }}>\n <GjsEditor grapesjs={grapesjs} options={options} className=\"flex min-h-0 flex-1 flex-col\">\n <Shell {...props} rootRef={rootRef} />\n </GjsEditor>\n </div>\n );\n}\n\nfunction Shell({\n rootRef,\n title = \"Untitled page\",\n onSave,\n onChange,\n blocks: extraBlocks,\n builtIns = true,\n templates = STARTER_TEMPLATES,\n styles = STYLE_GROUPS,\n actions,\n saveLabel = \"Save\",\n onUploadImage,\n builderRef,\n}) {\n const editor = useEditorMaybe();\n const [ready, setReady] = useState(false);\n const [rev, setRev] = useState(0);\n const [selected, setSelected] = useState(null);\n const [device, setDevice] = useState(\"desktop\");\n const [preview, setPreview] = useState(false);\n const [dragging, setDragging] = useState(false);\n const [empty, setEmpty] = useState(false);\n const [startBlank, setStartBlank] = useState(false);\n const [dirty, setDirty] = useState(false);\n const [status, setStatus] = useState(\"idle\"); // idle · saving · saved · error\n const [history, setHistory] = useState({ undo: false, redo: false });\n const [tab, setTab] = useState(\"add\");\n const [sheet, setSheet] = useState(null); // add · layers · design · templates\n const saving = useRef(false);\n const outline = useRef(false);\n\n const blocks = useMemo(\n () => [...builtInBlocks(builtIns), ...(extraBlocks ?? [])].map((b, i) => ({ ...normalizeBlock(b, i), level: b.level ?? (b.category === \"Sections\" || b.id === \"section\" ? \"section\" : \"element\") })),\n [builtIns, extraBlocks],\n );\n const blockCss = useMemo(() => [BASE_CSS, ...blocks.map((b) => b.css).filter(Boolean)].join(\"\\n\"), [blocks]);\n const onChangeRef = useLatest(onChange);\n const onSaveRef = useLatest(onSave);\n\n // ---- the editor's life -----------------------------------------------------\n\n // Register (and re-register) the blocks with GrapesJS; their CSS joins\n // the page's rules (only rules a page uses are exported).\n useEffect(() => {\n if (!editor) return;\n const bm = editor.Blocks;\n bm.getAll().reset([]);\n for (const b of blocks) {\n bm.add(b.id, { label: b.label, category: b.category, content: b.content ?? b.html, select: true, activate: false });\n }\n if (ready) editor.Css.addRules(blockCss);\n }, [editor, blocks, blockCss, ready]);\n\n useEffect(() => {\n if (!editor) return;\n const bump = () => setRev((r) => r + 1);\n const sync = () => {\n const um = editor.UndoManager;\n setHistory({ undo: um.hasUndo(), redo: um.hasRedo() });\n setDirty(editor.getDirtyCount() > 0);\n setEmpty(editor.getWrapper()?.components().length === 0);\n };\n const onSelect = () => {\n setSelected(editor.getSelected() ?? null);\n bump();\n };\n let timer = null;\n const onUpdate = () => {\n bump();\n sync();\n if (!onChangeRef.current) return;\n clearTimeout(timer);\n timer = setTimeout(() => onChangeRef.current?.(readPage(editor)), 500);\n };\n // Picture double-click opens GrapesJS's asset modal by default; ours is\n // the Design panel's picture field.\n const openAssets = {\n run: () => {\n const c = editor.getSelected();\n if (rootRef.current && rootRef.current.getBoundingClientRect().width < 1024) setSheet(\"design\");\n // The Design panel may be drawn twice (the side panel, hidden on a\n // narrow screen, and the sheet): focus the one that shows.\n setTimeout(() => {\n const fields = rootRef.current?.ownerDocument.querySelectorAll(`[id=\"pb-src-${c?.cid}\"]`) ?? [];\n [...fields].find((el) => el.offsetParent !== null)?.focus();\n }, 80);\n },\n };\n editor.Commands.add(\"core:open-assets\", openAssets);\n editor.Commands.add(\"open-assets\", openAssets);\n\n const onLoad = () => {\n editor.Css.addRules(blockCss);\n const wrapper = editor.getWrapper();\n wrapper?.set({ selectable: false, hoverable: false, highlightable: false, name: \"Page\" });\n editor.runCommand(\"core:component-outline\");\n outline.current = true;\n editor.UndoManager.clear();\n editor.clearDirtyCount();\n sync();\n setReady(true);\n };\n editor.on(\"component:toggled\", onSelect);\n // \"update\" is GrapesJS's \"the page changed\" (it counts toward the dirty\n // count); hover and selection changes do not fire it.\n editor.on(\"update undo redo\", onUpdate);\n editor.on(\"component:styleUpdate component:add component:remove layer:component\", bump);\n editor.onReady(onLoad);\n return () => {\n clearTimeout(timer);\n editor.off(\"component:toggled\", onSelect);\n editor.off(\"update undo redo\", onUpdate);\n editor.off(\"component:styleUpdate component:add component:remove layer:component\", bump);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [editor]);\n\n // Paint the frame in the theme: the resolved tokens, the app's fonts and\n // the frame's base. Again whenever the light/dark scheme or theme changes.\n useEffect(() => {\n if (!editor || !ready) return;\n let fonts = null;\n let raf = 0;\n const paint = () => {\n const doc = editor.Canvas.getDocument();\n if (!doc?.head) return;\n fonts ??= fontFaceCss();\n let el = doc.getElementById(\"pb-theme\");\n if (!el) {\n el = doc.createElement(\"style\");\n el.id = \"pb-theme\";\n doc.head.appendChild(el);\n }\n el.textContent = `${fonts}\\n${tokenCss(rootRef.current)}\\n${FRAME_CSS}`;\n // A link in the page never navigates the editor's frame.\n if (!doc.__pbLinks) {\n doc.__pbLinks = true;\n doc.addEventListener(\"click\", (e) => e.target?.closest?.(\"a[href]\") && e.preventDefault(), true);\n }\n };\n const later = () => {\n cancelAnimationFrame(raf);\n raf = requestAnimationFrame(paint);\n };\n paint();\n editor.on(\"canvas:frame:load:body\", later);\n const watch = new MutationObserver(later);\n const opts = { attributes: true, attributeFilter: [\"class\", \"style\", \"data-theme\", \"data-scheme\"] };\n watch.observe(document.documentElement, opts);\n if (document.body) watch.observe(document.body, opts);\n const scoped = rootRef.current?.closest?.(\"[data-theme]\");\n if (scoped && scoped !== document.documentElement) watch.observe(scoped, opts);\n const mq = typeof matchMedia !== \"undefined\" ? matchMedia(\"(prefers-color-scheme: dark)\") : null;\n mq?.addEventListener?.(\"change\", later);\n return () => {\n cancelAnimationFrame(raf);\n editor.off(\"canvas:frame:load:body\", later);\n watch.disconnect();\n mq?.removeEventListener?.(\"change\", later);\n };\n }, [editor, ready, rootRef]);\n\n // The canvas follows its box (panels folding, preview, the window).\n useEffect(() => {\n if (!editor || !ready) return;\n const box = editor.Canvas.getElement()?.parentElement;\n if (!box || typeof ResizeObserver === \"undefined\") return;\n let raf = 0;\n const ro = new ResizeObserver(() => {\n cancelAnimationFrame(raf);\n raf = requestAnimationFrame(() => editor.refresh());\n });\n ro.observe(box);\n return () => {\n cancelAnimationFrame(raf);\n ro.disconnect();\n };\n }, [editor, ready]);\n\n // ---- actions ---------------------------------------------------------------------\n\n const select = useCallback(\n (c) => {\n if (!editor || !c) return;\n editor.select(c);\n editor.Canvas.scrollTo(c, { behavior: \"smooth\", block: \"center\" });\n },\n [editor],\n );\n\n const addBlock = useCallback(\n (block) => {\n if (!editor) return;\n const content = block.content ?? block.html;\n const wrapper = editor.getWrapper();\n const sel = editor.getSelected();\n let target = wrapper;\n let at;\n if (sel && sel !== wrapper) {\n if (block.level === \"section\") {\n at = topLevel(editor, sel).index() + 1;\n } else if (!sel.is(\"text\") && !sel.is(\"link\") && !sel.is(\"image\") && sel.get(\"droppable\") !== false) {\n target = sel;\n } else {\n target = sel.parent() ?? wrapper;\n at = sel.index() + 1;\n }\n }\n const added = target.append(content, at === undefined ? {} : { at });\n setStartBlank(true);\n setSheet(null);\n if (added?.[0]) setTimeout(() => select(added[0]), 30);\n },\n [editor, select],\n );\n\n const applyTemplate = useCallback(\n (template, { quiet = false } = {}) => {\n if (!editor) return;\n const before = editor.getProjectData();\n const wasEmpty = editor.getWrapper()?.components().length === 0;\n const next = templatePage(template);\n if (next?.project) editor.loadProjectData(next.project);\n else {\n editor.setComponents(cleanHtml(next?.html ?? \"\"));\n if (next?.css) editor.Css.addRules(cleanCss(next.css));\n }\n editor.Css.addRules(blockCss);\n editor.UndoManager.clear();\n editor.select();\n setSheet(null);\n setStartBlank(true);\n setRev((r) => r + 1);\n setEmpty(editor.getWrapper()?.components().length === 0);\n if (!wasEmpty && !quiet) {\n toast(`Started from “${template.label}”`, {\n description: \"Your previous page was replaced.\",\n action: {\n label: \"Undo\",\n onClick: () => {\n editor.loadProjectData(before);\n editor.Css.addRules(blockCss);\n setRev((r) => r + 1);\n },\n },\n });\n }\n },\n [editor, blockCss],\n );\n\n const act = useCallback(\n (what) => {\n const c = editor?.getSelected();\n if (!c) return;\n const parent = c.parent();\n const i = c.index();\n if (what === \"parent\" && parent && parent !== editor.getWrapper()) select(parent);\n else if ((what === \"up\" || what === \"down\") && parent) {\n if (what === \"up\" ? i <= 0 : i >= parent.components().length - 1) return;\n // move() takes the slot to insert before, counted before the move.\n c.move(parent, { at: what === \"up\" ? i - 1 : i + 2 });\n select(c);\n } else if (what === \"duplicate\" && parent) {\n const copy = c.clone();\n parent.append(copy, { at: i + 1 });\n select(copy);\n } else if (what === \"delete\") {\n const name = nameOf(editor, c);\n c.remove();\n editor.select();\n setSheet((s) => (s === \"design\" ? null : s));\n toast(`${name} deleted`, { action: { label: \"Undo\", onClick: () => editor.UndoManager.undo() } });\n }\n },\n [editor, select],\n );\n\n const save = useCallback(async () => {\n if (!editor || saving.current) return { ok: false };\n saving.current = true;\n setStatus(\"saving\");\n const snapshot = editor.getDirtyCount();\n let res;\n try {\n res = onSaveRef.current ? await onSaveRef.current(readPage(editor)) : { ok: true };\n } catch (e) {\n res = { ok: false, error: e?.message };\n }\n saving.current = false;\n if (res?.ok) {\n // Edits made while saving stay unsaved.\n if (editor.getDirtyCount() === snapshot) editor.clearDirtyCount();\n setDirty(editor.getDirtyCount() > 0);\n setStatus(\"saved\");\n } else {\n setStatus(\"error\");\n toast.error(\"The page was not saved\", {\n description: res?.error ?? \"Your changes are still here. Try again in a moment.\",\n action: { label: \"Try again\", onClick: () => saveRef.current?.() },\n });\n }\n return res ?? { ok: false };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [editor]);\n const saveRef = useLatest(save);\n\n const pickDevice = useCallback(\n (id) => {\n if (!id || !editor) return;\n editor.Devices.select(id);\n setDevice(id);\n },\n [editor],\n );\n\n const togglePreview = useCallback(\n (on) => {\n if (!editor) return;\n if (on) {\n editor.select();\n if (editor.Commands.isActive(\"core:component-outline\")) editor.stopCommand(\"core:component-outline\");\n editor.getModel().stopDefault();\n } else {\n editor.getModel().runDefault();\n if (outline.current) editor.runCommand(\"core:component-outline\");\n }\n setSheet(null);\n setPreview(on);\n },\n [editor],\n );\n\n // ⌘/Ctrl+S saves; Esc leaves preview. Undo, redo and delete are\n // GrapesJS's own keymaps (⌘Z, ⇧⌘Z, Backspace), live in the frame too.\n useEffect(() => {\n if (!editor) return;\n const onKey = (e) => {\n if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === \"s\") {\n e.preventDefault();\n saveRef.current?.();\n } else if (e.key === \"Escape\" && preview) togglePreview(false);\n };\n const frameWin = () => editor.Canvas.getWindow();\n document.addEventListener(\"keydown\", onKey);\n let fw = null;\n const attach = () => {\n fw?.removeEventListener(\"keydown\", onKey);\n fw = frameWin();\n fw?.addEventListener(\"keydown\", onKey);\n };\n if (ready) attach();\n return () => {\n document.removeEventListener(\"keydown\", onKey);\n fw?.removeEventListener(\"keydown\", onKey);\n };\n }, [editor, ready, preview, togglePreview, saveRef]);\n\n // The handle.\n useEffect(() => {\n if (!builderRef) return;\n builderRef.current = editor\n ? {\n editor,\n getPage: () => readPage(editor),\n save: () => saveRef.current?.(),\n addBlock: (id) => {\n const b = blocks.find((x) => x.id === id);\n if (b) addBlock(b);\n return Boolean(b);\n },\n loadTemplate: (t) => {\n const tpl = typeof t === \"string\" ? templates.find((x) => x.id === t) : t;\n if (tpl) applyTemplate(tpl);\n return Boolean(tpl);\n },\n setPage: (p) => applyTemplate(p ?? {}, { quiet: true }),\n undo: () => editor.UndoManager.undo(),\n redo: () => editor.UndoManager.redo(),\n setDevice: pickDevice,\n preview: (on = true) => togglePreview(on),\n }\n : null;\n }, [builderRef, editor, blocks, templates, addBlock, applyTemplate, pickDevice, togglePreview, saveRef]);\n\n // ---- render ------------------------------------------------------------------------\n\n const showEmpty = ready && empty && !startBlank && !dragging && !preview;\n const statusText =\n status === \"saving\" ? \"Saving…\" : status === \"error\" ? \"Not saved\" : dirty ? \"Unsaved changes\" : status === \"saved\" ? \"All changes saved\" : \"No changes yet\";\n\n const addPanel = (\n <AddPanel editor={editor} blocks={blocks} templates={templates} onAdd={addBlock} onDragging={setDragging} onTemplates={() => setSheet(\"templates\")} />\n );\n const layersPanel = <LayersPanel editor={editor} selected={selected} rev={rev} onSelect={select} />;\n const designPanel = <DesignPanel editor={editor} selected={selected} rev={rev} groups={styles} onUploadImage={onUploadImage} onAction={act} />;\n\n const devices = (\n <ToggleGroup\n aria-label=\"Preview width\"\n size=\"sm\"\n value={device}\n onChange={pickDevice}\n items={DEVICES.map((d) => ({ value: d.id, label: d.label, icon: d.icon, iconOnly: true }))}\n />\n );\n\n return (\n <>\n {/* top bar */}\n <header className=\"flex h-14 shrink-0 items-center gap-2 border-b border-border px-3 @3xl:px-4\">\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium leading-tight\">{title}</p>\n <p className={cx(\"truncate text-xs leading-tight\", status === \"error\" ? \"text-destructive\" : \"text-muted-foreground\")} aria-live=\"polite\">\n {statusText}\n </p>\n </div>\n <div className=\"hidden @3xl:block\">{devices}</div>\n <div className=\"flex items-center gap-0.5\">\n <Tooltip label=\"Undo (⌘Z)\">\n <IconButton icon=\"undo\" label=\"Undo\" size=\"sm\" disabled={!history.undo || preview} onClick={() => editor?.UndoManager.undo()} />\n </Tooltip>\n <Tooltip label=\"Redo (⇧⌘Z)\">\n <IconButton icon=\"redo\" label=\"Redo\" size=\"sm\" disabled={!history.redo || preview} onClick={() => editor?.UndoManager.redo()} />\n </Tooltip>\n </div>\n <span className=\"hidden @xl:block\">\n <Button variant={preview ? \"primary\" : \"secondary\"} size=\"sm\" icon={preview ? \"eye-off\" : \"eye\"} onClick={() => togglePreview(!preview)} aria-pressed={preview}>\n {preview ? \"Exit preview\" : \"Preview\"}\n </Button>\n </span>\n <span className=\"@3xl:hidden\">\n <Menu\n label=\"More\"\n items={[\n { type: \"radio\", label: \"Width\", value: device, onValueChange: pickDevice, options: DEVICES.map((d) => ({ value: d.id, label: d.label })) },\n { type: \"separator\" },\n { label: preview ? \"Exit preview\" : \"Preview\", icon: \"eye\", onSelect: () => togglePreview(!preview) },\n templates?.length ? { label: \"Start from a template\", icon: \"file\", onSelect: () => setSheet(\"templates\") } : null,\n ]}\n />\n </span>\n {actions}\n <Button variant=\"primary\" size=\"sm\" loading={status === \"saving\"} disabled={!ready || (!dirty && status !== \"error\")} onClick={save}>\n {saveLabel}\n </Button>\n </header>\n\n <div className=\"relative flex min-h-0 flex-1\">\n {/* left: add + layers */}\n {!preview ? (\n <aside aria-label=\"Blocks and layers\" className=\"hidden w-72 shrink-0 flex-col border-r border-border bg-background @5xl:flex\">\n <Tabs\n aria-label=\"Panel\"\n tabs={[\n { key: \"add\", label: \"Add\" },\n { key: \"layers\", label: \"Layers\" },\n ]}\n value={tab}\n onChange={setTab}\n className=\"shrink-0 px-2\"\n />\n <TabPanel tab=\"add\" value={tab} className=\"flex min-h-0 flex-1 flex-col\">\n {addPanel}\n </TabPanel>\n <TabPanel tab=\"layers\" value={tab} className=\"min-h-0 flex-1 overflow-y-auto\">\n {layersPanel}\n </TabPanel>\n </aside>\n ) : null}\n\n {/* the canvas */}\n <div className=\"relative min-w-0 flex-1 bg-muted\">\n <Canvas className=\"absolute inset-0\" />\n {!ready ? (\n <div className=\"absolute inset-0 grid place-items-center bg-muted p-6\" aria-hidden=\"true\">\n <div className=\"grid w-full max-w-xl gap-3\">\n <Skeleton className=\"mx-auto h-4 w-40\" />\n <Skeleton className=\"mx-auto h-8 w-4/5\" />\n <Skeleton className=\"mx-auto h-4 w-3/5\" />\n </div>\n </div>\n ) : null}\n {showEmpty ? (\n <div className=\"pointer-events-none absolute inset-0 grid place-items-center overflow-y-auto p-4\">\n <div className=\"pointer-events-auto grid w-full max-w-2xl gap-5 rounded-xl border border-border bg-popover p-5 text-popover-foreground shadow-sm @3xl:p-6\">\n <div className=\"grid gap-1\">\n <p className=\"text-lg font-semibold\">Start your page</p>\n <p className=\"text-sm text-muted-foreground\">\n {templates?.length ? \"Pick a template to begin with, or start blank and add sections one at a time.\" : \"Add a section from the Add panel, or drag one onto the page.\"}\n </p>\n </div>\n {templates?.length ? <TemplateGrid templates={templates} onPick={applyTemplate} compact /> : null}\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button\n variant={templates?.length ? \"secondary\" : \"primary\"}\n size=\"sm\"\n icon=\"plus\"\n onClick={() => {\n setStartBlank(true);\n setTab(\"add\");\n if (rootRef.current && rootRef.current.getBoundingClientRect().width < 1024) setSheet(\"add\");\n }}\n >\n {templates?.length ? \"Start blank\" : \"Add a section\"}\n </Button>\n </div>\n </div>\n </div>\n ) : null}\n {preview ? (\n <div className=\"pointer-events-none absolute inset-x-0 bottom-4 flex justify-center\">\n <Button variant=\"primary\" size=\"sm\" icon=\"eye-off\" onClick={() => togglePreview(false)} className=\"pointer-events-auto shadow-md\">\n Exit preview · Esc\n </Button>\n </div>\n ) : null}\n </div>\n\n {/* right: design */}\n {!preview ? (\n <aside aria-label=\"Design\" className=\"hidden w-72 shrink-0 flex-col border-l border-border bg-background @5xl:flex\">\n <div className=\"flex h-11 shrink-0 items-center border-b border-border px-4\">\n <h2 className=\"text-sm font-medium\">Design</h2>\n </div>\n <div className=\"min-h-0 flex-1 overflow-y-auto pt-4\">{designPanel}</div>\n </aside>\n ) : null}\n </div>\n\n {/* narrow: a bottom bar and sheets */}\n {!preview ? (\n <nav aria-label=\"Editor panels\" className=\"grid shrink-0 grid-cols-3 border-t border-border bg-background @5xl:hidden\">\n <Button variant=\"quiet\" icon=\"plus\" onClick={() => setSheet(\"add\")}>\n Add\n </Button>\n <Button variant=\"quiet\" icon=\"layers\" onClick={() => setSheet(\"layers\")}>\n Layers\n </Button>\n <Button variant=\"quiet\" icon=\"palette\" onClick={() => setSheet(\"design\")} disabled={!ready}>\n <span className=\"truncate\">{selected ? `Design · ${nameOf(editor, selected)}` : \"Design\"}</span>\n </Button>\n </nav>\n ) : null}\n\n <Sheet open={sheet === \"add\"} onClose={() => setSheet(null)} title=\"Add a block\" description=\"Tap a block to add it below the selection.\">\n <div className=\"-mx-4 -my-4 flex min-h-[60vh] flex-col pt-3\">{sheet === \"add\" ? addPanel : null}</div>\n </Sheet>\n <Sheet open={sheet === \"layers\"} onClose={() => setSheet(null)} title=\"Layers\">\n <div className=\"-mx-2\">{sheet === \"layers\" ? layersPanel : null}</div>\n </Sheet>\n <Sheet open={sheet === \"design\"} onClose={() => setSheet(null)} title=\"Design\">\n <div className=\"-mx-4\">{sheet === \"design\" ? designPanel : null}</div>\n </Sheet>\n <Sheet open={sheet === \"templates\"} onClose={() => setSheet(null)} side=\"center\" title=\"Start from a template\" description=\"A template replaces the page. You can undo it right after.\">\n {sheet === \"templates\" ? <TemplateGrid templates={templates ?? []} onPick={applyTemplate} /> : null}\n </Sheet>\n\n {/* GrapesJS needs the blocks flagged custom before it is created. */}\n <BlocksProvider>{() => <></>}</BlocksProvider>\n </>\n );\n}\n"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"path": "registry/blocks/page-builder/panels.jsx",
|
|
29
|
+
"type": "registry:component",
|
|
30
|
+
"target": "@components/makersclaw/blocks/page-builder/panels.jsx",
|
|
31
|
+
"content": "\"use client\";\n\n// The builder's panels, in the kit: Add (blocks), Layers (the page's tree),\n// Design (the selected element's look, token choices only) and the\n// template picker. Each reads the GrapesJS editor it is handed and changes\n// the page through the editor's own API, so undo/redo covers every edit.\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { Button, Field, FileInput, Icon, IconButton, Input, Select, Swatch, Switch, ToggleGroup, Tooltip, cx, toast } from \"../../ui\";\nimport { BlocksProvider } from \"./grapes\";\nimport { BlockThumb, PageThumb } from \"./thumbs\";\nimport { currentOption, groupsFor, patchFor, swatchVar } from \"./tokens\";\n\n// ---- helpers over components ------------------------------------------------\n\nconst TEXT_TAGS = new Set([\"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\", \"p\", \"li\", \"span\", \"strong\", \"em\", \"blockquote\", \"label\", \"small\"]);\n\n/** text · link · image · box — which Design groups apply. */\nexport function kindOf(c) {\n if (!c) return \"box\";\n if (c.is(\"image\")) return \"image\";\n if (c.is(\"link\")) return \"link\";\n if (c.is(\"text\") || TEXT_TAGS.has(String(c.get(\"tagName\") ?? \"\").toLowerCase())) return \"text\";\n return \"box\";\n}\n\nexport function nameOf(editor, c) {\n if (!c) return \"\";\n const own = c.get(\"name\") || c.getAttributes?.()[\"data-gjs-name\"];\n if (own) return own;\n const tag = String(c.get(\"tagName\") ?? \"\").toLowerCase();\n if (/^h[1-6]$/.test(tag)) return \"Heading\";\n if (tag === \"p\") return \"Text\";\n if (tag === \"ul\" || tag === \"ol\") return \"List\";\n if (tag === \"li\") return \"List item\";\n if (tag === \"hr\") return \"Divider\";\n if (tag === \"nav\") return \"Links\";\n if (c.is(\"link\")) return c.getClasses?.().includes(\"pb-button\") ? \"Button\" : \"Link\";\n if (c.is(\"image\")) return \"Image\";\n if (c.is(\"text\")) return \"Text\";\n return editor?.Layers?.getName?.(c) || c.getName?.() || \"Box\";\n}\n\n/** The children a layer tree shows: no bare text nodes, nothing inside a\n * text (its bold and italic runs are part of the text). */\nfunction layerKids(c) {\n if (!c || c.is(\"text\") || c.is(\"link\")) return [];\n return c.components().filter((k) => k.get(\"type\") !== \"textnode\" && k.get(\"layerable\") !== false);\n}\n\nexport function topLevel(editor, c) {\n const wrapper = editor.getWrapper();\n let cur = c;\n while (cur?.parent() && cur.parent() !== wrapper) cur = cur.parent();\n return cur;\n}\n\n// ---- Add ---------------------------------------------------------------------\n\n/** The block library: search, blocks grouped by category, click or drag. */\nexport function AddPanel({ editor, blocks, templates, onAdd, onDragging, onTemplates }) {\n const [query, setQuery] = useState(\"\");\n const q = query.trim().toLowerCase();\n const groups = useMemo(() => {\n const out = new Map();\n for (const b of blocks) {\n if (q && !`${b.label} ${b.description} ${b.category}`.toLowerCase().includes(q)) continue;\n if (!out.has(b.category)) out.set(b.category, []);\n out.get(b.category).push(b);\n }\n return [...out.entries()];\n }, [blocks, q]);\n\n return (\n <div className=\"flex min-h-0 flex-1 flex-col\">\n <div className=\"flex items-center gap-2 px-3 pb-3\">\n <Input\n type=\"search\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search blocks\"\n aria-label=\"Search blocks\"\n />\n {templates?.length ? (\n <Tooltip label=\"Start from a template\">\n <IconButton icon=\"file\" label=\"Templates\" size=\"sm\" variant=\"secondary\" onClick={onTemplates} />\n </Tooltip>\n ) : null}\n </div>\n <div className=\"min-h-0 flex-1 overflow-y-auto px-3 pb-4\">\n {groups.length === 0 ? (\n <p className=\"px-1 py-6 text-center text-sm text-muted-foreground\">No block matches “{query}”.</p>\n ) : (\n <BlocksProvider>\n {({ dragStart, dragStop }) => (\n <div className=\"grid gap-5\">\n {groups.map(([category, list]) => (\n <section key={category} aria-label={category} className=\"grid gap-2\">\n <h3 className=\"px-1 text-xs font-medium text-muted-foreground\">{category}</h3>\n <div className=\"grid grid-cols-2 gap-2\">\n {list.map((b) => (\n <BlockTile\n key={b.id}\n block={b}\n onAdd={() => onAdd(b)}\n onDragStart={(ev) => {\n const gb = editor?.Blocks.get(b.id);\n if (!gb) return;\n onDragging?.(true);\n dragStart(gb, ev);\n }}\n onDragEnd={() => {\n dragStop(false);\n onDragging?.(false);\n }}\n />\n ))}\n </div>\n </section>\n ))}\n </div>\n )}\n </BlocksProvider>\n )}\n </div>\n <p className=\"border-t border-border px-4 py-2.5 text-xs text-muted-foreground\">\n Click to add below the selection, or drag onto the page.\n </p>\n </div>\n );\n}\n\nfunction BlockTile({ block, onAdd, onDragStart, onDragEnd }) {\n // raw: a tile that is both a button and an HTML5 drag source; a kit\n // Button is not draggable in every browser (Firefox drops drags that\n // start on a <button>), so this is a div with the button role and keys.\n const tile = (\n <div\n role=\"button\"\n tabIndex={0}\n draggable\n aria-label={`Add ${block.label}`}\n onClick={onAdd}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onAdd();\n }\n }}\n onDragStart={(e) => {\n e.dataTransfer.effectAllowed = \"copy\";\n onDragStart(e.nativeEvent);\n }}\n onDragEnd={onDragEnd}\n className={cx(\n \"group grid cursor-grab gap-1.5 rounded-lg border border-transparent p-1.5 text-left outline-none transition-colors duration-150 active:cursor-grabbing\",\n \"hover:border-border hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none\",\n )}\n >\n <BlockThumb name={block.thumb} className=\"aspect-[16/10] w-full transition-shadow duration-150 group-hover:shadow-sm\" />\n <span className=\"truncate px-0.5 text-xs font-medium\">{block.label}</span>\n </div>\n );\n return block.description ? (\n <Tooltip label={block.description} side=\"right\">\n {tile}\n </Tooltip>\n ) : (\n tile\n );\n}\n\n// ---- Templates -----------------------------------------------------------------\n\n/** Template cards: a stacked wireframe, a name, a line. */\nexport function TemplateGrid({ templates, onPick, compact = false }) {\n return (\n <div className={cx(\"grid gap-3\", compact ? \"grid-cols-2 @2xl:grid-cols-3\" : \"grid-cols-1 sm:grid-cols-2\")}>\n {templates.map((t) => (\n // raw: a card-sized choice (picture, name, description); Button is a\n // one-line control and cannot hold this layout.\n <button\n key={t.id}\n type=\"button\"\n onClick={() => onPick(t)}\n className={cx(\n \"group grid gap-2 rounded-lg border border-border bg-card p-2 text-left outline-none transition-[border-color,box-shadow] duration-150\",\n \"hover:border-primary/50 hover:shadow-sm focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none\",\n )}\n >\n <PageThumb names={t.thumbs ?? []} className=\"w-full\" />\n <span className=\"grid gap-0.5 px-1 pb-1\">\n <span className=\"text-sm font-medium\">{t.label}</span>\n {t.description ? <span className=\"text-xs text-muted-foreground\">{t.description}</span> : null}\n </span>\n </button>\n ))}\n </div>\n );\n}\n\n// ---- Layers ----------------------------------------------------------------------\n\n/** The page as a tree: select, fold, hide. Arrow keys walk it. */\nexport function LayersPanel({ editor, selected, rev, onSelect }) {\n const [closed, setClosed] = useState(() => new Set());\n const list = useRef(null);\n const wrapper = editor?.getWrapper();\n // `rev` changes whenever the page does; the tree is rebuilt from the editor.\n const rows = useMemo(() => {\n const out = [];\n const walk = (c, depth) => {\n for (const k of layerKids(c)) {\n const kids = layerKids(k);\n out.push({ c: k, depth, hasKids: kids.length > 0 });\n if (kids.length && !closed.has(k.cid)) walk(k, depth + 1);\n }\n };\n if (wrapper) walk(wrapper, 0);\n return out;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [wrapper, rev, closed]);\n\n const toggle = (cid) =>\n setClosed((s) => {\n const n = new Set(s);\n n.has(cid) ? n.delete(cid) : n.add(cid);\n return n;\n });\n\n function onKeyDown(e, i, row) {\n const items = [...(list.current?.querySelectorAll(\"[data-layer-row]\") ?? [])];\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n items[Math.min(items.length - 1, Math.max(0, i + (e.key === \"ArrowDown\" ? 1 : -1)))]?.focus();\n } else if (e.key === \"ArrowRight\" && row.hasKids && closed.has(row.c.cid)) {\n e.preventDefault();\n toggle(row.c.cid);\n } else if (e.key === \"ArrowLeft\" && row.hasKids && !closed.has(row.c.cid)) {\n e.preventDefault();\n toggle(row.c.cid);\n }\n }\n\n if (!rows.length) {\n return <p className=\"px-4 py-8 text-center text-sm text-muted-foreground\">Nothing on the page yet. Add a section to begin.</p>;\n }\n return (\n <ul ref={list} role=\"tree\" aria-label=\"Page layers\" className=\"grid gap-px px-2 pb-4\">\n {rows.map((row, i) => {\n const on = selected === row.c;\n const hidden = editor.Layers.isVisible(row.c) === false;\n const open = !closed.has(row.c.cid);\n return (\n <li key={row.c.cid} role=\"treeitem\" aria-selected={on} aria-expanded={row.hasKids ? open : undefined} aria-level={row.depth + 1}>\n <div\n className={cx(\n \"group flex min-h-9 items-center gap-1 rounded-md pr-1 text-sm transition-colors duration-150 motion-reduce:transition-none\",\n on ? \"bg-accent text-accent-foreground\" : \"hover:bg-accent/60\",\n hidden && \"text-muted-foreground\",\n )}\n style={{ paddingInlineStart: 4 + row.depth * 14 }}\n >\n {row.hasKids ? (\n // raw: a 24px fold chevron inside a 36px row; the kit's\n // smallest IconButton is the row's own height.\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={open ? \"Fold\" : \"Unfold\"}\n onClick={() => toggle(row.c.cid)}\n className=\"grid size-6 shrink-0 place-items-center rounded text-muted-foreground hover:text-foreground\"\n >\n <Icon name={open ? \"chevron-down\" : \"chevron-right\"} size={14} />\n </button>\n ) : (\n <span className=\"size-6 shrink-0\" />\n )}\n {/* raw: the row's name is the tree item's focus target; a kit\n Button would add its own padding and 36px minimum inside the row. */}\n <button\n type=\"button\"\n data-layer-row\n onClick={() => onSelect(row.c)}\n onKeyDown={(e) => onKeyDown(e, i, row)}\n className=\"min-w-0 flex-1 truncate py-1.5 text-left outline-none focus-visible:underline\"\n >\n {nameOf(editor, row.c)}\n </button>\n <Tooltip label={hidden ? \"Show while editing\" : \"Hide while editing\"}>\n <IconButton\n icon={hidden ? \"eye-off\" : \"eye\"}\n label={hidden ? `Show ${nameOf(editor, row.c)}` : `Hide ${nameOf(editor, row.c)}`}\n size=\"sm\"\n onClick={() => editor.Layers.setVisible(row.c, hidden)}\n className={cx(\"transition-opacity duration-150\", hidden ? \"opacity-100\" : \"opacity-0 group-hover:opacity-100 focus-visible:opacity-100 [@media(hover:none)]:opacity-100\")}\n />\n </Tooltip>\n </div>\n </li>\n );\n })}\n </ul>\n );\n}\n\n// ---- Design ------------------------------------------------------------------------\n\n/** The selected element's look — only the token choices in `groups` — plus\n * its link or picture, and the moves (parent, up, down, copy, delete). */\nexport function DesignPanel({ editor, selected, rev, groups, onUploadImage, onAction }) {\n if (!selected) {\n return (\n <div className=\"grid gap-4 px-4 py-8 text-center\">\n <span className=\"mx-auto grid size-10 place-items-center rounded-full bg-muted text-muted-foreground\">\n <Icon name=\"mouse-pointer\" size={18} />\n </span>\n <div className=\"grid gap-1\">\n <p className=\"text-sm font-medium\">Select something to style it</p>\n <p className=\"text-sm text-muted-foreground\">Click any part of the page. Double-click text to edit the words.</p>\n </div>\n </div>\n );\n }\n const kind = kindOf(selected);\n const style = selected.getStyle?.() ?? {};\n const shown = groupsFor(groups, kind);\n const parent = selected.parent();\n const index = selected.index();\n const siblings = parent ? parent.components().length : 0;\n\n function apply(group, value) {\n const next = { ...(selected.getStyle?.() ?? {}) };\n for (const [k, v] of Object.entries(patchFor(group, value))) {\n if (v === \"\") delete next[k];\n else next[k] = v;\n }\n selected.setStyle(next);\n }\n\n return (\n <div className=\"grid gap-5 px-4 pb-6\" key={selected.cid}>\n <div className=\"grid gap-2\">\n <div className=\"flex items-baseline justify-between gap-2\">\n <p className=\"truncate text-sm font-semibold\">{nameOf(editor, selected)}</p>\n <p className=\"shrink-0 text-xs text-muted-foreground\">{KIND_LABEL[kind]}</p>\n </div>\n <div className=\"flex flex-wrap gap-1\" role=\"toolbar\" aria-label=\"Arrange\">\n <Tooltip label=\"Select the parent\">\n <IconButton icon=\"arrow-up\" label=\"Select the parent\" size=\"sm\" variant=\"secondary\" disabled={!parent || parent === editor.getWrapper()} onClick={() => onAction(\"parent\")} />\n </Tooltip>\n <Tooltip label=\"Move up\">\n <IconButton icon=\"chevron-up\" label=\"Move up\" size=\"sm\" variant=\"secondary\" disabled={index <= 0} onClick={() => onAction(\"up\")} />\n </Tooltip>\n <Tooltip label=\"Move down\">\n <IconButton icon=\"chevron-down\" label=\"Move down\" size=\"sm\" variant=\"secondary\" disabled={index < 0 || index >= siblings - 1} onClick={() => onAction(\"down\")} />\n </Tooltip>\n <Tooltip label=\"Duplicate\">\n <IconButton icon=\"copy\" label=\"Duplicate\" size=\"sm\" variant=\"secondary\" onClick={() => onAction(\"duplicate\")} />\n </Tooltip>\n <Tooltip label=\"Delete\">\n <IconButton icon=\"trash\" label=\"Delete\" size=\"sm\" variant=\"danger\" onClick={() => onAction(\"delete\")} />\n </Tooltip>\n </div>\n </div>\n\n {kind === \"link\" ? <LinkFields key={`l${selected.cid}`} component={selected} /> : null}\n {kind === \"image\" ? <ImageFields key={`i${selected.cid}`} component={selected} onUploadImage={onUploadImage} /> : null}\n\n {shown.map((g) => (\n <StyleGroup key={g.id} group={g} value={currentOption(g, style)} onChange={(v) => apply(g, v)} />\n ))}\n\n {Object.keys(style).length ? (\n <Button variant=\"quiet\" size=\"sm\" icon=\"refresh\" onClick={() => selected.setStyle({})} className=\"justify-self-start\">\n Clear this element’s style\n </Button>\n ) : null}\n </div>\n );\n}\n\nconst KIND_LABEL = { text: \"Text\", link: \"Link\", image: \"Image\", box: \"Box\" };\n\nfunction StyleGroup({ group, value, onChange }) {\n const current = group.options.find((o) => o.value === value);\n const choices = group.options.filter((o) => o.value !== \"\");\n const id = `pb-${group.id}`;\n return (\n <div className=\"grid gap-2\">\n <div className=\"flex min-h-5 items-center justify-between gap-2\">\n <span id={id} className=\"text-xs font-medium\">\n {group.label}\n </span>\n {value ? (\n <span className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n {value === \"custom\" ? \"Custom\" : current?.label}\n <Button variant=\"link\" size=\"inherit\" tone=\"muted\" onClick={() => onChange(\"\")}>\n Reset\n </Button>\n </span>\n ) : (\n <span className=\"text-xs text-muted-foreground\">Default</span>\n )}\n </div>\n {group.kind === \"swatch\" ? (\n <SwatchRow labelledBy={id} options={choices} value={value} onChange={onChange} />\n ) : group.kind === \"select\" ? (\n <Select\n aria-labelledby={id}\n value={value === \"custom\" ? \"\" : value}\n onChange={(e) => onChange(e.target.value)}\n options={group.options.map((o) => ({ value: o.value, label: o.label }))}\n />\n ) : (\n <ToggleGroup\n aria-label={group.label}\n size=\"sm\"\n value={value}\n onChange={(v) => onChange(v === value ? \"\" : v)}\n items={choices.map((o) => (o.icon ? { value: o.value, label: o.label, icon: o.icon, iconOnly: true } : { value: o.value, label: o.label }))}\n className=\"w-full\"\n />\n )}\n </div>\n );\n}\n\nfunction SwatchRow({ labelledBy, options, value, onChange }) {\n const refs = useRef([]);\n const at = Math.max(0, options.findIndex((o) => o.value === value));\n function onKeyDown(e, i) {\n const d = e.key === \"ArrowRight\" || e.key === \"ArrowDown\" ? 1 : e.key === \"ArrowLeft\" || e.key === \"ArrowUp\" ? -1 : 0;\n if (!d) return;\n e.preventDefault();\n const n = (i + d + options.length) % options.length;\n refs.current[n]?.focus();\n onChange(options[n].value);\n }\n return (\n <div role=\"radiogroup\" aria-labelledby={labelledBy} className=\"flex flex-wrap gap-1.5\">\n {options.map((o, i) => {\n const on = o.value === value;\n const color = swatchVar(o);\n return (\n <Tooltip key={o.value} label={o.label}>\n {/* raw: a colour choice is a swatch you press; the kit's\n ToggleGroup items carry an icon or words, not a colour. */}\n <button\n ref={(el) => {\n refs.current[i] = el;\n }}\n type=\"button\"\n role=\"radio\"\n aria-checked={on}\n aria-label={o.label}\n tabIndex={i === at ? 0 : -1}\n onClick={() => onChange(on ? \"\" : o.value)}\n onKeyDown={(e) => onKeyDown(e, i)}\n className={cx(\n \"grid size-9 place-items-center rounded-md outline-none transition-shadow duration-150 motion-reduce:transition-none\",\n \"focus-visible:ring-2 focus-visible:ring-ring\",\n on ? \"ring-2 ring-primary\" : \"hover:bg-accent\",\n )}\n >\n {color ? (\n <Swatch color={color} className=\"size-6 rounded-full\" />\n ) : (\n <span aria-hidden=\"true\" className=\"relative block size-6 overflow-hidden rounded-full border border-border bg-background\">\n <span className=\"absolute left-1/2 top-[-2px] h-[calc(100%+4px)] w-px -translate-x-1/2 rotate-45 bg-muted-foreground\" />\n </span>\n )}\n </button>\n </Tooltip>\n );\n })}\n </div>\n );\n}\n\n// ---- Content fields ---------------------------------------------------------------\n\nfunction LinkFields({ component }) {\n const attrs = component.getAttributes();\n const [href, setHref] = useState(attrs.href ?? \"\");\n const [blank, setBlank] = useState(attrs.target === \"_blank\");\n const commit = () => {\n const v = href.trim();\n if (v === (component.getAttributes().href ?? \"\")) return;\n if (/^\\s*javascript:/i.test(v)) {\n toast.error(\"That link cannot be used\", { description: \"Use a web address or a path such as /pricing.\" });\n setHref(component.getAttributes().href ?? \"\");\n return;\n }\n component.addAttributes({ href: v });\n };\n return (\n <div className=\"grid gap-3\">\n <Field id={`pb-href-${component.cid}`} label=\"Link\" hint=\"A path like /pricing, or a full web address.\">\n <Input\n id={`pb-href-${component.cid}`}\n value={href}\n onChange={(e) => setHref(e.target.value)}\n onBlur={commit}\n onKeyDown={(e) => e.key === \"Enter\" && commit()}\n placeholder=\"/signup\"\n />\n </Field>\n <Switch\n id={`pb-blank-${component.cid}`}\n label=\"Open in a new tab\"\n checked={blank}\n onChange={(next) => {\n const on = typeof next === \"boolean\" ? next : next?.target?.checked;\n setBlank(on);\n if (on) component.addAttributes({ target: \"_blank\", rel: \"noopener\" });\n else component.removeAttributes([\"target\", \"rel\"]);\n }}\n />\n </div>\n );\n}\n\nfunction ImageFields({ component, onUploadImage }) {\n const attrs = component.getAttributes();\n const [src, setSrc] = useState(/^data:image\\/svg/.test(attrs.src ?? \"\") ? \"\" : attrs.src ?? \"\");\n const [alt, setAlt] = useState(attrs.alt ?? \"\");\n const [busy, setBusy] = useState(false);\n const commitSrc = (v = src) => {\n const next = v.trim();\n if (!next || /^\\s*javascript:/i.test(next)) return;\n component.set(\"src\", next);\n };\n async function upload(files) {\n const file = files?.[0];\n if (!file) return;\n setBusy(true);\n try {\n const res = await onUploadImage(file);\n if (res?.ok && res.url) {\n setSrc(res.url);\n component.set(\"src\", res.url);\n } else {\n toast.error(\"The picture was not uploaded\", { description: res?.error ?? \"Try again, or paste an image address instead.\" });\n }\n } catch {\n toast.error(\"The picture was not uploaded\", { description: \"Try again, or paste an image address instead.\" });\n } finally {\n setBusy(false);\n }\n }\n return (\n <div className=\"grid gap-3\">\n <Field id={`pb-src-${component.cid}`} label=\"Picture\" hint={onUploadImage ? \"Upload one, or paste its address.\" : \"Paste the picture’s address.\"}>\n <div className=\"flex gap-2\">\n <Input\n id={`pb-src-${component.cid}`}\n value={src}\n onChange={(e) => setSrc(e.target.value)}\n onBlur={() => commitSrc()}\n onKeyDown={(e) => e.key === \"Enter\" && commitSrc()}\n placeholder=\"https://…\"\n \n />\n {onUploadImage ? (\n <FileInput accept=\"image/*\" onFiles={upload} label=\"Upload a picture\" size=\"sm\" variant=\"secondary\" disabled={busy} />\n ) : null}\n </div>\n </Field>\n <Field id={`pb-alt-${component.cid}`} label=\"Describe the picture\" hint=\"For people using a screen reader. Leave empty if it is decoration.\">\n <Input\n id={`pb-alt-${component.cid}`}\n value={alt}\n onChange={(e) => setAlt(e.target.value)}\n onBlur={() => component.addAttributes({ alt: alt.trim() })}\n />\n </Field>\n </div>\n );\n}\n\n/** Keep a stable callback identity for handlers that read fresh props. */\nexport function useLatest(value) {\n const ref = useRef(value);\n useEffect(() => {\n ref.current = value;\n });\n return ref;\n}\n"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"path": "registry/blocks/page-builder/page-view.jsx",
|
|
35
|
+
"type": "registry:component",
|
|
36
|
+
"target": "@components/makersclaw/blocks/page-builder/page-view.jsx",
|
|
37
|
+
"content": "// PageView — shows a page the builder made, inside an app, in the theme.\n// Server-safe (no hooks): a landing page can render on the server.\n//\n// The page's CSS names theme tokens (var(--color-…)); inside the app they\n// resolve to the workspace theme, so the page repaints with it. KEEP_TOKENS\n// on the wrapper makes sure Tailwind emits every token the CSS may name.\n// The HTML is cleaned (no scripts, handlers or javascript: links) and the\n// CSS loses any body / html / * rule before either reaches the page.\nimport { cx } from \"../../ui\";\nimport { cleanCss, cleanHtml, pageDocument } from \"./page\";\nimport { KEEP_TOKENS, fontFaceCss, fontLinks, tokenCss } from \"./tokens\";\n\nexport function PageView({ page, label, className }) {\n const html = cleanHtml(page?.html);\n const css = cleanCss(page?.css);\n return (\n <div\n role={label ? \"region\" : undefined}\n aria-label={label || undefined}\n className={cx(\"bg-background font-sans text-[16px] leading-[1.55] text-foreground [&_img]:max-w-full\", KEEP_TOKENS, className)}\n >\n {css ? <style>{css}</style> : null}\n <div dangerouslySetInnerHTML={{ __html: html }} />\n </div>\n );\n}\n\n/** Browser only: the page as a standalone HTML document with the theme\n * resolved where `root` sits (colours, faces, radius) — for a download or\n * a static host that does not load the app's stylesheet. */\nexport function standaloneDocument(page, { root, title = \"Page\", lang = \"en\" } = {}) {\n return pageDocument(page, { title, lang, tokens: tokenCss(root), fonts: fontLinks(), fontFaces: fontFaceCss() });\n}\n"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"path": "registry/blocks/page-builder/thumbs.jsx",
|
|
41
|
+
"type": "registry:component",
|
|
42
|
+
"target": "@components/makersclaw/blocks/page-builder/thumbs.jsx",
|
|
43
|
+
"content": "// Small wireframes of the blocks, for the Add panel and the template cards.\n// Drawn from tokens with plain boxes (decoration, hidden from readers): a\n// bar is a line of text, a filled bar a button, a tinted box a picture.\n// Server-safe. A block names one with `thumb`; an unknown name draws\n// \"blank\".\nimport { cx } from \"../../ui\";\n\nconst Bar = ({ w = \"w-2/3\", strong = false, className }) => (\n <span className={cx(\"block h-1 rounded-full\", strong ? \"bg-foreground/70\" : \"bg-muted-foreground/35\", w, className)} />\n);\nconst Btn = ({ quiet = false, className }) => (\n <span className={cx(\"block h-2 w-6 rounded-sm\", quiet ? \"border border-border\" : \"bg-primary\", className)} />\n);\nconst Pic = ({ className }) => <span className={cx(\"block rounded-sm border border-border bg-accent\", className)} />;\nconst Card = ({ children }) => <span className=\"flex flex-1 flex-col gap-1 rounded-sm border border-border bg-card p-1\">{children}</span>;\n\nconst THUMBS = {\n hero: (\n <span className=\"flex h-full flex-col items-center justify-center gap-1.5\">\n <Bar w=\"w-8\" />\n <Bar w=\"w-3/4\" strong className=\"h-1.5\" />\n <Bar w=\"w-1/2\" />\n <span className=\"flex gap-1\">\n <Btn />\n <Btn quiet />\n </span>\n </span>\n ),\n split: (\n <span className=\"flex h-full items-center gap-2 px-1\">\n <span className=\"flex flex-1 flex-col gap-1.5\">\n <Bar w=\"w-full\" strong className=\"h-1.5\" />\n <Bar w=\"w-3/4\" />\n <Btn />\n </span>\n <Pic className=\"h-10 flex-1\" />\n </span>\n ),\n features: (\n <span className=\"flex h-full flex-col justify-center gap-1.5\">\n <Bar w=\"w-1/2\" strong className=\"mx-auto\" />\n <span className=\"flex gap-1\">\n {[0, 1, 2].map((i) => (\n <Card key={i}>\n <Pic className=\"size-1.5\" />\n <Bar w=\"w-full\" />\n <Bar w=\"w-2/3\" />\n </Card>\n ))}\n </span>\n </span>\n ),\n logos: (\n <span className=\"flex h-full flex-col items-center justify-center gap-2\">\n <Bar w=\"w-1/3\" />\n <span className=\"flex w-full justify-center gap-1.5\">\n {[0, 1, 2, 3].map((i) => (\n <Bar key={i} w=\"w-4\" strong className=\"h-1.5\" />\n ))}\n </span>\n </span>\n ),\n quote: (\n <span className=\"flex h-full flex-col items-center justify-center gap-1.5\">\n <Bar w=\"w-5/6\" strong />\n <Bar w=\"w-2/3\" strong />\n <Bar w=\"w-1/4\" className=\"mt-1\" />\n </span>\n ),\n pricing: (\n <span className=\"flex h-full items-center gap-1.5 px-2\">\n {[0, 1].map((i) => (\n <Card key={i}>\n <Bar w=\"w-1/2\" />\n <Bar w=\"w-1/3\" strong className=\"h-1.5\" />\n <Bar w=\"w-full\" />\n <Btn quiet={i === 0} className=\"w-full\" />\n </Card>\n ))}\n </span>\n ),\n faq: (\n <span className=\"flex h-full flex-col justify-center gap-1.5 px-3\">\n <Bar w=\"w-1/2\" strong />\n {[0, 1, 2].map((i) => (\n <span key={i} className=\"flex flex-col gap-0.5 border-t border-border pt-1\">\n <Bar w=\"w-2/3\" />\n </span>\n ))}\n </span>\n ),\n cta: (\n <span className=\"flex h-full flex-col items-center justify-center gap-1.5 rounded-sm bg-primary\">\n <span className=\"block h-1.5 w-1/2 rounded-full bg-primary-foreground/80\" />\n <span className=\"block h-2 w-6 rounded-sm bg-primary-foreground\" />\n </span>\n ),\n footer: (\n <span className=\"flex h-full flex-col justify-end\">\n <span className=\"flex items-center justify-between gap-1 border-t border-border px-1 pt-2\">\n <Bar w=\"w-4\" strong />\n <span className=\"flex gap-1\">\n <Bar w=\"w-3\" />\n <Bar w=\"w-3\" />\n <Bar w=\"w-3\" />\n </span>\n </span>\n </span>\n ),\n heading: (\n <span className=\"flex h-full items-center px-2\">\n <Bar w=\"w-3/4\" strong className=\"h-2\" />\n </span>\n ),\n text: (\n <span className=\"flex h-full flex-col justify-center gap-1 px-2\">\n <Bar w=\"w-full\" />\n <Bar w=\"w-full\" />\n <Bar w=\"w-2/3\" />\n </span>\n ),\n button: (\n <span className=\"flex h-full items-center justify-center\">\n <span className=\"block h-3 w-10 rounded-sm bg-primary\" />\n </span>\n ),\n image: (\n <span className=\"flex h-full items-center justify-center px-3 py-1.5\">\n <Pic className=\"h-full w-full\" />\n </span>\n ),\n columns: (\n <span className=\"flex h-full items-stretch gap-1.5 p-1.5\">\n <span className=\"flex-1 rounded-sm border border-dashed border-border\" />\n <span className=\"flex-1 rounded-sm border border-dashed border-border\" />\n </span>\n ),\n list: (\n <span className=\"flex h-full flex-col justify-center gap-1 px-3\">\n {[0, 1, 2].map((i) => (\n <span key={i} className=\"flex items-center gap-1\">\n <span className=\"block size-1 rounded-full bg-muted-foreground/60\" />\n <Bar w=\"w-2/3\" />\n </span>\n ))}\n </span>\n ),\n divider: (\n <span className=\"flex h-full items-center px-2\">\n <span className=\"block h-px w-full bg-border\" />\n </span>\n ),\n spacer: (\n <span className=\"flex h-full flex-col items-center justify-center gap-0.5 text-muted-foreground/60\">\n <span className=\"block h-px w-6 bg-muted-foreground/40\" />\n <span className=\"block h-3 w-px bg-muted-foreground/40\" />\n <span className=\"block h-px w-6 bg-muted-foreground/40\" />\n </span>\n ),\n blank: (\n <span className=\"flex h-full items-center justify-center p-1.5\">\n <span className=\"block h-full w-full rounded-sm border border-dashed border-border\" />\n </span>\n ),\n};\n\n/** One block's wireframe, filling its box. */\nexport function BlockThumb({ name, className }) {\n return (\n <span aria-hidden=\"true\" className={cx(\"block overflow-hidden rounded-md border border-border bg-background p-1.5\", className)}>\n {THUMBS[name] ?? THUMBS.blank}\n </span>\n );\n}\n\n/** A whole page's wireframe: its sections stacked, for a template card. */\nexport function PageThumb({ names = [], className }) {\n return (\n <span aria-hidden=\"true\" className={cx(\"flex flex-col gap-1 overflow-hidden rounded-md border border-border bg-background p-1.5\", className)}>\n {(names.length ? names : [\"blank\"]).slice(0, 4).map((n, i) => (\n <span key={i} className=\"block h-9 shrink-0\">\n {THUMBS[n] ?? THUMBS.blank}\n </span>\n ))}\n </span>\n );\n}\n\nexport const THUMB_NAMES = Object.keys(THUMBS);\n"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"path": "registry/blocks/page-builder/grapes.js",
|
|
47
|
+
"type": "registry:component",
|
|
48
|
+
"target": "@components/makersclaw/blocks/page-builder/grapes.js",
|
|
49
|
+
"content": "// The one door to GrapesJS (grapesjs, BSD-3-Clause, core only — never the\n// commercial Studio SDK) and its React wrapper (@grapesjs/react, MIT).\n// Every file in this block reaches them through here; the blocks built on\n// this one (landing-builder…) reach them through PageBuilder, never the\n// packages directly.\n//\n// Both load on the server without a shim: GrapesJS touches `window` only\n// when an editor is created, which happens in an effect after the mount\n// guard. Its stylesheet ships the way sketch-canvas's does: an import of\n// the package's CSS, which the app's bundler picks up. theme.js repaints\n// what of it is still drawn (the canvas spots, the text toolbar) with\n// tokens and hides nothing else — no default panel is ever created, since\n// the editor runs with <Canvas> (custom UI).\nimport \"grapesjs/dist/css/grapes.min.css\";\nimport grapesjs from \"grapesjs\";\nimport GjsEditor, { BlocksProvider, Canvas, WithEditor, useEditorMaybe } from \"@grapesjs/react\";\n\nexport { grapesjs, GjsEditor, BlocksProvider, Canvas, WithEditor, useEditorMaybe };\n"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"path": "registry/blocks/page-builder/theme.js",
|
|
53
|
+
"type": "registry:component",
|
|
54
|
+
"target": "@components/makersclaw/blocks/page-builder/theme.js",
|
|
55
|
+
"content": "// GrapesJS in the workspace theme — the one place its look is mapped. Like\n// sketch-canvas's theme.js for Excalidraw.\n//\n// With <Canvas> (custom UI) GrapesJS draws no panels at all. What it still\n// draws, around the iframe, is: the hover and selection outlines, the name\n// badge, the small toolbar on the selected element (select parent, move,\n// copy, delete), resize handles, the drop placeholder while dragging, and\n// the rich-text toolbar while a text is edited. Its stylesheet paints them\n// from --gjs-* variables set on :root; each is pointed at a token here,\n// set on our wrapper (a nearer ancestor wins). The few colours it writes\n// literally (white toolbar text, white resize handles) are overridden with\n// `!` utilities: its stylesheet is unlayered, and !important inside a\n// layer is the one thing that outranks it.\n//\n// --gjs-left-width / --gjs-canvas-top are the room its default panels\n// would take; there are none, so the canvas fills the frame.\nexport const GJS_VARS = [\n \"[--gjs-color-blue:var(--color-primary)]\",\n \"[--gjs-color-highlight:var(--color-primary)]\",\n \"[--gjs-placeholder-background-color:var(--color-primary)]\",\n \"[--gjs-color-yellow:var(--color-warning)]\",\n \"[--gjs-color-warn:var(--color-warning)]\",\n \"[--gjs-color-red:var(--color-destructive)]\",\n \"[--gjs-color-green:var(--color-success)]\",\n \"[--gjs-primary-color:var(--color-muted)]\",\n \"[--gjs-secondary-color:var(--color-foreground)]\",\n \"[--gjs-main-color:var(--color-popover)]\",\n \"[--gjs-font-color:var(--color-popover-foreground)]\",\n \"[--gjs-font-color-active:var(--color-foreground)]\",\n \"[--gjs-main-dark-color:var(--color-border)]\",\n \"[--gjs-main-light-color:var(--color-accent)]\",\n \"[--gjs-soft-light-color:var(--color-accent)]\",\n \"[--gjs-main-font:var(--theme-font-sans)]\",\n \"[--gjs-font-size:0.8125rem]\",\n \"[--gjs-left-width:0px]\",\n \"[--gjs-canvas-top:0px]\",\n].join(\" \");\n\nexport const GJS_THEME = [\n // the editor and the canvas behind the frame\n \"[&_.gjs-editor]:bg-muted!\",\n \"[&_.gjs-cv-canvas]:bg-muted!\",\n \"[&_.gjs-frame-wrapper]:transition-[width]\",\n \"[&_.gjs-frame-wrapper]:motion-reduce:transition-none\",\n // the selected element's toolbar and the name badge\n \"[&_.gjs-toolbar]:text-primary-foreground!\",\n \"[&_.gjs-toolbar]:rounded-md!\",\n \"[&_.gjs-toolbar]:overflow-hidden\",\n \"[&_.gjs-toolbar]:shadow-sm\",\n \"[&_.gjs-toolbar-item]:w-7!\",\n \"[&_.gjs-toolbar-item]:p-1.5!\",\n \"[&_.gjs-toolbar-item:hover]:bg-primary-foreground/15\",\n \"[&_.gjs-badge]:text-primary-foreground!\",\n \"[&_.gjs-badge]:rounded-md!\",\n \"[&_.gjs-badge]:px-2!\",\n \"[&_.gjs-badge]:py-0.5!\",\n \"[&_.gjs-badge]:text-xs!\",\n \"[&_.gjs-badge]:font-medium!\",\n // resize handles\n \"[&_.gjs-resizer-h]:bg-background!\",\n \"[&_.gjs-resizer-h]:rounded-full\",\n // the text toolbar\n \"[&_.gjs-rte-toolbar]:rounded-lg!\",\n \"[&_.gjs-rte-toolbar]:border!\",\n \"[&_.gjs-rte-toolbar]:border-border!\",\n \"[&_.gjs-rte-toolbar]:shadow-md\",\n \"[&_.gjs-rte-toolbar]:overflow-hidden\",\n \"[&_.gjs-rte-toolbar-ui]:border-0!\",\n \"[&_.gjs-rte-action]:w-8!\",\n \"[&_.gjs-rte-action]:h-8!\",\n \"[&_.gjs-rte-action]:text-popover-foreground!\",\n // the drop placeholder\n \"[&_.gjs-placeholder-int]:rounded-full\",\n].join(\" \");\n\n/** The frame's own stylesheet (inside the iframe; never exported): the\n * page's base on the theme, a calm dashed outline for empty boxes while\n * editing, and a hint in an empty frame. The tokens it reads are written\n * into the frame beside it (tokens.js tokenCss). */\nexport const FRAME_CSS =\n \"body{margin:0;min-height:100vh;background-color:var(--color-background);color:var(--color-foreground);\" +\n \"font-family:var(--theme-font-sans);font-size:16px;line-height:1.55;-webkit-font-smoothing:antialiased}\" +\n \"*,*::before,*::after{box-sizing:border-box}img{max-width:100%}\" +\n \"a{color:inherit}\" +\n \".gjs-dashed *[data-gjs-highlightable]{outline:1px dashed var(--color-border)!important;outline-offset:-1px}\" +\n \".gjs-dashed .pb-col:empty,.gjs-dashed .pb-spacer{background-color:var(--color-muted)}\" +\n \"::selection{background-color:var(--color-accent)}\";\n"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"path": "registry/blocks/page-builder/tokens.js",
|
|
59
|
+
"type": "registry:component",
|
|
60
|
+
"target": "@components/makersclaw/blocks/page-builder/tokens.js",
|
|
61
|
+
"content": "// The token choices — the only styles the Design panel can set, and how a\n// page keeps the workspace theme.\n//\n// A page's CSS never holds a colour. Every colour it can carry is a theme\n// token written as `var(--color-<name>)`, the same variables the app's\n// Tailwind utilities read; fonts and corners read `--theme-font-*` and\n// `--theme-radius`. So a saved page repaints when the theme changes: the\n// values live in the theme, the page only names them.\n//\n// Two places need the tokens defined:\n// the app Tailwind emits a theme variable only when some source names\n// it. KEEP_TOKENS below names every token a page can use (as\n// no-op arbitrary properties on the editor and PageView roots),\n// so installing this block is enough for them to exist.\n// a frame the editor's canvas is an iframe and does not see the app's\n// stylesheet. tokenCss(root) resolves each token where the\n// editor sits (its own light/dark and [data-theme] scope) and\n// the editor writes the result into the frame; it re-runs when\n// the theme changes. pageDocument() uses the same string for a\n// standalone HTML export.\n\n/** Every colour token a page may name, in the order the pickers show. */\nexport const COLOR_TOKENS = [\n \"background\",\n \"foreground\",\n \"card\",\n \"card-foreground\",\n \"primary\",\n \"primary-foreground\",\n \"secondary\",\n \"secondary-foreground\",\n \"muted\",\n \"muted-foreground\",\n \"accent\",\n \"accent-foreground\",\n \"border\",\n \"destructive\",\n \"success\",\n \"warning\",\n \"info\",\n];\n\n/** The non-colour theme variables a page reads (always present: globals.css sets them). */\nexport const THEME_VARS = [\"--theme-font-sans\", \"--theme-font-display\", \"--theme-font-mono\", \"--theme-radius\"];\n\n// Written out whole so Tailwind reads each var() and emits the variable.\nexport const KEEP_TOKENS = [\n \"[--pb-k1:var(--color-background)]\",\n \"[--pb-k2:var(--color-foreground)]\",\n \"[--pb-k3:var(--color-card)]\",\n \"[--pb-k4:var(--color-card-foreground)]\",\n \"[--pb-k5:var(--color-primary)]\",\n \"[--pb-k6:var(--color-primary-foreground)]\",\n \"[--pb-k7:var(--color-secondary)]\",\n \"[--pb-k8:var(--color-secondary-foreground)]\",\n \"[--pb-k9:var(--color-muted)]\",\n \"[--pb-k10:var(--color-muted-foreground)]\",\n \"[--pb-k11:var(--color-accent)]\",\n \"[--pb-k12:var(--color-accent-foreground)]\",\n \"[--pb-k13:var(--color-border)]\",\n \"[--pb-k14:var(--color-destructive)]\",\n \"[--pb-k15:var(--color-success)]\",\n \"[--pb-k16:var(--color-warning)]\",\n \"[--pb-k17:var(--color-info)]\",\n].join(\" \");\n\nconst v = (name) => `var(--color-${name})`;\n\n/**\n * The Design panel's groups. Each option is a plain CSS patch; `match`\n * is the property read back to show which option is on. A consumer can\n * pass its own list (the `styles` prop) built from the same shape; keep\n * colours as tokens (`var(--color-…)`) so the page repaints with the theme.\n */\nexport const STYLE_GROUPS = [\n {\n id: \"color\",\n label: \"Text colour\",\n kind: \"swatch\",\n for: [\"text\", \"link\", \"box\"],\n match: \"color\",\n options: [\n { value: \"\", label: \"Default\", css: { color: \"\" } },\n { value: \"foreground\", label: \"Text\", css: { color: v(\"foreground\") } },\n { value: \"muted-foreground\", label: \"Muted\", css: { color: v(\"muted-foreground\") } },\n { value: \"primary\", label: \"Primary\", css: { color: v(\"primary\") } },\n { value: \"primary-foreground\", label: \"On primary\", css: { color: v(\"primary-foreground\") } },\n { value: \"background\", label: \"Page\", css: { color: v(\"background\") } },\n { value: \"destructive\", label: \"Alert\", css: { color: v(\"destructive\") } },\n ],\n },\n {\n id: \"fill\",\n label: \"Background\",\n kind: \"swatch\",\n for: [\"link\", \"box\"],\n match: \"background-color\",\n options: [\n { value: \"\", label: \"Default\", css: { \"background-color\": \"\" } },\n { value: \"transparent\", label: \"None\", css: { \"background-color\": \"transparent\" } },\n { value: \"background\", label: \"Page\", css: { \"background-color\": v(\"background\") } },\n { value: \"card\", label: \"Card\", css: { \"background-color\": v(\"card\") } },\n { value: \"muted\", label: \"Muted\", css: { \"background-color\": v(\"muted\") } },\n { value: \"accent\", label: \"Accent\", css: { \"background-color\": v(\"accent\") } },\n { value: \"primary\", label: \"Primary\", css: { \"background-color\": v(\"primary\") } },\n { value: \"foreground\", label: \"Inverted\", css: { \"background-color\": v(\"foreground\") } },\n ],\n },\n {\n id: \"size\",\n label: \"Text size\",\n kind: \"select\",\n for: [\"text\", \"link\"],\n match: \"font-size\",\n options: [\n { value: \"\", label: \"Default\", css: { \"font-size\": \"\", \"line-height\": \"\" } },\n { value: \"0.875rem\", label: \"Small\", css: { \"font-size\": \"0.875rem\", \"line-height\": \"1.5\" } },\n { value: \"1rem\", label: \"Body\", css: { \"font-size\": \"1rem\", \"line-height\": \"1.6\" } },\n { value: \"1.25rem\", label: \"Large\", css: { \"font-size\": \"1.25rem\", \"line-height\": \"1.5\" } },\n { value: \"1.75rem\", label: \"Title\", css: { \"font-size\": \"1.75rem\", \"line-height\": \"1.25\" } },\n { value: \"2.5rem\", label: \"Headline\", css: { \"font-size\": \"2.5rem\", \"line-height\": \"1.1\" } },\n { value: \"3.5rem\", label: \"Display\", css: { \"font-size\": \"3.5rem\", \"line-height\": \"1.05\" } },\n ],\n },\n {\n id: \"weight\",\n label: \"Weight\",\n kind: \"segmented\",\n for: [\"text\", \"link\"],\n match: \"font-weight\",\n options: [\n { value: \"\", label: \"Default\", css: { \"font-weight\": \"\" } },\n { value: \"400\", label: \"Regular\", css: { \"font-weight\": \"400\" } },\n { value: \"500\", label: \"Medium\", css: { \"font-weight\": \"500\" } },\n { value: \"700\", label: \"Bold\", css: { \"font-weight\": \"700\" } },\n ],\n },\n {\n id: \"font\",\n label: \"Typeface\",\n kind: \"segmented\",\n for: [\"text\"],\n match: \"font-family\",\n options: [\n { value: \"\", label: \"Default\", css: { \"font-family\": \"\" } },\n { value: \"var(--theme-font-display)\", label: \"Display\", css: { \"font-family\": \"var(--theme-font-display)\" } },\n { value: \"var(--theme-font-mono)\", label: \"Mono\", css: { \"font-family\": \"var(--theme-font-mono)\" } },\n ],\n },\n {\n id: \"align\",\n label: \"Align\",\n kind: \"segmented\",\n for: [\"text\", \"box\"],\n match: \"text-align\",\n options: [\n { value: \"\", label: \"Default\", css: { \"text-align\": \"\" } },\n { value: \"left\", label: \"Left\", icon: \"align-left\", css: { \"text-align\": \"left\" } },\n { value: \"center\", label: \"Centre\", icon: \"align-center\", css: { \"text-align\": \"center\" } },\n { value: \"right\", label: \"Right\", icon: \"align-right\", css: { \"text-align\": \"right\" } },\n ],\n },\n {\n id: \"spaceY\",\n label: \"Space above and below\",\n kind: \"segmented\",\n for: [\"text\", \"link\", \"box\"],\n match: \"padding-top\",\n options: [\n { value: \"\", label: \"Default\", css: { \"padding-top\": \"\", \"padding-bottom\": \"\" } },\n { value: \"0px\", label: \"None\", css: { \"padding-top\": \"0px\", \"padding-bottom\": \"0px\" } },\n { value: \"12px\", label: \"S\", css: { \"padding-top\": \"12px\", \"padding-bottom\": \"12px\" } },\n { value: \"32px\", label: \"M\", css: { \"padding-top\": \"32px\", \"padding-bottom\": \"32px\" } },\n { value: \"64px\", label: \"L\", css: { \"padding-top\": \"64px\", \"padding-bottom\": \"64px\" } },\n { value: \"112px\", label: \"XL\", css: { \"padding-top\": \"112px\", \"padding-bottom\": \"112px\" } },\n ],\n },\n {\n id: \"spaceX\",\n label: \"Space at the sides\",\n kind: \"segmented\",\n for: [\"link\", \"box\"],\n match: \"padding-left\",\n options: [\n { value: \"\", label: \"Default\", css: { \"padding-left\": \"\", \"padding-right\": \"\" } },\n { value: \"0px\", label: \"None\", css: { \"padding-left\": \"0px\", \"padding-right\": \"0px\" } },\n { value: \"12px\", label: \"S\", css: { \"padding-left\": \"12px\", \"padding-right\": \"12px\" } },\n { value: \"24px\", label: \"M\", css: { \"padding-left\": \"24px\", \"padding-right\": \"24px\" } },\n { value: \"48px\", label: \"L\", css: { \"padding-left\": \"48px\", \"padding-right\": \"48px\" } },\n ],\n },\n {\n id: \"width\",\n label: \"Width\",\n kind: \"segmented\",\n for: [\"box\", \"image\"],\n match: \"max-width\",\n options: [\n { value: \"\", label: \"Default\", css: { \"max-width\": \"\", \"margin-left\": \"\", \"margin-right\": \"\" } },\n { value: \"640px\", label: \"Narrow\", css: { \"max-width\": \"640px\", \"margin-left\": \"auto\", \"margin-right\": \"auto\" } },\n { value: \"960px\", label: \"Normal\", css: { \"max-width\": \"960px\", \"margin-left\": \"auto\", \"margin-right\": \"auto\" } },\n { value: \"none\", label: \"Full\", css: { \"max-width\": \"none\", \"margin-left\": \"\", \"margin-right\": \"\" } },\n ],\n },\n {\n id: \"radius\",\n label: \"Corners\",\n kind: \"segmented\",\n for: [\"link\", \"box\", \"image\"],\n match: \"border-radius\",\n options: [\n { value: \"\", label: \"Default\", css: { \"border-radius\": \"\" } },\n { value: \"0px\", label: \"Square\", css: { \"border-radius\": \"0px\" } },\n { value: \"var(--theme-radius)\", label: \"Theme\", css: { \"border-radius\": \"var(--theme-radius)\" } },\n { value: \"calc(var(--theme-radius) * 2.5)\", label: \"Soft\", css: { \"border-radius\": \"calc(var(--theme-radius) * 2.5)\" } },\n { value: \"9999px\", label: \"Pill\", css: { \"border-radius\": \"9999px\" } },\n ],\n },\n {\n id: \"border\",\n label: \"Border\",\n kind: \"swatch\",\n for: [\"link\", \"box\", \"image\"],\n match: \"border-color\",\n options: [\n { value: \"\", label: \"Default\", css: { \"border-width\": \"\", \"border-style\": \"\", \"border-color\": \"\" } },\n { value: \"transparent\", label: \"None\", css: { \"border-width\": \"0px\", \"border-style\": \"none\", \"border-color\": \"transparent\" } },\n { value: \"border\", label: \"Hairline\", css: { \"border-width\": \"1px\", \"border-style\": \"solid\", \"border-color\": v(\"border\") } },\n { value: \"primary\", label: \"Primary\", css: { \"border-width\": \"1px\", \"border-style\": \"solid\", \"border-color\": v(\"primary\") } },\n ],\n },\n];\n\n/** The token name a swatch option paints, for its colour dot (\"\" for\n * Default and None). */\nexport function swatchVar(option) {\n if (!option?.value || option.value === \"transparent\") return \"\";\n return `var(--color-${option.value})`;\n}\n\n/** The groups that apply to a kind of element: text · link · image · box.\n * A group with no `for` applies to every kind. */\nexport function groupsFor(groups, kind) {\n return (groups ?? []).filter((g) => !g.for || g.for.includes(kind));\n}\n\n/** Which option of a group is on, from a component's own style object. */\nexport function currentOption(group, style = {}) {\n const raw = String(style?.[group.match] ?? \"\").replace(/\\s*!important\\s*$/, \"\").trim();\n if (!raw) return \"\";\n for (const o of group.options) {\n const css = o.css?.[group.match];\n if (css && css === raw) return o.value;\n }\n return \"custom\";\n}\n\n/** The patch for choosing `value` in `group`: every prop the group owns,\n * set or cleared (\"\" clears). */\nexport function patchFor(group, value) {\n const option = group.options.find((o) => o.value === value) ?? group.options[0];\n const props = new Set();\n for (const o of group.options) for (const k of Object.keys(o.css ?? {})) props.add(k);\n const patch = {};\n for (const k of props) patch[k] = option.css?.[k] ?? \"\";\n return patch;\n}\n\n/** Browser only: the tokens resolved where `root` sits, as one `:root{…}`\n * rule — for the canvas frame and for a standalone export. Each colour is\n * read off a hidden probe (`color: var(--color-x)`), so light-dark(),\n * color-mix() and a [data-theme] scope all resolve as the app paints them. */\nexport function tokenCss(root) {\n if (typeof document === \"undefined\") return \"\";\n const host = root ?? document.body;\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 host.appendChild(probe);\n const lines = [];\n try {\n for (const name of COLOR_TOKENS) {\n probe.style.color = \"\";\n probe.style.color = `var(--color-${name})`;\n const value = getComputedStyle(probe).color;\n if (value) lines.push(`--color-${name}:${value}`);\n }\n const cs = getComputedStyle(host);\n for (const name of THEME_VARS) {\n const value = cs.getPropertyValue(name).trim();\n if (value) lines.push(`${name}:${value}`);\n }\n const scheme = cs.colorScheme;\n if (scheme && scheme !== \"normal\") lines.push(`color-scheme:${scheme}`);\n } finally {\n probe.remove();\n }\n return `:root{${lines.join(\";\")}}`;\n}\n\n/** Browser only: the @font-face rules the app has loaded (next/font and\n * friends), so the canvas frame draws the theme's typefaces. Rules in a\n * stylesheet from another origin cannot be read and are skipped. */\nexport function fontFaceCss() {\n if (typeof document === \"undefined\") return \"\";\n const out = [];\n for (const sheet of Array.from(document.styleSheets)) {\n let rules;\n try {\n rules = sheet.cssRules;\n } catch {\n continue;\n }\n for (const rule of Array.from(rules ?? [])) {\n if (rule.type === 5 /* CSSRule.FONT_FACE_RULE */) out.push(rule.cssText);\n }\n }\n return out.join(\"\\n\");\n}\n\n/** Browser only: the href of every font stylesheet link (Google Fonts…). */\nexport function fontLinks() {\n if (typeof document === \"undefined\") return [];\n return Array.from(document.querySelectorAll('link[rel=\"stylesheet\"]'))\n .map((l) => l.href)\n .filter((h) => /fonts\\.(googleapis|bunny)\\.|typekit|fontshare/.test(h));\n}\n"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"path": "registry/blocks/page-builder/sections.js",
|
|
65
|
+
"type": "registry:component",
|
|
66
|
+
"target": "@components/makersclaw/blocks/page-builder/sections.js",
|
|
67
|
+
"content": "// The built-in blocks and starter templates, as plain data. Server-safe.\n//\n// Every block is an HTML string with `pb-` classes; BASE_CSS styles those\n// classes with theme tokens only (var(--color-…), --theme-font-*,\n// --theme-radius), so a page built from them repaints with the theme.\n// `data-gjs-name` names a section in the Layers panel. A consumer adds its\n// own blocks in the same shape (README: \"Your own sections\"); a block may\n// carry its own `css`, which the editor adds once.\n//\n// `thumb` picks the small wireframe the Add panel draws for the block:\n// hero · split · features · logos · quote · pricing · faq · cta · footer ·\n// heading · text · button · image · columns · divider · spacer · list ·\n// blank.\n\nexport const BASE_CSS = `\n.pb-section{padding:88px 24px;background-color:var(--color-background);color:var(--color-foreground)}\n.pb-container{max-width:1080px;margin-left:auto;margin-right:auto}\n.pb-narrow{max-width:720px;margin-left:auto;margin-right:auto}\n.pb-center{text-align:center}\n.pb-kicker{display:inline-block;margin:0 0 16px;padding:4px 12px;border:1px solid var(--color-border);border-radius:9999px;font-size:0.8125rem;font-weight:500;color:var(--color-muted-foreground)}\n.pb-display{margin:0 0 20px;font-family:var(--theme-font-display);font-size:3.25rem;line-height:1.05;font-weight:700;letter-spacing:-0.02em}\n.pb-h2{margin:0 0 12px;font-family:var(--theme-font-display);font-size:2.25rem;line-height:1.15;font-weight:700;letter-spacing:-0.015em}\n.pb-h3{margin:0 0 8px;font-size:1.125rem;line-height:1.4;font-weight:600}\n.pb-lead{margin:0 0 32px;font-size:1.25rem;line-height:1.55;color:var(--color-muted-foreground)}\n.pb-text{margin:0 0 16px;font-size:1rem;line-height:1.65}\n.pb-muted{color:var(--color-muted-foreground)}\n.pb-small{font-size:0.875rem;line-height:1.5}\n.pb-actions{display:flex;flex-wrap:wrap;gap:12px;align-items:center}\n.pb-center .pb-actions{justify-content:center}\n.pb-button{display:inline-flex;align-items:center;justify-content:center;min-height:44px;padding:10px 20px;border-radius:var(--theme-radius);background-color:var(--color-primary);color:var(--color-primary-foreground);font-weight:600;font-size:0.9375rem;text-decoration:none;border:1px solid transparent}\n.pb-button-quiet{background-color:transparent;color:var(--color-foreground);border-color:var(--color-border)}\n.pb-grid-2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:48px;align-items:center}\n.pb-grid-3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:24px}\n.pb-cols{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:32px;padding:16px 0;min-height:80px}\n.pb-col{min-height:48px}\n.pb-card{padding:28px;border:1px solid var(--color-border);border-radius:calc(var(--theme-radius) * 1.5);background-color:var(--color-card);color:var(--color-card-foreground)}\n.pb-dot{width:36px;height:36px;margin-bottom:20px;border-radius:var(--theme-radius);background-color:var(--color-accent);border:1px solid var(--color-border)}\n.pb-media{aspect-ratio:4/3;width:100%;border-radius:calc(var(--theme-radius) * 1.5);background-color:var(--color-muted);border:1px solid var(--color-border)}\n.pb-img{display:block;max-width:100%;height:auto;border-radius:var(--theme-radius)}\n.pb-head{margin-bottom:48px}\n.pb-logos{display:flex;flex-wrap:wrap;justify-content:center;gap:16px 48px;margin-top:20px;font-family:var(--theme-font-display);font-size:1.25rem;font-weight:600;color:var(--color-muted-foreground)}\n.pb-logos-section{padding:48px 24px;border-top:1px solid var(--color-border);border-bottom:1px solid var(--color-border)}\n.pb-quote{margin:0 0 24px;font-family:var(--theme-font-display);font-size:1.75rem;line-height:1.35;font-weight:500;letter-spacing:-0.01em}\n.pb-price{margin:8px 0 4px;font-family:var(--theme-font-display);font-size:2.5rem;font-weight:700;line-height:1.1}\n.pb-plan-strong{border-color:var(--color-primary);box-shadow:0 0 0 1px var(--color-primary)}\n.pb-list{margin:20px 0 28px;padding-left:20px;line-height:1.9}\n.pb-faq{border-top:1px solid var(--color-border);padding:20px 0}\n.pb-cta{padding:72px 24px;background-color:var(--color-primary);color:var(--color-primary-foreground)}\n.pb-cta .pb-lead{color:var(--color-primary-foreground);opacity:0.85}\n.pb-cta .pb-button{background-color:var(--color-primary-foreground);color:var(--color-primary)}\n.pb-footer{padding:48px 24px;border-top:1px solid var(--color-border);background-color:var(--color-background);color:var(--color-muted-foreground);font-size:0.875rem}\n.pb-footer-row{display:flex;flex-wrap:wrap;gap:16px 32px;align-items:center;justify-content:space-between}\n.pb-footer-links{display:flex;flex-wrap:wrap;gap:8px 24px}\n.pb-footer a{color:inherit;text-decoration:none}\n.pb-brand{font-family:var(--theme-font-display);font-weight:700;font-size:1.125rem;color:var(--color-foreground)}\n.pb-divider{height:1px;border:0;margin:32px 0;background-color:var(--color-border)}\n.pb-spacer{height:64px}\n@media (max-width: 768px){\n.pb-section{padding:56px 20px}\n.pb-display{font-size:2.25rem}\n.pb-h2{font-size:1.75rem}\n.pb-lead{font-size:1.0625rem}\n.pb-quote{font-size:1.375rem}\n.pb-grid-2,.pb-grid-3,.pb-cols{grid-template-columns:minmax(0,1fr);gap:24px}\n.pb-cta{padding:56px 20px}\n}\n`.trim();\n\nconst hero = `<section class=\"pb-section pb-center\" data-gjs-name=\"Hero\">\n <div class=\"pb-narrow\">\n <p class=\"pb-kicker\">New · Early access is open</p>\n <h1 class=\"pb-display\">The calm way to run your week</h1>\n <p class=\"pb-lead\">Plan the work, share the plan, and see what moved — in one place your whole team will actually open.</p>\n <div class=\"pb-actions\">\n <a class=\"pb-button\" href=\"/signup\">Start free</a>\n <a class=\"pb-button pb-button-quiet\" href=\"/tour\">Take the tour</a>\n </div>\n </div>\n</section>`;\n\nconst split = `<section class=\"pb-section\" data-gjs-name=\"Hero, split\">\n <div class=\"pb-container pb-grid-2\">\n <div>\n <h1 class=\"pb-display\">Every customer, one clear picture</h1>\n <p class=\"pb-lead\">Notes, deals and conversations sit together, so the next step is always obvious.</p>\n <div class=\"pb-actions\">\n <a class=\"pb-button\" href=\"/signup\">Get started</a>\n <a class=\"pb-button pb-button-quiet\" href=\"/demo\">Book a demo</a>\n </div>\n </div>\n <div class=\"pb-media\" data-gjs-name=\"Picture\"></div>\n </div>\n</section>`;\n\nconst features = `<section class=\"pb-section\" data-gjs-name=\"Features\">\n <div class=\"pb-container\">\n <div class=\"pb-head pb-narrow pb-center\">\n <h2 class=\"pb-h2\">Built for the way small teams work</h2>\n <p class=\"pb-lead\">Three things we obsess over, so you do not have to.</p>\n </div>\n <div class=\"pb-grid-3\">\n <div class=\"pb-card\"><div class=\"pb-dot\"></div><h3 class=\"pb-h3\">Set up in minutes</h3><p class=\"pb-text pb-muted\">Import what you have and start where you left off. No training day required.</p></div>\n <div class=\"pb-card\"><div class=\"pb-dot\"></div><h3 class=\"pb-h3\">Everyone on the same page</h3><p class=\"pb-text pb-muted\">One shared view of priorities, owners and dates — updated the moment anything changes.</p></div>\n <div class=\"pb-card\"><div class=\"pb-dot\"></div><h3 class=\"pb-h3\">Private by default</h3><p class=\"pb-text pb-muted\">Your data stays yours. Export everything, any time, in plain formats.</p></div>\n </div>\n </div>\n</section>`;\n\nconst logos = `<section class=\"pb-logos-section pb-center\" data-gjs-name=\"Logos\">\n <p class=\"pb-small pb-muted\">Trusted by teams at</p>\n <div class=\"pb-logos\"><span>Northwind</span><span>Halcyon</span><span>Brightline</span><span>Tessellate</span><span>Oakridge</span></div>\n</section>`;\n\nconst quote = `<section class=\"pb-section pb-center\" data-gjs-name=\"Quote\">\n <div class=\"pb-narrow\">\n <p class=\"pb-quote\">“We replaced three tools and a weekly status meeting. The team finally knows what matters this week.”</p>\n <p class=\"pb-text\"><strong>Maya Okafor</strong><br><span class=\"pb-muted pb-small\">Head of Operations, Brightline</span></p>\n </div>\n</section>`;\n\nconst pricing = `<section class=\"pb-section\" data-gjs-name=\"Pricing\">\n <div class=\"pb-container\">\n <div class=\"pb-head pb-narrow pb-center\">\n <h2 class=\"pb-h2\">Simple pricing</h2>\n <p class=\"pb-lead\">Start free. Upgrade when your team grows.</p>\n </div>\n <div class=\"pb-grid-2\" style=\"align-items:stretch\">\n <div class=\"pb-card\"><h3 class=\"pb-h3\">Starter</h3><p class=\"pb-price\">Free</p><p class=\"pb-text pb-muted\">For you and a teammate.</p><ul class=\"pb-list\"><li>Up to 3 projects</li><li>Shared views</li><li>Email support</li></ul><a class=\"pb-button pb-button-quiet\" href=\"/signup\">Start free</a></div>\n <div class=\"pb-card pb-plan-strong\"><h3 class=\"pb-h3\">Team</h3><p class=\"pb-price\">$12</p><p class=\"pb-text pb-muted\">Per person, per month.</p><ul class=\"pb-list\"><li>Unlimited projects</li><li>Guests and permissions</li><li>Priority support</li></ul><a class=\"pb-button\" href=\"/signup?plan=team\">Try Team free</a></div>\n </div>\n </div>\n</section>`;\n\nconst faq = `<section class=\"pb-section\" data-gjs-name=\"Questions\">\n <div class=\"pb-narrow\">\n <h2 class=\"pb-h2\">Questions, answered</h2>\n <div class=\"pb-faq\"><h3 class=\"pb-h3\">Can I try it before I pay?</h3><p class=\"pb-text pb-muted\">Yes. Every plan starts with a free trial, and the Starter plan is free for good.</p></div>\n <div class=\"pb-faq\"><h3 class=\"pb-h3\">Can I bring my data with me?</h3><p class=\"pb-text pb-muted\">Import a spreadsheet or connect the tools you already use. It takes a few clicks.</p></div>\n <div class=\"pb-faq\"><h3 class=\"pb-h3\">What happens if I cancel?</h3><p class=\"pb-text pb-muted\">You keep read access and can export everything. No questions, no hoops.</p></div>\n </div>\n</section>`;\n\nconst cta = `<section class=\"pb-cta pb-center\" data-gjs-name=\"Call to action\">\n <div class=\"pb-narrow\">\n <h2 class=\"pb-h2\">Ready when you are</h2>\n <p class=\"pb-lead\">Set up your first project in the next five minutes.</p>\n <div class=\"pb-actions\"><a class=\"pb-button\" href=\"/signup\">Start free</a></div>\n </div>\n</section>`;\n\nconst footer = `<footer class=\"pb-footer\" data-gjs-name=\"Footer\">\n <div class=\"pb-container pb-footer-row\">\n <span class=\"pb-brand\">Acme</span>\n <nav class=\"pb-footer-links\"><a href=\"/about\">About</a><a href=\"/pricing\">Pricing</a><a href=\"/blog\">Blog</a><a href=\"/privacy\">Privacy</a></nav>\n <span>© 2026 Acme, Inc.</span>\n </div>\n</footer>`;\n\n/** The built-in blocks. Sections first, then the basics. */\nexport const SECTION_BLOCKS = [\n { id: \"hero\", label: \"Hero\", category: \"Sections\", thumb: \"hero\", description: \"A headline, a line of detail and two buttons, centred.\", html: hero },\n { id: \"hero-split\", label: \"Hero, split\", category: \"Sections\", thumb: \"split\", description: \"Headline and buttons beside a picture.\", html: split },\n { id: \"features\", label: \"Features\", category: \"Sections\", thumb: \"features\", description: \"Three cards that each make one point.\", html: features },\n { id: \"logos\", label: \"Logos\", category: \"Sections\", thumb: \"logos\", description: \"A quiet row of customer names.\", html: logos },\n { id: \"quote\", label: \"Quote\", category: \"Sections\", thumb: \"quote\", description: \"One customer, in their own words.\", html: quote },\n { id: \"pricing\", label: \"Pricing\", category: \"Sections\", thumb: \"pricing\", description: \"Two plans side by side.\", html: pricing },\n { id: \"faq\", label: \"Questions\", category: \"Sections\", thumb: \"faq\", description: \"The questions people ask before they sign up.\", html: faq },\n { id: \"cta\", label: \"Call to action\", category: \"Sections\", thumb: \"cta\", description: \"A bold band with one button.\", html: cta },\n { id: \"footer\", label: \"Footer\", category: \"Sections\", thumb: \"footer\", description: \"Brand, links and the small print.\", html: footer },\n];\n\nexport const BASIC_BLOCKS = [\n { id: \"heading\", label: \"Heading\", category: \"Basics\", thumb: \"heading\", html: `<h2 class=\"pb-h2\">A clear heading</h2>` },\n { id: \"text\", label: \"Text\", category: \"Basics\", thumb: \"text\", html: `<p class=\"pb-text\">Write a sentence or two that says what this part of the page is about.</p>` },\n { id: \"button\", label: \"Button\", category: \"Basics\", thumb: \"button\", html: `<a class=\"pb-button\" href=\"/signup\">Get started</a>` },\n { id: \"image\", label: \"Image\", category: \"Basics\", thumb: \"image\", content: { type: \"image\", classes: [\"pb-img\"], attributes: { alt: \"\" } } },\n { id: \"columns\", label: \"Two columns\", category: \"Basics\", thumb: \"columns\", html: `<div class=\"pb-cols\" data-gjs-name=\"Columns\"><div class=\"pb-col\" data-gjs-name=\"Column\"></div><div class=\"pb-col\" data-gjs-name=\"Column\"></div></div>` },\n { id: \"list\", label: \"List\", category: \"Basics\", thumb: \"list\", html: `<ul class=\"pb-list\"><li>First point</li><li>Second point</li><li>Third point</li></ul>` },\n { id: \"divider\", label: \"Divider\", category: \"Basics\", thumb: \"divider\", html: `<hr class=\"pb-divider\">` },\n { id: \"spacer\", label: \"Space\", category: \"Basics\", thumb: \"spacer\", html: `<div class=\"pb-spacer\" data-gjs-name=\"Space\"></div>` },\n { id: \"section\", label: \"Empty section\", category: \"Basics\", thumb: \"blank\", html: `<section class=\"pb-section\" data-gjs-name=\"Section\"><div class=\"pb-container\"><h2 class=\"pb-h2\">New section</h2><p class=\"pb-text pb-muted\">Drop blocks here.</p></div></section>` },\n];\n\nconst joinBlocks = (ids) => ids.map((id) => SECTION_BLOCKS.find((b) => b.id === id)?.html ?? \"\").join(\"\\n\");\n\n/** The built-in starter templates. A template is a whole page. */\nexport const STARTER_TEMPLATES = [\n {\n id: \"launch\",\n label: \"Product launch\",\n description: \"Hero, logos, features, a quote and a call to action.\",\n thumbs: [\"hero\", \"logos\", \"features\", \"cta\"],\n html: joinBlocks([\"hero\", \"logos\", \"features\", \"quote\", \"cta\", \"footer\"]),\n },\n {\n id: \"sales\",\n label: \"Sales page\",\n description: \"A split hero, features, pricing and questions.\",\n thumbs: [\"split\", \"features\", \"pricing\", \"faq\"],\n html: joinBlocks([\"hero-split\", \"features\", \"pricing\", \"faq\", \"footer\"]),\n },\n {\n id: \"simple\",\n label: \"One-pager\",\n description: \"Just a hero and a footer to build on.\",\n thumbs: [\"hero\", \"footer\"],\n html: joinBlocks([\"hero\", \"footer\"]),\n },\n];\n"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"path": "registry/blocks/page-builder/page.js",
|
|
71
|
+
"type": "registry:component",
|
|
72
|
+
"target": "@components/makersclaw/blocks/page-builder/page.js",
|
|
73
|
+
"content": "// Page helpers — plain functions of plain data, server-safe.\n//\n// A page, as the builder hands it out (onSave, onChange, the handle):\n// { html, css, project }\n// html the body's content, cleaned (no <script>, no on* handlers, no\n// javascript: links)\n// css the rules the page uses, token-based (var(--color-…)); no body\n// or * rules, so it can sit inside an app page\n// project GrapesJS project JSON — what to store to edit the page again\n// Pass `project` back as `page` to reopen it; `html`/`css` alone also open\n// (as a fresh project), which is how a template or an import starts.\n\n/** The base the page's content sits on: the theme's canvas, text and face.\n * PageView gives the same with Tailwind classes; pageDocument() writes it. */\nexport const PAGE_BASE_CSS =\n \"body{margin:0;background-color:var(--color-background);color:var(--color-foreground);\" +\n \"font-family:var(--theme-font-sans);font-size:16px;line-height:1.55;-webkit-font-smoothing:antialiased}\" +\n \"*,*::before,*::after{box-sizing:border-box}img{max-width:100%}\";\n\n/** A page with nothing in it. */\nexport function emptyPage() {\n return { html: \"\", css: \"\", project: null };\n}\n\n/** True when the page has no content: no project components, no html. */\nexport function isPageEmpty(page) {\n if (!page) return true;\n const frames = page.project?.pages?.[0]?.frames;\n if (frames?.length) {\n const root = frames[0]?.component;\n const kids = root?.components;\n if (Array.isArray(kids) && kids.length) return false;\n if (typeof kids === \"string\" && kids.trim()) return false;\n return !textOf(page.html);\n }\n return !textOf(page.html) && !/<(img|hr|video|iframe)\\b/i.test(page.html ?? \"\");\n}\n\nfunction textOf(html) {\n return String(html ?? \"\")\n .replace(/<style[\\s\\S]*?<\\/style>/gi, \"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/ /g, \" \")\n .trim();\n}\n\n/** Remove what a page must never carry into an app: scripts, inline event\n * handlers, javascript: URLs. The editor already emits none; this is for\n * HTML that came from elsewhere (an import, an agent). */\nexport function cleanHtml(html) {\n return String(html ?? \"\")\n .replace(/<script\\b[\\s\\S]*?<\\/script\\s*>/gi, \"\")\n .replace(/<script\\b[^>]*\\/?>/gi, \"\")\n .replace(/\\s+on[a-z]+\\s*=\\s*(\"[^\"]*\"|'[^']*'|[^\\s>]+)/gi, \"\")\n .replace(/(href|src)\\s*=\\s*([\"'])\\s*javascript:[^\"']*\\2/gi, '$1=\"#\"')\n .replace(/^\\s*<body[^>]*>/i, \"\")\n .replace(/<\\/body>\\s*$/i, \"\")\n .trim();\n}\n\n/** Drop the rules that would restyle a host page (body, html, *), keep the\n * rest. */\nexport function cleanCss(css) {\n return String(css ?? \"\")\n .replace(/(?:^|(?<=[{}]))\\s*(?:html|body|\\*)(?:\\s*,\\s*(?:html|body|\\*))*\\s*\\{[^{}]*\\}/g, \"\")\n .trim();\n}\n\n/** A block as the builder uses it: every field filled. */\nexport function normalizeBlock(block, i = 0) {\n const id = String(block?.id ?? `block-${i + 1}`);\n return {\n id,\n label: block?.label ?? id,\n category: block?.category ?? \"Sections\",\n description: block?.description ?? \"\",\n thumb: block?.thumb ?? \"blank\",\n html: block?.html ?? \"\",\n content: block?.content ?? null,\n css: block?.css ?? \"\",\n };\n}\n\n/** A template as the builder loads it: { html, css } or { project }. */\nexport function templatePage(template) {\n if (!template) return null;\n if (template.project) return { project: template.project };\n return { html: template.html ?? \"\", css: template.css ?? \"\" };\n}\n\nconst escapeText = (s) => String(s ?? \"\").replace(/[&<>\"]/g, (c) => ({ \"&\": \"&\", \"<\": \"<\", \">\": \">\", '\"': \""\" })[c]);\n\n/** A standalone HTML document for the page — for a download, an email\n * preview, a static host. `tokens` is the `:root{…}` rule tokenCss()\n * returns in the browser (the theme, resolved); without it the page reads\n * the variables from whatever stylesheet the host adds. */\nexport function pageDocument(page, { title = \"Page\", lang = \"en\", tokens = \"\", fonts = [], fontFaces = \"\" } = {}) {\n const links = fonts.map((href) => `<link rel=\"stylesheet\" href=\"${escapeText(href)}\">`).join(\"\");\n return (\n `<!doctype html><html lang=\"${escapeText(lang)}\"><head><meta charset=\"utf-8\">` +\n `<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">` +\n `<title>${escapeText(title)}</title>${links}` +\n `<style>${fontFaces}\\n${tokens}\\n${PAGE_BASE_CSS}\\n${cleanCss(page?.css)}</style>` +\n `</head><body>${cleanHtml(page?.html)}</body></html>`\n );\n}\n\n/** Walk a project's components (depth first) — for a consumer that wants\n * to read what a page holds without the editor (e.g. list its sections). */\nexport function projectSections(project) {\n const root = project?.pages?.[0]?.frames?.[0]?.component;\n const kids = Array.isArray(root?.components) ? root.components : [];\n return kids.map((c, i) => ({\n index: i,\n name: c.name ?? c.attributes?.[\"data-gjs-name\"] ?? c.tagName ?? c.type ?? \"Section\",\n classes: (c.classes ?? []).map((k) => (typeof k === \"string\" ? k : k.name)).filter(Boolean),\n }));\n}\n"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"path": "registry/blocks/page-builder/README.md",
|
|
77
|
+
"type": "registry:file",
|
|
78
|
+
"target": "@components/makersclaw/blocks/page-builder/README.md",
|
|
79
|
+
"content": "# PageBuilder\n\nA visual page editor in the workspace theme, calm like Webflow's canvas\nwithout its cockpit. The page sits in the middle at desktop, tablet or\nphone width. On the left, **Add** lists blocks as small wireframes —\nsections (hero, features, logos, quote, pricing, questions, call to action,\nfooter) and basics (heading, text, button, image, two columns, list,\ndivider, space). Click one to add it below the selection, or drag it onto\nthe page. **Layers** shows the page as a tree: select, fold, hide while\nediting. On the right, **Design** changes the selected element's look,\nlimited to the theme's choices: text colour, background, text size,\nweight, typeface, alignment, spacing, width, corners and border. It also\nedits a link's address or a picture's source and description, and moves,\nduplicates or deletes the element. The top bar has the save state, the\ndevice toggle, undo and redo, Preview and Save. A new page opens on a\nfriendly empty state with starter templates. On a narrow screen the\npanels move to a bottom bar and open as sheets.\n\nIt is [GrapesJS](https://github.com/GrapesJS/grapesjs) (BSD-3-Clause, the\nfree core — never the commercial Studio SDK) with its React wrapper\n[@grapesjs/react](https://github.com/GrapesJS/react) (MIT). GrapesJS does\nthe page model, the canvas, selection, drag and drop, text editing, undo\nand the export. The panels, the top bar, the empty state and the Design\nchoices are ours, built from the kit. GrapesJS's own panels, style manager,\ntrait manager, asset modal and code viewer are never created. The blocks\nbuilt on this one — `landing-builder` first — import it, never `grapesjs`\ndirectly.\n\n```jsx\nimport { PageBuilder, PageView } from \"../components/blocks/page-builder\"; // a Makersclaw app (\"./blocks/page-builder\" from components/)\nimport { PageBuilder, PageView } from \"@/components/makersclaw/blocks/page-builder\"; // a shadcn project\n```\n\n`PageBuilder` is a client component: render it from a `\"use client\"`\ncomponent. `PageView` and the helpers (`pageDocument`, `cleanHtml`,\n`cleanCss`, `isPageEmpty`, `projectSections`…) work on the server too.\n\n`makersclaw add page-builder` adds `grapesjs` and `@grapesjs/react`\n(pinned, exact) to package.json: run `npm install` after. They load with\nthe page, with no `import()`. The editor is created after the first render.\nUntil then, and on the server, the frame shows a skeleton.\n\n## The data\n\nThe builder hands out a page as three things. You get it from `onSave`,\nfrom `onChange`, and from the handle's `getPage()`:\n\n```js\n{\n html: \"<section class=\\\"pb-section pb-center\\\" …>…</section>…\", // the body's content, cleaned\n css: \".pb-section{padding:88px 24px;background-color:var(--color-background);…}#i4kd{…}\",\n project: { pages: [ … ], styles: [ … ], assets: [], … }, // GrapesJS project JSON\n}\n```\n\n- **Store `project`** (a `jsonb` column). It is what reopens the page for\n editing: pass it back as `page={{ project }}`.\n- **Show `html` + `css`** with `PageView`, or publish them. They are the\n source of truth for what visitors see. They are derived from `project`\n at save time, so store them next to it instead of recomputing them.\n- `page={{ html, css }}` also opens, as a fresh project. This is how a\n template, an import or an agent-written page starts.\n- The HTML is cleaned: no `<script>`, no `on*` handlers, no `javascript:`\n links. The CSS has no `body`, `html` or `*` rules. Only rules the page\n uses are exported.\n\n### Pages keep the theme\n\n**A page never holds a colour. It names theme tokens, so it repaints when\nthe theme changes.** Every Design choice writes a CSS variable:\n`var(--color-primary)`, `var(--color-muted-foreground)`,\n`var(--theme-font-display)`, `var(--theme-radius)`. These are the same\nvariables the app's Tailwind utilities read. The built-in blocks' CSS\n(`BASE_CSS`, `pb-` classes) uses them too. Sizes and spacing are plain\n`rem`/`px` steps.\n\n- **In the editor**, the canvas is an iframe that cannot see the app's\n stylesheet. The builder resolves each token where it sits (its light or\n dark scheme and any `[data-theme]` scope) and writes the values into the\n frame. It does this again when the theme changes. It also copies the\n app's `@font-face` rules, so the page uses the theme's typefaces.\n- **In the app**, `PageView` shows the page. The tokens resolve to the\n workspace theme. Tailwind emits a theme variable only when some source\n names it, so this block names every token a page may use\n (`KEEP_TOKENS`). Installing the block is enough.\n- **Outside the app** (a download, a static host),\n `standaloneDocument(page, { root })` returns a full HTML document with\n the tokens resolved and inlined. `pageDocument(page, { tokens })` is the\n same function for the server, if you pass it the `:root{…}` string\n yourself.\n\n## Props\n\n| Prop | Default | |\n| --- | --- | --- |\n| `page` | empty | The page to open: `{ project }` or `{ html, css }`. Read once, when the editor is created. To open another page, change the component's `key` or call the handle's `setPage`. |\n| `onSave` | — | `async (page) => ({ ok, error? })`. Called by Save and ⌘/Ctrl+S with `{ html, css, project }`. On `{ ok: false }`, the page stays unsaved and a toast shows your `error` with \"Try again\". The top bar says \"Not saved\". |\n| `onChange` | — | `(page) => void`, 500 ms after the last edit. Use it for autosave or a live preview elsewhere. |\n| `title` | `\"Untitled page\"` | The name in the top bar. |\n| `blocks` | `[]` | Your own blocks, added after the built-ins. See \"Your own sections\". |\n| `builtIns` | `true` | Which built-in blocks to offer: `true` (sections and basics), `\"sections\"`, `\"basics\"` or `false`. |\n| `templates` | `STARTER_TEMPLATES` | Whole pages offered in the empty state and in \"Start from a template\". Pass `[]` for none. |\n| `styles` | `STYLE_GROUPS` | The Design panel's choices. See \"The Design choices\". |\n| `onUploadImage` | — | `async (file) => ({ ok, url, error? })`. When given, a picture's field gets an Upload button. Without it, people paste an address. |\n| `actions` | — | Extra top-bar controls before Save, such as landing-builder's Publish. Use kit `Button`s at `size=\"sm\"`. |\n| `saveLabel` | `\"Save\"` | The Save button's words. |\n| `builderRef` | — | A ref from `usePageBuilder()`. It receives the handle. |\n| `height` | `720` | The builder's height: a number (px) or any CSS length (`\"min(760px, 88dvh)\"`). It fills its width. |\n| `className` | — | Added to the frame. |\n\n### The handle\n\n```jsx\nconst builder = usePageBuilder();\n<PageBuilder builderRef={builder} … />\nbuilder.current?.getPage(); // { html, css, project }\n```\n\n`getPage()` · `save()` (runs `onSave`, returns its result) ·\n`addBlock(id)` · `loadTemplate(idOrTemplate)` · `setPage({ html, css } |\n{ project })` · `undo()` · `redo()` · `setDevice(\"desktop\" | \"tablet\" |\n\"mobile\")` · `preview(on = true)` · `editor` (the GrapesJS editor, for what\nthis block does not wrap; changes made through it are yours to keep\ntoken-only).\n\n## Your own sections\n\nThis is how `landing-builder` builds on this block. A block is plain data:\n\n```js\nconst HERO_WAITLIST = {\n id: \"hero-waitlist\", // unique across built-ins and yours\n label: \"Waitlist hero\", // the tile's name\n category: \"Hero\", // the Add panel's group heading\n thumb: \"split\", // a wireframe from THUMB_NAMES\n description: \"Headline, one line and an email button.\", // the tile's tooltip\n level: \"section\", // \"section\": added between top-level sections ·\n // \"element\": added inside the selected box\n // (default: \"section\" when category is \"Sections\")\n html: `<section class=\"lb-hero\" data-gjs-name=\"Waitlist hero\">…</section>`,\n css: `.lb-hero{padding:96px 24px;background-color:var(--color-muted);color:var(--color-foreground)}`,\n};\n<PageBuilder blocks={[HERO_WAITLIST]} builtIns=\"basics\" templates={LANDING_TEMPLATES} />\n```\n\n- `html` is the markup, or use `content`, a GrapesJS component definition,\n in its place. `data-gjs-name` names the element in Layers. Any\n `data-gjs-*` attribute sets that GrapesJS property: for example,\n `data-gjs-draggable=\"false\"` or `data-gjs-removable=\"false\"` lock a part.\n- `css` is added to the page once, when the editor opens. **Name only\n tokens for colour** (`var(--color-…)`, from `COLOR_TOKENS`) and\n `--theme-font-*` / `--theme-radius` for faces and corners. Then your\n sections repaint with the theme. Prefix your classes (`lb-`), and reuse\n the built-in `pb-` classes (`pb-button`, `pb-h2`, `pb-lead`,\n `pb-container`…) when they fit. Add an `@media (max-width: 768px)` rule\n for the phone layout. The canvas's phone width is a real narrow frame,\n so media queries work there.\n- Several blocks may share one `css` string. The same rule added twice is\n one rule.\n\nA **template** is a whole page:\n`{ id, label, description?, thumbs?: [\"hero\", \"features\", \"cta\"], html, css? }`\nor `{ id, label, project }`. `thumbs` draws its card, up to four sections\ntall. Picking a template replaces the page. A toast offers Undo when there\nwas something to replace.\n\n`BlockThumb` and `PageThumb` draw the same wireframes elsewhere, for\nexample in a landing-builder gallery of templates.\n\n## The Design choices\n\n`STYLE_GROUPS` is a list of groups. Each group is one row of choices:\n\n```js\n{\n id: \"fill\", label: \"Background\", kind: \"swatch\" | \"segmented\" | \"select\",\n for: [\"link\", \"box\"], // which elements show it: text · link · image · box (all if left out)\n match: \"background-color\", // the property read back to show the current choice\n options: [\n { value: \"\", label: \"Default\", css: { \"background-color\": \"\" } }, // \"\" = reset: the block's own CSS applies\n { value: \"primary\", label: \"Primary\", css: { \"background-color\": \"var(--color-primary)\" } },\n ],\n}\n```\n\nPicking an option writes its `css` onto that element only (an `#id`\nrule). Any property another option of the group sets is cleared. \"Reset\"\nbeside a group removes the group's properties. \"Clear this element's\nstyle\" removes all of them. To offer fewer or different choices, pass\nyour own list, filtered from or modelled on `STYLE_GROUPS`. A swatch\noption's `value` is the token name it paints (`primary` →\n`var(--color-primary)`). `groupsFor`, `currentOption` and `patchFor` are\nthe helpers the panel uses.\n\n## Mouse, keyboard and touch\n\n- Click an element to select it. Double-click text to edit the words\n (bold, italic and link in the small toolbar). Double-click a picture to\n jump to its field. Drag the selected element's handle to move it.\n- ⌘/Ctrl+S saves. ⌘/Ctrl+Z undoes, ⇧⌘Z redoes. Backspace or Delete\n removes the selection (with Undo in a toast when done from the panel).\n Esc leaves Preview.\n- The block tiles, the layer tree (↑ ↓ to move, ← → to fold), the swatches\n (arrow keys) and every control are reachable by keyboard. A tile adds\n on Enter.\n- On a phone, tap a tile to add it. Dragging is for a mouse.\n\n## Example\n\n```jsx\n\"use client\";\nimport { useState } from \"react\";\nimport { PageBuilder, PageView } from \"../components/blocks/page-builder\";\nimport { saveRow } from \"../lib/db\";\n\nexport function LandingEditor({ row }) {\n return (\n <PageBuilder\n key={row.id}\n title={row.title}\n page={row.project ? { project: row.project } : { html: row.html, css: row.css }}\n onSave={async ({ html, css, project }) => {\n const res = await saveRow(\"pages\", row.id, { html, css, project });\n return res.ok ? { ok: true } : { ok: false, error: \"The page was not saved. Check your connection and try again.\" };\n }}\n />\n );\n}\n\n// the public page, on the server:\nexport function LandingPage({ row }) {\n return <PageView page={row} label={row.title} />;\n}\n```\n\n## What it will not do\n\n- No live multiplayer. Two people editing the same page is last save wins.\n- No free colour picker, hex field or arbitrary CSS. The Design panel\n offers the theme's choices only, by design. A block's own `css` is where\n a fixed design lives.\n- Styles apply at every width. The canvas's device toggle previews the\n page's own media queries. It does not keep per-device overrides.\n- No forms that submit, and no scripts. The HTML is cleaned of them. A\n signup form belongs to the app (a block beside `PageView`), not to the\n page.\n- No asset library. Pictures are an address, or an upload through\n `onUploadImage`.\n- No publishing, domains or SEO fields. Those belong to `landing-builder`\n and the app.\n\n## Credits\n\nGrapesJS © Artur Arseniev, BSD-3-Clause. @grapesjs/react © Artur\nArseniev, MIT. Both are used unmodified, as npm dependencies. The block\nships none of their code. GrapesJS's stylesheet is imported from the\npackage and repainted with the workspace's tokens.\n"
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
"categories": [
|
|
83
|
+
"page builder",
|
|
84
|
+
"website builder",
|
|
85
|
+
"landing page",
|
|
86
|
+
"grapesjs",
|
|
87
|
+
"visual editor",
|
|
88
|
+
"drag and drop",
|
|
89
|
+
"sections",
|
|
90
|
+
"templates",
|
|
91
|
+
"wysiwyg",
|
|
92
|
+
"html",
|
|
93
|
+
"css",
|
|
94
|
+
"no-code",
|
|
95
|
+
"web page",
|
|
96
|
+
"layers"
|
|
97
|
+
],
|
|
98
|
+
"docs": "Installed to components/makersclaw/blocks/page-builder. Import: import { PageBuilder, usePageBuilder, readPage, DEVICES, PageView, standaloneDocument, BlockThumb, PageThumb, THUMB_NAMES, BASE_CSS, SECTION_BLOCKS, BASIC_BLOCKS, STARTER_TEMPLATES, STYLE_GROUPS, COLOR_TOKENS, groupsFor, currentOption, patchFor, tokenCss, fontFaceCss, fontLinks, PAGE_BASE_CSS, emptyPage, isPageEmpty, cleanHtml, cleanCss, normalizeBlock, templatePage, pageDocument, projectSections } from \"@/components/makersclaw/blocks/page-builder\". Props and an example: components/makersclaw/blocks/page-builder/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
99
|
+
"meta": {
|
|
100
|
+
"makersclaw": {
|
|
101
|
+
"version": "0.3.0",
|
|
102
|
+
"useWhen": "A screen where someone builds or edits a web page visually: a landing page, a waitlist or launch page, a simple site section, an announcement page. landing-builder is built on it (section templates, publishing). Not for rich text in a document (doc-editor), an email body (newsletter-builder), a form of fields (record-form) or a freeform drawing (sketch-canvas)."
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "payment-methods",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "PaymentMethods",
|
|
6
|
+
"description": "A customer's SAVED payment methods as cards: brand, last four digits, expiry (expired and expiring soon flagged) and a Default badge; Make default, Remove behind a Confirm, and Add, which hands off to the payment provider (Paddle). Never a card-number field. Empty, loading and error states built in.",
|
|
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/payment-methods/index.js",
|
|
14
|
+
"type": "registry:component",
|
|
15
|
+
"target": "@components/makersclaw/blocks/payment-methods/index.js",
|
|
16
|
+
"content": "// The PaymentMethods block — `makersclaw add payment-methods`. Fixed:\n// compose it through its props (README.md beside this file); a list it\n// cannot be is your own component in components/.\nexport { PaymentMethods } from \"./payment-methods\";\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "registry/blocks/payment-methods/payment-methods.jsx",
|
|
20
|
+
"type": "registry:component",
|
|
21
|
+
"target": "@components/makersclaw/blocks/payment-methods/payment-methods.jsx",
|
|
22
|
+
"content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { Badge, Button, Confirm, Empty, Icon, Notice, Skeleton, Tooltip, cx, todayString } from \"../../ui\";\n\n// The payment methods a customer has saved, as cards: the brand, the last\n// four digits, the expiry and which one is the default. Make default and\n// Remove (behind a Confirm) go out through callbacks that return { ok }.\n// Adding one is the app's: `onAdd` opens the payment provider (Paddle),\n// which collects the card. The block never holds a card number.\n\n// Server and browser may differ in locale, time zone and even the day:\n// the first render uses fixed ones (en-US, UTC) so it matches the server's\n// HTML, then the reader's own take over.\nfunction useHydrated() {\n const [hydrated, setHydrated] = useState(false);\n useEffect(() => setHydrated(true), []);\n return hydrated;\n}\n\nconst BRANDS = {\n visa: { name: \"Visa\", icon: \"credit-card\" },\n mastercard: { name: \"Mastercard\", icon: \"credit-card\" },\n amex: { name: \"American Express\", icon: \"credit-card\" },\n discover: { name: \"Discover\", icon: \"credit-card\" },\n jcb: { name: \"JCB\", icon: \"credit-card\" },\n unionpay: { name: \"UnionPay\", icon: \"credit-card\" },\n maestro: { name: \"Maestro\", icon: \"credit-card\" },\n card: { name: \"Card\", icon: \"credit-card\" },\n paypal: { name: \"PayPal\", icon: \"credit-card\" },\n apple_pay: { name: \"Apple Pay\", icon: \"apple\" },\n google_pay: { name: \"Google Pay\", icon: \"google\" },\n bank: { name: \"Bank account\", icon: \"building\" },\n};\n\nfunction brandOf(method) {\n const key = String(method.brand ?? \"card\").toLowerCase().replace(/[\\s-]+/g, \"_\");\n const known = BRANDS[key];\n return known ?? { name: method.brand || \"Card\", icon: \"credit-card\" };\n}\n\n// The name a method is called by in buttons and dialogs: \"Visa ending 4242\".\nfunction nameOf(method) {\n if (method.label) return method.label;\n const brand = brandOf(method).name;\n return method.last4 ? `${brand} ending ${method.last4}` : brand;\n}\n\n// \"expired\" · \"soon\" (this month or next) · \"ok\" · null (no expiry).\nfunction expiryState(method, today) {\n const m = Number(method.expMonth);\n let y = Number(method.expYear);\n if (!m || !y) return null;\n if (y < 100) y += 2000;\n const [ty, tm] = today.split(\"-\").map(Number);\n const months = (y - ty) * 12 + (m - tm);\n if (months < 0) return \"expired\";\n if (months <= 1) return \"soon\";\n return \"ok\";\n}\n\nconst expiryText = (method) => {\n const m = String(method.expMonth).padStart(2, \"0\");\n const y = String(method.expYear).slice(-2);\n return `${m}/${y}`;\n};\n\nasync function attempt(fn) {\n try {\n const res = await fn();\n return res && res.ok === false ? res.error || \"That did not save.\" : null;\n } catch (e) {\n return e?.message || \"That did not save.\";\n }\n}\n\nfunction MethodCard({ method, today, busy, error, onMakeDefault, onAskRemove, removable, lockedReason }) {\n const brand = brandOf(method);\n const expiry = expiryState(method, today);\n const name = nameOf(method);\n return (\n <li\n className={cx(\n \"relative grid gap-4 rounded-lg border bg-card p-4 text-card-foreground\",\n method.isDefault ? \"border-primary/60\" : \"border-border\",\n )}\n >\n <div className=\"flex items-start gap-3\">\n <span className=\"grid h-11 w-14 shrink-0 place-items-center rounded-md border border-border bg-muted text-foreground\">\n <Icon name={brand.icon} size={22} />\n </span>\n <div className=\"grid min-w-0 flex-1 gap-0.5\">\n <p className=\"truncate font-medium\">\n {method.last4 ? (\n <>\n <span aria-hidden=\"true\" className=\"tracking-widest text-muted-foreground\">\n ••••{\" \"}\n </span>\n <span className=\"sr-only\">{brand.name} ending </span>\n <span className=\"tabular-nums\">{method.last4}</span>\n </>\n ) : (\n method.label || brand.name\n )}\n </p>\n <p className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-muted-foreground\">\n <span>{method.last4 ? brand.name : method.holder || method.email || \"\"}</span>\n {method.expMonth && method.expYear ? (\n <span className={cx(expiry === \"expired\" && \"text-destructive\")}>\n {expiry === \"expired\" ? \"Expired\" : \"Expires\"} {expiryText(method)}\n </span>\n ) : null}\n </p>\n {method.last4 && method.holder ? <p className=\"truncate text-xs text-muted-foreground\">{method.holder}</p> : null}\n </div>\n <div className=\"flex shrink-0 flex-col items-end gap-1\">\n {method.isDefault ? (\n <Badge tone=\"primary\" variant=\"soft\">\n Default\n </Badge>\n ) : null}\n {expiry === \"expired\" ? (\n <Badge tone=\"danger\" variant=\"soft\">\n Expired\n </Badge>\n ) : expiry === \"soon\" ? (\n <Badge tone=\"warning\" variant=\"soft\">\n Expires soon\n </Badge>\n ) : null}\n </div>\n </div>\n\n {onMakeDefault || onAskRemove ? (\n <div className=\"flex flex-wrap items-center gap-2 border-t border-border pt-3\">\n {onMakeDefault && !method.isDefault ? (\n <Button\n size=\"sm\"\n variant=\"quiet\"\n icon=\"check\"\n onClick={() => onMakeDefault(method)}\n loading={busy === \"default\"}\n disabled={Boolean(busy) || expiry === \"expired\"}\n aria-label={`Make ${name} the default`}\n >\n Make default\n </Button>\n ) : null}\n {onAskRemove ? (\n <Tooltip label={removable ? \"\" : lockedReason} className=\"ml-auto\">\n <Button\n size=\"sm\"\n variant=\"danger\"\n icon=\"trash\"\n onClick={() => onAskRemove(method)}\n disabled={Boolean(busy) || !removable}\n aria-label={`Remove ${name}`}\n className=\"ml-auto\"\n >\n Remove\n </Button>\n </Tooltip>\n ) : null}\n </div>\n ) : null}\n\n {error ? (\n <p role=\"alert\" className=\"text-xs text-destructive\">\n {error}\n </p>\n ) : null}\n </li>\n );\n}\n\nfunction Loading() {\n return (\n <ul aria-busy=\"true\" aria-label=\"Loading payment methods\" className=\"grid gap-3 md:grid-cols-2\">\n {[0, 1].map((i) => (\n <li key={i} className=\"flex gap-3 rounded-lg border border-border p-4\">\n <Skeleton shape=\"rect\" className=\"h-11 w-14\" />\n <div className=\"grid flex-1 content-start gap-2\">\n <Skeleton className=\"w-1/2\" />\n <Skeleton className=\"w-1/3\" />\n </div>\n </li>\n ))}\n </ul>\n );\n}\n\nexport function PaymentMethods({\n methods,\n onAdd,\n onSetDefault,\n onRemove,\n loading = false,\n error,\n title = \"Payment methods\",\n description = \"The cards and wallets saved to your account. The default is charged first.\",\n addLabel = \"Add payment method\",\n emptyTitle = \"No saved payment methods\",\n emptyBody = \"Add one and it is ready at checkout.\",\n today,\n className,\n}) {\n const [busy, setBusy] = useState({});\n const [errors, setErrors] = useState({});\n const [removing, setRemoving] = useState(null);\n const [removeBusy, setRemoveBusy] = useState(false);\n const [adding, setAdding] = useState(false);\n const [addError, setAddError] = useState(null);\n const hydrated = useHydrated();\n const now = today ?? (hydrated ? todayString() : new Date().toISOString().slice(0, 10));\n const list = (methods ?? []).filter(Boolean);\n const onlyOne = list.length <= 1;\n\n const mark = (id, state) => setBusy((b) => ({ ...b, [id]: state }));\n const fail = (id, message) => setErrors((e) => ({ ...e, [id]: message }));\n\n async function makeDefault(method) {\n mark(method.id, \"default\");\n fail(method.id, null);\n fail(method.id, await attempt(() => onSetDefault(method)));\n mark(method.id, null);\n }\n\n async function confirmRemove() {\n const method = removing;\n if (!method) return;\n setRemoveBusy(true);\n fail(method.id, null);\n const failed = await attempt(() => onRemove(method));\n setRemoveBusy(false);\n setRemoving(null);\n if (failed) fail(method.id, failed);\n }\n\n async function add() {\n setAdding(true);\n setAddError(null);\n setAddError(await attempt(() => onAdd()));\n setAdding(false);\n }\n\n const addButton = onAdd ? (\n <Button variant=\"primary\" icon=\"plus\" onClick={add} loading={adding}>\n {addLabel}\n </Button>\n ) : null;\n\n let body;\n if (loading) body = <Loading />;\n else if (!list.length) body = <Empty icon=\"credit-card\" title={emptyTitle} body={emptyBody} action={addButton} />;\n else\n body = (\n <ul aria-label={title} className=\"grid gap-3 md:grid-cols-2\">\n {list.map((method) => (\n <MethodCard\n key={method.id}\n method={method}\n today={now}\n busy={busy[method.id]}\n error={errors[method.id]}\n onMakeDefault={onSetDefault ? makeDefault : null}\n onAskRemove={onRemove ? setRemoving : null}\n removable={!method.isDefault || onlyOne}\n lockedReason=\"Make another method the default before removing this one\"\n />\n ))}\n </ul>\n );\n\n return (\n <section className={cx(\"grid gap-5\", className)}>\n <header className=\"flex flex-wrap items-start justify-between gap-3\">\n <div className=\"min-w-0 max-w-prose\">\n <h2 className=\"text-xl font-semibold\">{title}</h2>\n {description ? <p className=\"mt-1 text-sm text-muted-foreground\">{description}</p> : null}\n </div>\n {!loading && list.length ? <div className=\"w-full sm:w-auto [&>button]:w-full\">{addButton}</div> : null}\n </header>\n {error ? <Notice tone=\"danger\">{error}</Notice> : null}\n {addError ? (\n <Notice tone=\"danger\" onDismiss={() => setAddError(null)}>\n {addError}\n </Notice>\n ) : null}\n {body}\n <Confirm\n open={Boolean(removing)}\n onClose={() => (removeBusy ? null : setRemoving(null))}\n onConfirm={confirmRemove}\n tone=\"danger\"\n busy={removeBusy}\n title={removing ? `Remove ${nameOf(removing)}?` : \"Remove?\"}\n body=\"It will no longer be offered at checkout. You can add it again later.\"\n confirmLabel=\"Remove\"\n />\n </section>\n );\n}\n"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "registry/blocks/payment-methods/README.md",
|
|
26
|
+
"type": "registry:file",
|
|
27
|
+
"target": "@components/makersclaw/blocks/payment-methods/README.md",
|
|
28
|
+
"content": "# PaymentMethods\n\nThe payment methods a customer has saved, as a grid of cards. Each card\nshows the brand, the last four digits, the expiry and, on one of them, a\nDefault badge. An expired card is marked in the danger tone, one that\nexpires this month or next in the warning tone.\n\nThe actions are Make default, Remove (behind a Confirm) and Add. Add is\nthe app's: it opens the payment provider (Paddle), which collects the\ncard. **The block never shows a card-number field.** Every action goes out\nthrough a callback that returns `{ ok }`, and a failed one is shown on the\ncard it was for.\n\n```jsx\nimport { PaymentMethods } from \"../components/blocks/payment-methods\"; // a Makersclaw app (\"./blocks/payment-methods\" from components/)\nimport { PaymentMethods } from \"@/components/makersclaw/blocks/payment-methods\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component, and\nload the methods in the server surface (README §4).\n\n## The data\n\nOne saved method, as the provider reports it:\n\n```js\n{ id: \"pm_1\", brand: \"visa\", last4: \"4242\", expMonth: 8, expYear: 2028, isDefault: true, holder: \"Maya Chen\" }\n```\n\n- `brand` — `visa`, `mastercard`, `amex`, `discover`, `jcb`, `unionpay`,\n `maestro`, `paypal`, `apple_pay`, `google_pay`, `bank` or `card`. Any\n other word is shown as written. Apple Pay and Google Pay get their\n marks; every card brand gets the card glyph and its name in words (the\n kit draws no card-brand logos).\n- `last4` — the last four digits. Nothing more of the number ever comes in.\n- `expMonth` (1–12) · `expYear` (2028 or 28) — optional; a wallet has none.\n- `isDefault` — true on one method.\n- `holder` — the name on it; `label` — call it something else (\"Company\n Amex\"); `email` — for a wallet with no last four (PayPal).\n\n## Props\n\n- `methods` — the saved methods. An empty list shows the empty state.\n- `onAdd()` — the Add button (in the header, and in the empty state).\n Open Paddle's update-payment-method checkout here; return\n `{ ok: false, error }` if it could not open. Without it there is no Add.\n- `onSetDefault(method)` — Make default on every other card; return\n `{ ok: true }` or `{ ok: false, error }`. An expired card cannot be made\n the default. Without it, no button.\n- `onRemove(method)` — Remove, asked through a Confirm; same return. The\n default cannot be removed while there are others: make another the\n default first (the button says why on hover). Without it, no button.\n- `loading` — skeleton cards. `error` — a line above the list (the\n methods did not load).\n- `title` · `description` · `addLabel` · `emptyTitle` · `emptyBody` — words.\n- `today` — `\"YYYY-MM-DD\"`, the day expiry is judged against; default\n today.\n- `className` — layout around it (width, margin). Never colour.\n\n## Example\n\n```jsx\n// components/Billing.jsx — client\n\"use client\";\nimport { PaymentMethods } from \"./blocks/payment-methods\";\n\nexport default function Billing({ methods, error }) {\n const post = (path, body) =>\n fetch(path, { method: \"POST\", body: JSON.stringify(body) }).then((r) =>\n r.ok ? { ok: true } : { ok: false, error: \"That did not save. Try again.\" },\n );\n return (\n <PaymentMethods\n methods={methods}\n error={error}\n onAdd={async () => {\n const res = await fetch(\"/api/billing/update-method\", { method: \"POST\" });\n if (!res.ok) return { ok: false, error: \"Could not open the payment form.\" };\n const { transactionId } = await res.json();\n window.Paddle?.Checkout.open({ transactionId });\n return { ok: true };\n }}\n onSetDefault={(m) => post(\"/api/billing/default-method\", { id: m.id })}\n onRemove={(m) => post(\"/api/billing/remove-method\", { id: m.id })}\n />\n );\n}\n```\n\n## What it will not do\n\n- Take or show a full card number, a CVC or an expiry field. Adding and\n editing a card is the provider's form, opened from `onAdd`.\n- Draw card-brand logos: brands are named in words beside a card glyph.\n- Edit a saved card in place: providers replace a card; add the new one,\n make it the default, remove the old.\n\nA list it cannot be is your own component in `components/`, built from\nthe kit — not an edit to this block. Its files are fixed: `npm run check`\nrefuses a change, and `makersclaw add payment-methods` restores them.\n\n## Credits\n\nShape and behaviour from Creative Tim UI (https://github.com/creativetimofficial/ui), MIT; this is a rewrite on the Makersclaw kit.\n"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"categories": [
|
|
32
|
+
"payment",
|
|
33
|
+
"payment methods",
|
|
34
|
+
"cards",
|
|
35
|
+
"credit card",
|
|
36
|
+
"wallet",
|
|
37
|
+
"billing",
|
|
38
|
+
"default card",
|
|
39
|
+
"saved cards",
|
|
40
|
+
"paddle",
|
|
41
|
+
"account",
|
|
42
|
+
"commerce"
|
|
43
|
+
],
|
|
44
|
+
"docs": "Installed to components/makersclaw/blocks/payment-methods. Import: import { PaymentMethods } from \"@/components/makersclaw/blocks/payment-methods\". Props and an example: components/makersclaw/blocks/payment-methods/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
45
|
+
"meta": {
|
|
46
|
+
"makersclaw": {
|
|
47
|
+
"version": "0.3.0",
|
|
48
|
+
"useWhen": "A billing or account page where a customer manages the cards and wallets on file. Not for taking a payment (cart's checkout step hands off to Paddle) or for a list of charges (transaction-list)."
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|