makersclaw 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -4
- package/package.json +1 -1
- package/r/activity-log.json +52 -0
- package/r/agent-changes.json +51 -0
- package/r/agent-chat.json +53 -0
- package/r/agent-column.json +85 -0
- package/r/agent-dock.json +57 -0
- package/r/agent-kit.json +49 -0
- package/r/agent-marks.json +49 -0
- package/r/agent-pane.json +93 -0
- package/r/ai-action.json +56 -0
- package/r/annotate.json +4 -4
- package/r/approval-request.json +2 -2
- package/r/approval.json +54 -0
- package/r/ask-bar.json +50 -0
- package/r/audio-player.json +2 -2
- package/r/auth-form.json +2 -2
- package/r/automation-editor.json +66 -0
- package/r/automation-list.json +57 -0
- package/r/automation-model.json +118 -0
- package/r/automations-screen.json +71 -0
- package/r/board.json +2 -2
- package/r/calendar-view.json +3 -3
- package/r/cart.json +2 -2
- package/r/chart.json +5 -5
- package/r/chat-pane.json +84 -0
- package/r/chat.json +3 -3
- package/r/code-editor.json +2 -2
- package/r/comment-box.json +3 -3
- package/r/connect-card.json +51 -0
- package/r/contact-section.json +2 -2
- package/r/core.json +18 -6
- package/r/cost.json +51 -0
- package/r/csv-import.json +3 -3
- package/r/data-picker.json +60 -0
- package/r/data-table.json +2 -2
- package/r/diagram.json +2 -2
- package/r/doc-editor.json +2 -2
- package/r/doc-page.json +3 -3
- package/r/email-render.json +2 -2
- package/r/faq.json +2 -2
- package/r/file-upload.json +2 -2
- package/r/first-run.json +63 -0
- package/r/fix-note.json +57 -0
- package/r/flow-canvas.json +3 -3
- package/r/flow-editor.json +79 -0
- package/r/footer.json +3 -3
- package/r/gantt.json +3 -3
- package/r/job-panel.json +2 -2
- package/r/json-editor.json +3 -3
- package/r/library-shelf.json +56 -0
- package/r/map.json +2 -2
- package/r/margin-notes.json +67 -0
- package/r/menubar.json +2 -2
- package/r/mind-map.json +4 -4
- package/r/navigation-menu.json +2 -2
- package/r/needs-you.json +55 -0
- package/r/order-detail.json +2 -2
- package/r/org-chart.json +4 -4
- package/r/pace-bar.json +69 -0
- package/r/page-builder.json +3 -3
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +3 -3
- package/r/product-detail.json +2 -2
- package/r/product-grid.json +3 -3
- package/r/record-form.json +2 -2
- package/r/record-grid.json +52 -0
- package/r/record-notes.json +4 -4
- package/r/registry.json +4089 -1510
- package/r/run-history.json +52 -0
- package/r/run-view.json +80 -0
- package/r/schedule-card.json +54 -0
- package/r/schedule-engine.json +79 -0
- package/r/schedule-gantt.json +83 -0
- package/r/schedule-ruler.json +64 -0
- package/r/search-dialog.json +2 -2
- package/r/settings-list.json +2 -2
- package/r/setup-checklist.json +2 -2
- package/r/share-dialog.json +2 -2
- package/r/sidebar.json +3 -3
- package/r/signature-pad.json +2 -2
- package/r/sketch-canvas.json +3 -3
- package/r/source-receipt.json +52 -0
- package/r/spreadsheet.json +3 -3
- package/r/stat-cards.json +3 -3
- package/r/step-picker.json +53 -0
- package/r/step-settings.json +72 -0
- package/r/suggestion.json +58 -0
- package/r/sum-bar.json +64 -0
- package/r/task-list.json +3 -3
- package/r/task-model.json +2 -2
- package/r/testimonials.json +2 -2
- package/r/tier-map.json +67 -0
- package/r/time-grid.json +73 -0
- package/r/time-strip.json +64 -0
- package/r/top-bar.json +66 -0
- package/r/transaction-list.json +2 -2
- package/r/trigger-picker.json +61 -0
- package/r/whiteboard.json +4 -4
- package/r/workflow-view.json +55 -0
- package/registry/blocks/activity-log/README.md +112 -0
- package/registry/blocks/activity-log/activity-log.jsx +351 -0
- package/registry/blocks/activity-log/block.json +33 -0
- package/registry/blocks/activity-log/demo.jsx +48 -0
- package/registry/blocks/activity-log/index.js +4 -0
- package/registry/blocks/agent-changes/README.md +94 -0
- package/registry/blocks/agent-changes/agent-changes.jsx +118 -0
- package/registry/blocks/agent-changes/block.json +28 -0
- package/registry/blocks/agent-changes/demo.jsx +39 -0
- package/registry/blocks/agent-changes/index.js +3 -0
- package/registry/blocks/agent-chat/README.md +145 -0
- package/registry/blocks/agent-chat/agent-chat.jsx +415 -0
- package/registry/blocks/agent-chat/block.json +30 -0
- package/registry/blocks/agent-chat/demo.jsx +165 -0
- package/registry/blocks/agent-chat/index.js +3 -0
- package/registry/blocks/agent-column/README.md +197 -0
- package/registry/blocks/agent-column/agent-column.jsx +492 -0
- package/registry/blocks/agent-column/block.json +67 -0
- package/registry/blocks/agent-column/column-picker.jsx +163 -0
- package/registry/blocks/agent-column/demo.jsx +50 -0
- package/registry/blocks/agent-column/index.js +7 -0
- package/registry/blocks/agent-column/model.js +164 -0
- package/registry/blocks/agent-column/receipt.jsx +483 -0
- package/registry/blocks/agent-column/spreadsheet-type.jsx +24 -0
- package/registry/blocks/agent-dock/README.md +210 -0
- package/registry/blocks/agent-dock/agent-dock.jsx +356 -0
- package/registry/blocks/agent-dock/block.json +47 -0
- package/registry/blocks/agent-dock/demo.jsx +153 -0
- package/registry/blocks/agent-dock/dock-model.js +118 -0
- package/registry/blocks/agent-dock/index.js +5 -0
- package/registry/blocks/agent-kit/README.md +73 -0
- package/registry/blocks/agent-kit/agent-kit.jsx +86 -0
- package/registry/blocks/agent-kit/block.json +10 -0
- package/registry/blocks/agent-kit/demo.jsx +35 -0
- package/registry/blocks/agent-kit/index.js +3 -0
- package/registry/blocks/agent-marks/README.md +80 -0
- package/registry/blocks/agent-marks/agent-marks.jsx +133 -0
- package/registry/blocks/agent-marks/block.json +27 -0
- package/registry/blocks/agent-marks/demo.jsx +49 -0
- package/registry/blocks/agent-marks/index.js +3 -0
- package/registry/blocks/agent-pane/README.md +225 -0
- package/registry/blocks/agent-pane/agent-pane.jsx +244 -0
- package/registry/blocks/agent-pane/block.json +41 -0
- package/registry/blocks/agent-pane/composer.jsx +6 -0
- package/registry/blocks/agent-pane/demo.jsx +233 -0
- package/registry/blocks/agent-pane/dock-context.js +10 -0
- package/registry/blocks/agent-pane/index.js +4 -0
- package/registry/blocks/agent-pane/prose.jsx +84 -0
- package/registry/blocks/agent-pane/questions.jsx +134 -0
- package/registry/blocks/agent-pane/transcript.jsx +268 -0
- package/registry/blocks/agent-pane/work.jsx +368 -0
- package/registry/blocks/ai-action/README.md +94 -0
- package/registry/blocks/ai-action/ai-action.jsx +188 -0
- package/registry/blocks/ai-action/block.json +34 -0
- package/registry/blocks/ai-action/demo.jsx +93 -0
- package/registry/blocks/ai-action/index.js +3 -0
- package/registry/blocks/annotate/README.md +14 -11
- package/registry/blocks/approval/README.md +145 -0
- package/registry/blocks/approval/approval.jsx +163 -0
- package/registry/blocks/approval/block.json +31 -0
- package/registry/blocks/approval/demo.jsx +121 -0
- package/registry/blocks/approval/index.js +3 -0
- package/registry/blocks/ask-bar/README.md +105 -0
- package/registry/blocks/ask-bar/ask-bar.jsx +166 -0
- package/registry/blocks/ask-bar/block.json +26 -0
- package/registry/blocks/ask-bar/demo.jsx +78 -0
- package/registry/blocks/ask-bar/index.js +3 -0
- package/registry/blocks/automation-editor/README.md +291 -0
- package/registry/blocks/automation-editor/automation-editor.jsx +656 -0
- package/registry/blocks/automation-editor/block.json +39 -0
- package/registry/blocks/automation-editor/demo.jsx +284 -0
- package/registry/blocks/automation-editor/index.js +4 -0
- package/registry/blocks/automation-editor/run-inputs.jsx +82 -0
- package/registry/blocks/automation-list/README.md +129 -0
- package/registry/blocks/automation-list/automation-list.jsx +476 -0
- package/registry/blocks/automation-list/block.json +35 -0
- package/registry/blocks/automation-list/demo.jsx +67 -0
- package/registry/blocks/automation-list/index.js +3 -0
- package/registry/blocks/automation-model/README.md +109 -0
- package/registry/blocks/automation-model/block.json +135 -0
- package/registry/blocks/automation-model/catalog.js +256 -0
- package/registry/blocks/automation-model/check.js +161 -0
- package/registry/blocks/automation-model/demo.jsx +42 -0
- package/registry/blocks/automation-model/edit.js +262 -0
- package/registry/blocks/automation-model/fixes.js +287 -0
- package/registry/blocks/automation-model/glyph.jsx +13 -0
- package/registry/blocks/automation-model/graph.js +410 -0
- package/registry/blocks/automation-model/index.js +33 -0
- package/registry/blocks/automation-model/overlay.js +175 -0
- package/registry/blocks/automation-model/parts.jsx +94 -0
- package/registry/blocks/automation-model/schedule.js +53 -0
- package/registry/blocks/automation-model/templates.js +128 -0
- package/registry/blocks/automation-model/values.js +29 -0
- package/registry/blocks/automation-model/words.js +439 -0
- package/registry/blocks/automations-screen/README.md +207 -0
- package/registry/blocks/automations-screen/automations-screen.jsx +452 -0
- package/registry/blocks/automations-screen/block.json +47 -0
- package/registry/blocks/automations-screen/demo.jsx +364 -0
- package/registry/blocks/automations-screen/index.js +4 -0
- package/registry/blocks/automations-screen/screen-model.js +129 -0
- package/registry/blocks/chart/cartesian.jsx +1 -1
- package/registry/blocks/chart/core.jsx +33 -23
- package/registry/blocks/chart/pie.jsx +1 -1
- package/registry/blocks/chat/README.md +4 -1
- package/registry/blocks/chat/demo.jsx +2 -2
- package/registry/blocks/chat-pane/README.md +143 -0
- package/registry/blocks/chat-pane/block.json +48 -0
- package/registry/blocks/chat-pane/chat-attach.js +166 -0
- package/registry/blocks/chat-pane/chat-composer.jsx +195 -0
- package/registry/blocks/chat-pane/chat-context.js +17 -0
- package/registry/blocks/chat-pane/chat-model.js +43 -0
- package/registry/blocks/chat-pane/chat-pane.jsx +398 -0
- package/registry/blocks/chat-pane/chats-list.jsx +52 -0
- package/registry/blocks/chat-pane/demo.jsx +103 -0
- package/registry/blocks/chat-pane/index.js +9 -0
- package/registry/blocks/connect-card/README.md +99 -0
- package/registry/blocks/connect-card/block.json +10 -0
- package/registry/blocks/connect-card/connect-card.jsx +103 -0
- package/registry/blocks/connect-card/demo.jsx +49 -0
- package/registry/blocks/connect-card/index.js +3 -0
- package/registry/blocks/cost/README.md +115 -0
- package/registry/blocks/cost/block.json +10 -0
- package/registry/blocks/cost/cost.jsx +176 -0
- package/registry/blocks/cost/demo.jsx +66 -0
- package/registry/blocks/cost/index.js +3 -0
- package/registry/blocks/data-picker/README.md +81 -0
- package/registry/blocks/data-picker/block.json +38 -0
- package/registry/blocks/data-picker/data-picker.jsx +210 -0
- package/registry/blocks/data-picker/demo.jsx +34 -0
- package/registry/blocks/data-picker/fields.js +89 -0
- package/registry/blocks/data-picker/index.js +4 -0
- package/registry/blocks/data-table/demo.jsx +1 -1
- package/registry/blocks/first-run/README.md +118 -0
- package/registry/blocks/first-run/block.json +11 -0
- package/registry/blocks/first-run/demo.jsx +88 -0
- package/registry/blocks/first-run/first-run-model.js +51 -0
- package/registry/blocks/first-run/first-run.jsx +156 -0
- package/registry/blocks/first-run/index.js +5 -0
- package/registry/blocks/first-run/probe-rules.js +26 -0
- package/registry/blocks/fix-note/README.md +79 -0
- package/registry/blocks/fix-note/block.json +35 -0
- package/registry/blocks/fix-note/demo.jsx +30 -0
- package/registry/blocks/fix-note/fix-note.jsx +235 -0
- package/registry/blocks/fix-note/index.js +3 -0
- package/registry/blocks/flow-canvas/README.md +15 -8
- package/registry/blocks/flow-editor/README.md +108 -0
- package/registry/blocks/flow-editor/block.json +44 -0
- package/registry/blocks/flow-editor/demo.jsx +115 -0
- package/registry/blocks/flow-editor/flow-canvas.jsx +568 -0
- package/registry/blocks/flow-editor/flow-editor.jsx +154 -0
- package/registry/blocks/flow-editor/index.js +6 -0
- package/registry/blocks/flow-editor/minimap.jsx +78 -0
- package/registry/blocks/flow-editor/outline.jsx +134 -0
- package/registry/blocks/footer/README.md +3 -2
- package/registry/blocks/library-shelf/README.md +117 -0
- package/registry/blocks/library-shelf/block.json +28 -0
- package/registry/blocks/library-shelf/demo.jsx +68 -0
- package/registry/blocks/library-shelf/index.js +3 -0
- package/registry/blocks/library-shelf/library-shelf.jsx +211 -0
- package/registry/blocks/library-shelf/probe-rules.js +15 -0
- package/registry/blocks/margin-notes/README.md +222 -0
- package/registry/blocks/margin-notes/block.json +10 -0
- package/registry/blocks/margin-notes/demo.jsx +144 -0
- package/registry/blocks/margin-notes/index.js +20 -0
- package/registry/blocks/margin-notes/margin-model.js +224 -0
- package/registry/blocks/margin-notes/margin-notes.jsx +465 -0
- package/registry/blocks/mind-map/README.md +4 -2
- package/registry/blocks/needs-you/README.md +177 -0
- package/registry/blocks/needs-you/block.json +34 -0
- package/registry/blocks/needs-you/demo.jsx +143 -0
- package/registry/blocks/needs-you/index.js +3 -0
- package/registry/blocks/needs-you/needs-you.jsx +441 -0
- package/registry/blocks/org-chart/README.md +4 -2
- package/registry/blocks/pace-bar/README.md +230 -0
- package/registry/blocks/pace-bar/block.json +10 -0
- package/registry/blocks/pace-bar/demo.jsx +255 -0
- package/registry/blocks/pace-bar/index.js +22 -0
- package/registry/blocks/pace-bar/pace-bar.jsx +461 -0
- package/registry/blocks/pace-bar/pace-model.js +288 -0
- package/registry/blocks/page-builder/README.md +1 -1
- package/registry/blocks/page-builder/demo.jsx +3 -3
- package/registry/blocks/pdf-viewer/README.md +4 -1
- package/registry/blocks/record-grid/README.md +100 -0
- package/registry/blocks/record-grid/block.json +28 -0
- package/registry/blocks/record-grid/demo.jsx +43 -0
- package/registry/blocks/record-grid/index.js +3 -0
- package/registry/blocks/record-grid/record-grid.jsx +251 -0
- package/registry/blocks/run-history/README.md +138 -0
- package/registry/blocks/run-history/block.json +10 -0
- package/registry/blocks/run-history/demo.jsx +74 -0
- package/registry/blocks/run-history/index.js +3 -0
- package/registry/blocks/run-history/run-history.jsx +218 -0
- package/registry/blocks/run-view/README.md +174 -0
- package/registry/blocks/run-view/block.json +50 -0
- package/registry/blocks/run-view/demo.jsx +286 -0
- package/registry/blocks/run-view/index.js +5 -0
- package/registry/blocks/run-view/run-controls.jsx +266 -0
- package/registry/blocks/run-view/run-view.jsx +236 -0
- package/registry/blocks/run-view/runs-list.jsx +99 -0
- package/registry/blocks/schedule-card/README.md +121 -0
- package/registry/blocks/schedule-card/block.json +10 -0
- package/registry/blocks/schedule-card/demo.jsx +82 -0
- package/registry/blocks/schedule-card/index.js +3 -0
- package/registry/blocks/schedule-card/schedule-card.jsx +136 -0
- package/registry/blocks/schedule-engine/README.md +137 -0
- package/registry/blocks/schedule-engine/block.json +10 -0
- package/registry/blocks/schedule-engine/demo.jsx +82 -0
- package/registry/blocks/schedule-engine/engine.js +769 -0
- package/registry/blocks/schedule-engine/index.js +9 -0
- package/registry/blocks/schedule-engine/ops.js +71 -0
- package/registry/blocks/schedule-engine/place.js +294 -0
- package/registry/blocks/schedule-engine/tree.js +79 -0
- package/registry/blocks/schedule-engine/waits.js +80 -0
- package/registry/blocks/schedule-gantt/README.md +159 -0
- package/registry/blocks/schedule-gantt/block.json +10 -0
- package/registry/blocks/schedule-gantt/demo.jsx +223 -0
- package/registry/blocks/schedule-gantt/gantt-model.js +219 -0
- package/registry/blocks/schedule-gantt/geometry.js +176 -0
- package/registry/blocks/schedule-gantt/index.js +26 -0
- package/registry/blocks/schedule-gantt/schedule-gantt.jsx +882 -0
- package/registry/blocks/schedule-gantt/sheet.jsx +372 -0
- package/registry/blocks/schedule-gantt/track.jsx +190 -0
- package/registry/blocks/schedule-ruler/README.md +148 -0
- package/registry/blocks/schedule-ruler/block.json +10 -0
- package/registry/blocks/schedule-ruler/demo.jsx +181 -0
- package/registry/blocks/schedule-ruler/index.js +21 -0
- package/registry/blocks/schedule-ruler/ruler-model.js +239 -0
- package/registry/blocks/schedule-ruler/schedule-ruler.jsx +548 -0
- package/registry/blocks/sidebar/README.md +23 -13
- package/registry/blocks/sketch-canvas/README.md +15 -9
- package/registry/blocks/source-receipt/README.md +97 -0
- package/registry/blocks/source-receipt/block.json +30 -0
- package/registry/blocks/source-receipt/demo.jsx +71 -0
- package/registry/blocks/source-receipt/index.js +3 -0
- package/registry/blocks/source-receipt/source-receipt.jsx +107 -0
- package/registry/blocks/spreadsheet/spreadsheet.jsx +15 -1
- package/registry/blocks/step-picker/README.md +78 -0
- package/registry/blocks/step-picker/block.json +31 -0
- package/registry/blocks/step-picker/demo.jsx +23 -0
- package/registry/blocks/step-picker/index.js +3 -0
- package/registry/blocks/step-picker/step-picker.jsx +144 -0
- package/registry/blocks/step-settings/README.md +100 -0
- package/registry/blocks/step-settings/block.json +44 -0
- package/registry/blocks/step-settings/demo.jsx +93 -0
- package/registry/blocks/step-settings/fields.jsx +58 -0
- package/registry/blocks/step-settings/flow-settings.jsx +115 -0
- package/registry/blocks/step-settings/index.js +5 -0
- package/registry/blocks/step-settings/step-settings.jsx +290 -0
- package/registry/blocks/suggestion/README.md +171 -0
- package/registry/blocks/suggestion/block.json +40 -0
- package/registry/blocks/suggestion/demo.jsx +91 -0
- package/registry/blocks/suggestion/index.js +3 -0
- package/registry/blocks/suggestion/suggestion.jsx +436 -0
- package/registry/blocks/sum-bar/README.md +122 -0
- package/registry/blocks/sum-bar/block.json +10 -0
- package/registry/blocks/sum-bar/demo.jsx +110 -0
- package/registry/blocks/sum-bar/index.js +5 -0
- package/registry/blocks/sum-bar/sum-bar.jsx +209 -0
- package/registry/blocks/sum-bar/sum-model.js +126 -0
- package/registry/blocks/tier-map/README.md +217 -0
- package/registry/blocks/tier-map/block.json +10 -0
- package/registry/blocks/tier-map/demo.jsx +222 -0
- package/registry/blocks/tier-map/index.js +24 -0
- package/registry/blocks/tier-map/tier-map.jsx +574 -0
- package/registry/blocks/tier-map/tier-model.js +318 -0
- package/registry/blocks/time-grid/README.md +305 -0
- package/registry/blocks/time-grid/block.json +10 -0
- package/registry/blocks/time-grid/demo.jsx +279 -0
- package/registry/blocks/time-grid/grid-model.js +322 -0
- package/registry/blocks/time-grid/index.js +25 -0
- package/registry/blocks/time-grid/time-grid.jsx +1325 -0
- package/registry/blocks/time-strip/README.md +141 -0
- package/registry/blocks/time-strip/block.json +10 -0
- package/registry/blocks/time-strip/demo.jsx +129 -0
- package/registry/blocks/time-strip/index.js +19 -0
- package/registry/blocks/time-strip/strip-model.js +178 -0
- package/registry/blocks/time-strip/time-strip.jsx +260 -0
- package/registry/blocks/top-bar/README.md +149 -0
- package/registry/blocks/top-bar/block.json +35 -0
- package/registry/blocks/top-bar/demo.jsx +78 -0
- package/registry/blocks/top-bar/index.js +5 -0
- package/registry/blocks/top-bar/probe-rules.js +74 -0
- package/registry/blocks/top-bar/top-bar-model.js +152 -0
- package/registry/blocks/top-bar/top-bar.jsx +244 -0
- package/registry/blocks/trigger-picker/README.md +98 -0
- package/registry/blocks/trigger-picker/block.json +38 -0
- package/registry/blocks/trigger-picker/demo.jsx +59 -0
- package/registry/blocks/trigger-picker/index.js +3 -0
- package/registry/blocks/trigger-picker/trigger-picker.jsx +374 -0
- package/registry/blocks/whiteboard/README.md +20 -14
- package/registry/blocks/workflow-view/README.md +123 -0
- package/registry/blocks/workflow-view/block.json +10 -0
- package/registry/blocks/workflow-view/demo.jsx +90 -0
- package/registry/blocks/workflow-view/index.js +3 -0
- package/registry/blocks/workflow-view/workflow-view.jsx +203 -0
- package/src/cli.mjs +2 -2
- package/src/install.mjs +100 -22
- package/src/registry.mjs +37 -3
- package/templates/next/browser/lib/data.js +6 -1
- package/templates/next/files/README.md +909 -26
- package/templates/next/files/app/shell.jsx +54 -37
- package/templates/next/files/check/automation-format.ts +2103 -0
- package/templates/next/files/check/automation-package.ts +142 -0
- package/templates/next/files/check/run.mjs +1043 -12
- package/templates/next/files/components/ui/field.jsx +17 -4
- package/templates/next/files/components/ui/icon.jsx +13 -0
- package/templates/next/files/components/ui/index.js +3 -1
- package/templates/next/files/components/ui/layout.jsx +382 -0
- package/templates/next/files/components/ui/notice.jsx +70 -0
- package/templates/next/files/components/ui/page.jsx +124 -0
- package/templates/next/files/components/ui/stat.jsx +22 -7
- package/templates/next/files/components/ui/toast.jsx +6 -3
- package/templates/next/files/lib/automations.js +660 -0
- package/templates/next/files/lib/claw.js +244 -5
- package/templates/next/files/lib/door.js +92 -0
- package/templates/next/files/lib/hooks.js +1694 -6
- package/templates/next/files/lib/platform.js +611 -14
- package/templates/next/template.json +22 -3
package/r/flow-canvas.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"title": "FlowCanvas",
|
|
6
6
|
"description": "A Miro-like board (React Flow) in the workspace theme: sticky notes, shapes, text, frames, cards and images on an infinite dotted canvas; a tool rail with shortcuts, double-click for a sticky, drag-out connectors that re-route as cards move, labels on connectors, a floating toolbar over the selection, right-click menu, undo/redo, copy/paste, snap to grid, minimap, list view and presentation mode. The board is plain JSON; helpers turn it into text or markdown and lay it out. Other blocks add their own node types and rail tools.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json"
|
|
9
9
|
],
|
|
10
10
|
"dependencies": [
|
|
11
11
|
"@xyflow/react@12.11.6"
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"path": "registry/blocks/flow-canvas/README.md",
|
|
64
64
|
"type": "registry:file",
|
|
65
65
|
"target": "@components/makersclaw/blocks/flow-canvas/README.md",
|
|
66
|
-
"content": "# FlowCanvas\n\nA Miro-like board in the workspace theme. Sticky notes, shapes, text,\nframes, cards and images on an infinite dotted canvas. A tool rail on the\nleft (a bar along the bottom on a narrow canvas), double-click anywhere\nfor a sticky, drag from a card's dot to another card to connect them —\nor drop on empty canvas to pick what to add there. A floating toolbar\nover the selection, a right-click menu, undo/redo, copy/paste, snap to\ngrid, a minimap, a list view for keyboards and screen readers, and a\npresentation mode that steps through the frames.\n\nIt is [React Flow](https://reactflow.dev) (`@xyflow/react`, MIT)\nunderneath, with its colours mapped onto the workspace's tokens. The\ncanvas blocks — `account-map`, `process-map`, `journey-map`, `idea-board`\n— are built on this one: they import it, never `@xyflow/react`.\n\n```jsx\nimport { FlowCanvas, boardToText } from \"../components/blocks/flow-canvas\"; // a Makersclaw app (\"./blocks/flow-canvas\" from components/)\nimport { FlowCanvas, boardToText } from \"@/components/makersclaw/blocks/flow-canvas\"; // a shadcn project\n```\n\n`FlowCanvas` is a client component: render it from a `\"use client\"`\ncomponent. The helpers (`boardToText`, `autoLayout`, `createNode`…) are\nplain functions and work on the server too.\n\n`makersclaw add flow-canvas` adds `@xyflow/react` (pinned, exact) to\npackage.json: run `npm install` after. It loads with the page (no\n`import()`): the board mounts after the first render; until then — on the\nserver — the box holds the board as text for a screen reader.\n\n## The data\n\nA board is plain JSON. Store it as it comes out of `onChange` (one `jsonb`\ncolumn).\n\n```js\n{\n nodes: [\n { id: \"s1\", type: \"sticky\", position: { x: 40, y: 80 }, width: 200, height: 200,\n data: { text: \"Interview churned customers\", color: \"warning\", badge: 3, meta: \"Priya\" } },\n { id: \"f1\", type: \"frame\", position: { x: 0, y: 0 }, width: 720, height: 420, zIndex: -1,\n data: { title: \"Discover\", color: \"chart-2\" } },\n ],\n edges: [\n { id: \"e1\", source: \"s1\", target: \"s2\", label: \"feeds\",\n data: { line: \"curved\", arrow: \"end\", dashed: false } },\n ],\n viewport: { x: 0, y: 0, zoom: 1 }, // where the person last looked; optional\n}\n```\n\n- **Positions are absolute**, top-left corner, in board pixels. A card in a\n frame is not stored relative to it: a frame holds whatever lies inside\n its rectangle (`frameContents`, `frameOf`). Dragging a frame moves what\n is inside it.\n- **Colours are token names**, never colour values, so a board repaints\n with the theme: `\"warning\"` (the yellow sticky), `\"chart-1\"` … `\"chart-5\"`,\n `\"success\"`, `\"info\"`, `\"destructive\"`, `\"muted\"`. `COLORS` lists them;\n `STICKY_COLORS` is the sticky palette.\n- **Paint order is list order** (later is on top); frames always sit\n under everything (`zIndex: -1`).\n- `width`/`height` are set by resizing; `text` and `card` nodes leave\n `height` out and grow with their content.\n- A `sticky` or `shape` always has a box: one that arrives without a\n `width` or `height` gets its type's default for the missing side\n (sticky 200×200; a shape its outline's, e.g. pill 180×64). Their words\n are fitted to that box, so give a starter board's stickies the size you\n want them to have (`withFittedSize` is what the canvas applies).\n- A node may carry React Flow's own flags: `draggable: false`,\n `deletable: false`, `selectable: false`, `connectable: false`. Locking\n (the toolbar's lock) sets `data.locked: true` with `draggable` and\n `deletable` false.\n\nThe built-in node types and their `data`:\n\n| type | data |\n| --- | --- |\n| `sticky` | `text`, `color` (default `\"warning\"`), `badge?` (a count in the corner: votes), `meta?` (a small line at the foot: an author) |\n| `shape` | `text`, `shape`: `rect` · `ellipse` · `diamond` · `pill`, `color` (default `\"muted\"`), `textSize`: `sm` · `md` · `lg` · `xl` |\n| `text` | `text`, `textSize`, `align?`: `left` · `center` · `right` |\n| `frame` | `title`, `color?` (a faint wash and edge; none = the card surface) |\n| `card` | `title`, `body?`, `icon?` (a kit icon name), `badge?`, `color?` (a strip on top), `kind?`, anything else your `renderCard` reads |\n| `image` | `src`, `alt` |\n\nConnectors: `data.line` `curved` (default) · `straight` · `elbow`;\n`data.arrow` `end` (default) · `both` · `none`; `data.dashed`. They leave\nand enter by the sides of the two cards that face each other, so they\nre-route as cards move; handle ids are not stored.\n\n## Props\n\n- `value` — the board, controlled. A new `value` that differs from what\n the canvas last sent through `onChange` replaces it (loading another\n board, or a change made elsewhere — a vote count); that replacement can\n be undone. Pass back exactly what `onChange` gave you and nothing resets.\n- `defaultValue` — the starting board, uncontrolled. With neither, it\n starts empty (and shows the empty state).\n- `onChange(board)` — after each committed change (a drag ends, a card is\n placed, words stop being edited, a colour is picked), debounced by\n `changeDelay` (default 400 ms). The board is clean JSON (see above).\n Selection, typing in progress and panning never call it.\n- `onSave(board)` — `async`, return `{ ok: true }` or `{ ok: false, error }`\n (a throw counts as failed). Called after `onChange`, and on ⌘S. The\n canvas shows \"Saving…\", \"Saved\", or the error with a Retry beside\n undo/redo. Leave it out when `onChange` already saves.\n- `changeDelay` — ms, default 400.\n- `readOnly` — `false` (default). `true`: no rail, no editing, no\n connecting; pan, zoom, fit, the list view and `onOpenNode` still work.\n- `presentation` — `false` (default). `true`: read-only, and a bar steps\n through the frames in reading order (← → or the buttons; each fills the\n screen). `onExitPresentation()` — Esc and the bar's Exit call it; turn\n `presentation` off there.\n- `height` — the canvas's height, default `560` (px; any CSS length works:\n `\"70vh\"`, `\"100%\"` inside a sized box).\n- `bare` — `false` (default: rounded, bordered). `true`: no border or\n radius, for a full-page board.\n- `label` — the board's accessible name, default `\"Board\"`; also the list\n view's heading.\n- `className` — layout around it (margin, width). Never colour.\n- `palette` — the sticky colours offered, token names, default\n `STICKY_COLORS` (six). The toolbar adds a few more for shapes and frames.\n- `snap` — `true` (default): positions and sizes snap to `grid` (default\n `20` px — the dot spacing). The person can turn it off from the view menu.\n- `minimap` — `false` (default): whether the minimap starts open (a toggle\n in the zoom bar either way; hidden on a narrow canvas).\n- `hideAttribution` — `false` (default). React Flow asks that its small\n credit stays; its MIT licence allows hiding it.\n- `canvasRef` — a ref (`useFlowCanvas()`) that receives the handle (below).\n- `onSelectionChange(nodes, edges)` — the selection changed (a side panel\n for the selected person).\n- `onOpenNode(node)` — double-click or Enter on a node that has no words of\n its own to edit: your own node types, cards drawn by `renderCard`, and\n every node in read-only mode. Open your `Sheet` there. Without it,\n double-click edits the words.\n- `describeNode(node)` — a one-line description of one of YOUR node types,\n for the list view, `boardToText` and the server placeholder; return\n nothing for the default (`Sticky: \"…\"`).\n\nExtending it — how a block makes the canvas its own:\n\n- `nodeTypes` — `{ person: PersonNode, … }`: your own node components,\n added to the built-in ones (a key that matches a built-in replaces it).\n See \"Your own node types\".\n- `renderCard(node, ctx)` — draws the inside of every `card` node; return\n `undefined` for the default (title + body). `ctx`: `{ selected, editing,\n update(patch), api }`. Kit parts only (Avatar, Badge, Stat…); give an\n interactive part inside `className=\"nodrag\"` so pressing it does not drag\n the card. The simplest way to put your own card on the board.\n- `tools` — more rail tools: `[{ id, label, icon, shortcut?, create?,\n onSelect?, sizable?, connectable? }]`. See \"Rail tools\".\n- `rail` — the rail's contents, in order: built-in ids (`\"select\"`,\n `\"hand\"`, `\"sticky\"`, `\"shape\"`, `\"text\"`, `\"connector\"`, `\"frame\"`,\n `\"image\"`), your tools' ids, and `\"|\"` for a divider. Default: all the\n built-ins, then your tools (and `\"image\"` when `pickImage` is set). A\n process map drops the sticky: `rail={[\"select\", \"hand\", \"|\", \"shape\", \"connector\", \"step\"]}`.\n- `pickImage()` — `async () => ({ src, alt, width?, height? }) | null`: turns\n on the image tool (your upload, your picker). Without it there is no\n image tool; image nodes in the data still show.\n- `topBar` — a node shown floating at the top centre: a title, a save\n button, a share menu, a timer.\n- `toolbarExtra(selection, api)` — more controls at the end of the\n floating toolbar over a selection (`selection.nodes`): kit\n `IconButton size=\"sm\"` / `Menu` parts. \"Vote\", \"Assign owner\".\n- `contextMenuItems(selection, api)` — more right-click items, kit `Menu`\n shape (`[{ label, onSelect, icon?, shortcut?, tone? }]`); `selection.nodes`\n is empty for a right-click on the empty canvas.\n- `emptyState` — a node in place of the first-run hint; `false` for none.\n- `templates` — the starters on the empty state: `[{ id, label, icon?,\n description?, board }]` (`board` a board or a function returning one).\n Default `starterTemplates()` (Brainstorm, Flowchart, Retro); `[]` for none.\n- `constrain(node, { reason, board })` — runs on every node a change\n touched before it is committed (`reason`: `\"move\"` after a drag or a\n nudge, `\"create\"`, `\"paste\"`, `\"resize\"`, `\"align\"`, `\"layout\"`). Return\n the node adjusted (a new position, size or data), or nothing to keep it.\n This is how a journey map snaps a sticky into its cell and records it.\n- `beforeChange(next, prev, reason)` — runs on the whole board at every\n commit (`next` and `prev` are clean boards). Return `false` to refuse the\n change (the board goes back), a board to commit instead, or nothing to\n accept it.\n- `canConnect(connection, board)` — `false` refuses a connector\n (`connection`: `{ source, target }`). A card never connects to itself.\n\n## The handle\n\n`const board = useFlowCanvas()`, `<FlowCanvas canvasRef={board} />`, then\n`board.current?.…`. Tools' `create`/`onSelect`, `renderCard`,\n`toolbarExtra` and `contextMenuItems` get the same object as `api`, and a\nnode component reaches it with `useCanvas().api`. Every change made through\nit commits (undoable, then `onChange`) unless `{ commit: false }` says not.\n\n- Read: `getBoard()` (clean JSON, with the viewport) · `getSelection()` →\n `{ nodes, edges }` · `toText()` · `toMarkdown({ title })` · `canUndo()` ·\n `canRedo()` · `getTool()` · `readOnly`\n- Nodes: `addNodes(node | nodes, { select = true, edit = false, edges })` →\n ids · `updateNode(id, patch | (node) => patch, { commit })` ·\n `updateNodeData(id, patch | (data) => patch, { commit })` ·\n `updateNodesData(ids, patch)` · `removeNodes(ids)` · `duplicate(ids)` ·\n `lock(ids, on)` · `arrange(ids, \"front\" | \"back\")` · `align(ids, \"left\" |\n \"center\" | \"right\" | \"top\" | \"middle\" | \"bottom\")` · `distribute(ids,\n \"horizontal\" | \"vertical\")` · `frameSelection(ids)`\n- Connectors: `addEdge({ source, target, label?, data? })` → id ·\n `updateEdge(id, patch, { commit })` · `removeEdges(ids)`\n- The whole board: `setBoard(board, { fit = true })` (undoable) ·\n `autoLayout({ direction: \"LR\" | \"TB\", ids?, rankGap?, nodeGap? })` ·\n `undo()` · `redo()` · `save()` (send now, skipping the debounce)\n- Editing: `edit(id)` (start editing a node's words) · `editEdge(id)` ·\n `stopEditing()` · `commit(reason, ids?)` (after `{ commit: false }` changes)\n- View: `select(ids, edgeIds?)` · `focusNode(id)` (select, centre, focus) ·\n `fitView({ nodes?: [{ id }] })` · `zoomIn()` · `zoomOut()` · `zoomTo(z)` ·\n `centerPoint()` (the board point at the middle of the screen — where a\n picker's choice lands) · `screenToFlow({ x, y })` · `setTool(id)`\n\n## Your own node types\n\nA node component gets React Flow's props: `{ id, type, data, selected,\nwidth, height, dragging }`. Wrap its body in `NodeShell` — it draws the\nselection box, the four connection dots (shown on hover), the resize\nhandles, the lock mark and the whole-card drop target while a connector is\ndragged. Without it (or `NodeHandles`), connectors cannot reach the node.\nRead the canvas with `useCanvas()`; change the node through `api`.\n\n```jsx\nimport { FlowCanvas, NodeShell, EditableText, useCanvas, createNode } from \"./blocks/flow-canvas\";\nimport { Avatar, Badge } from \"./ui\";\n\nfunction PersonNode({ id, data, selected }) {\n const { api, editingNode, readOnly } = useCanvas();\n return (\n <NodeShell id={id} selected={selected} locked={data.locked} resizable={false}>\n <div className=\"flex w-64 items-center gap-3 rounded-lg border border-border bg-card p-3 shadow-sm\">\n <Avatar name={data.name} src={data.avatar} />\n <div className=\"min-w-0 flex-1\">\n <EditableText\n singleLine\n editing={!readOnly && editingNode === id}\n value={data.name}\n onChange={(v) => api.updateNodeData(id, { name: v }, { commit: false })}\n onDone={() => api.stopEditing()}\n label=\"Name\"\n className=\"truncate text-sm font-medium\"\n />\n <p className=\"truncate text-xs text-muted-foreground\">{data.title}</p>\n </div>\n <Badge tone={data.role === \"blocker\" ? \"danger\" : \"neutral\"}>{data.role}</Badge>\n </div>\n </NodeShell>\n );\n}\nconst NODE_TYPES = { person: PersonNode }; // module level: a new object each render remounts every node\n\n<FlowCanvas nodeTypes={NODE_TYPES} describeNode={(n) => n.type === \"person\" ? `Person: ${n.data.name} (${n.data.role})` : undefined} … />\n```\n\n- `NodeShell` props: `id`, `selected`, `locked`, `resizable` (default\n true), `minWidth`/`minHeight` (40/30), `keepAspectRatio`, `autoHeight`\n (height follows content), `handles` (false: nothing connects), `ring`\n (false: draw your own selection), `className` (layout only).\n- `EditableText` props: `value`, `editing`, `onChange(text)`, `onDone()`,\n `placeholder`, `singleLine` (Enter finishes), `label`, `className`,\n `style`. Shown and edited in the same box, so nothing jumps.\n- Double-click and Enter call `onOpenNode(node)` for your types when you\n pass it, else they set `editingNode` to the node's id (use it as above).\n- Colour your node from a token in its data with `colorClasses(token)` →\n `{ tint, solid, fill, stroke, wash, edge }` class strings\n (`colorClasses(\"chart-3\").tint` is `bg-chart-3/30`). `Palette`,\n `StickySwatch`, `ColorDot` and `ShapeGlyph` are the canvas's own pickers\n and swatches, for a legend or a side panel.\n- An interactive kit part inside a node takes `className=\"nodrag\"`.\n- Put a node's actions in `toolbarExtra` or `contextMenuItems`, not in a\n kit `Menu`/`Popover` inside the node: the node lives in the zoomed and\n panned viewport (a CSS transform), and the kit's floating panels anchor\n to their button without knowing about it. The canvas's own toolbars sit\n outside the viewport for this reason.\n\n## Rail tools\n\n```js\nconst TOOLS = [\n // A placing tool: pick it (or press P), click the canvas, a node appears there.\n { id: \"person\", label: \"Person\", icon: \"user\", shortcut: \"P\",\n create: (point, { rect, api }) => createNode(\"person\", point, { width: 256, data: { name: \"\", role: \"user\" } }) },\n // An action: pressing it runs onSelect — open your picker, then add.\n { id: \"pick\", label: \"Add from CRM\", icon: \"search\",\n onSelect: (api) => openPicker((p) => api.addNodes(createNode(\"person\", api.centerPoint(), { data: p }))) },\n];\n```\n\n- `create(point, { rect, api, grid })` returns a node or several (use\n `createNode`: it centres on `point` and snaps). With `sizable: true` the\n person can drag out a rectangle and you get it as `rect`.\n- A tool with `create` is also offered in the menu that opens when a\n connector is dropped on empty canvas (the new node arrives connected);\n `connectable: false` leaves it out.\n- The tool returns to Select after one placement. `shortcut` is one key.\n\n## Constraining behaviour — recipes\n\n- **Snap stickies into cells** (journey map): compute the cells from your\n lanes, then\n `constrain={(n) => { if (n.type !== \"sticky\") return; const r = nodeRect(n); const c = snapToCell(r, cells); return c && { position: { x: c.x, y: c.y }, data: { ...n.data, cell: c.cellId } }; }}`.\n- **Lock the lanes**: lanes are `frame` nodes with `draggable: false,\n deletable: false, selectable: false` (and `data.locked: true` to hide the\n resize handles). They still hold what is dropped inside them.\n- **Only steps connect** (process map): `canConnect={(c, b) => isStep(c.source) && isStep(c.target)}`.\n- **At most N votes** (idea board): keep votes in `data.badge` (the sticky\n shows it) and refuse extra ones in your own handler; or\n `beforeChange={(next) => votesOk(next) || false}`.\n- **Tidy a flow**: `api.autoLayout({ direction: \"LR\" })`, or in data\n `autoLayout(nodes, edges, { direction: \"TB\" })` before passing `value`.\n\n## The helpers\n\nImport them from the block; they take a board and never throw on a partial\none.\n\n- `emptyBoard()` — `{ nodes: [], edges: [] }`.\n- `createNode(type, point, { id?, data?, width?, height?, rect?, grid = 20, … })`\n — a node centred on `point` with its type's defaults, snapped. Extra keys\n (`draggable`, `zIndex`…) go on the node.\n- `createEdge(source, target, { id?, label?, data? })`.\n- `cleanBoard(board)` — what is stored (React Flow's working fields gone).\n- `boardToText(board, { describe? })` — plain text for an agent or a screen\n reader: `Board: 12 items, 5 connectors.`, then each frame with what it\n holds (reading order, nested), loose items, and `Connectors:` as\n `\"A\" → \"B\" (label)`.\n- `boardToMarkdown(board, { title?, describe? })` — a heading per frame, a\n bullet per item, a Connections list.\n- `autoLayout(nodes, edges, { direction = \"LR\", rankGap = 80, nodeGap = 40, ids?, grid = 20 })`\n — a layered layout written here (no dagre/elk): cycles broken, ranks by\n longest path, barycentre sweeps against crossings; unconnected nodes in\n a row after; the top-left corner kept. Returns every node, the laid-out\n ones moved. Frames are left alone unless named in `ids`.\n- `alignNodes(nodes, ids, how)`, `distributeNodes(nodes, ids, axis)`,\n `duplicateNodes(board, ids, { offset })` → `{ nodes, edges }`,\n `removeNodes(board, ids)`.\n- `frameContents(board, frameId)` → ids inside it; `frameOf(board)` →\n Map(nodeId → the smallest frame holding it).\n- `nodeRect(node)`, `boardBounds(nodes)`, `snapPoint(p, grid)`,\n `snapToCell(rect, cells, { pad })` → `{ x, y, cellId }`,\n `fitFontSize(text, width, height)` (the sticky's shrinking words).\n- `historyInit(board)`, `historyReducer(state, { type: \"push\" | \"undo\" |\n \"redo\" | \"reset\", present? })` — the undo stack the canvas uses, for a\n block that keeps its own.\n- `starterTemplates()`, `describeNode(node, describe?)`, `nodeText(node)`,\n `COLORS`, `STICKY_COLORS`, `COLOR_NAMES`, `SHAPES`, `GRID`.\n\n## Mouse and keyboard\n\n- Scroll or Space + drag pans; pinch or Ctrl/⌘ + scroll zooms; the middle\n button drags the canvas in any tool.\n- Drag on empty canvas draws a selection box (what it fully encloses);\n Shift-click adds to the selection; drag a frame by its title.\n- Tools: V select, H hand, N sticky, S shape, T text, L connector, F frame,\n and your tools' keys. Shapes, text and frames can be dragged out to a size.\n- Enter edits the selected card's words, Esc stops; Delete/Backspace\n deletes; ⌘D duplicates; ⌘C/⌘X/⌘V copy, cut, paste (paste lines of text\n from elsewhere: a sticky each); ⌘A selects all; ⌘Z / ⇧⌘Z undo, redo;\n arrows nudge 1 px (⇧ 10); ⌘] / ⌘[ front, back; ⇧⌘L lock; ⇧1 fit;\n ⇧2 zoom to selection; ⌘+ / ⌘− / ⌘0 zoom; ? the shortcuts sheet.\n- Tab walks the cards (each selected as it gets focus); Shift+F10 or the\n Menu key opens the actions menu; the List view (zoom bar) shows the board\n as a list, and choosing a row goes back to that card.\n\n## Example\n\n```jsx\n// surfaces/launch.jsx — client: one project's board\n\"use client\";\nimport { useState } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { useRow } from \"../lib/data\";\nimport { updateRow } from \"../lib/actions\";\nimport { FlowCanvas } from \"../components/blocks/flow-canvas\";\n\nexport default function LaunchSurface() {\n const id = useSearchParams().get(\"row\");\n const { row: project, loading } = useRow({ source: \"projects\", id });\n if (loading || !project) return null;\n return <LaunchBoard key={project.id} project={project} />;\n}\n\nfunction LaunchBoard({ project }) {\n const [board, setBoard] = useState(project.board); // jsonb, may be null; read once per project\n return (\n <FlowCanvas\n value={board ?? undefined}\n onChange={setBoard}\n onSave={(b) => updateRow({ source: \"projects\", id: project.id, patch: { board: b } })}\n label={`${project.name} board`}\n height=\"calc(100dvh - 8rem)\"\n />\n );\n}\n```\n\nIn a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.\n\nA card drawn with kit parts, and an agent reading the board:\n\n```jsx\n<FlowCanvas\n value={board}\n onChange={setBoard}\n renderCard={(n) => n.data.kind === \"deal\" ? (\n <div className=\"grid gap-1 p-3\">\n <p className=\"text-sm font-medium\">{n.data.title}</p>\n <Stat size=\"sm\" label=\"Value\" value={n.data.amount} />\n </div>\n ) : undefined}\n/>\n\nconst context = boardToText(board); // hand this to the agent\n```\n\n## What it will not do\n\n- Collaborate live (several cursors at once). Saves are last-write-wins.\n- Draw freehand (that is `sketch-canvas`), or export an image.\n- Nest data under frames: membership is geometric, so a card dragged out\n of a frame simply leaves it.\n- Route connectors around cards; they take the shortest curve between the\n facing sides.\n- Upload images itself: pass `pickImage`.\n- Render on the server: the server shows the board as text until the\n canvas mounts. For a read-only page that must be rich in the HTML, render\n `boardToMarkdown` yourself.\n\nA board it cannot be is your own component in `components/` — not an edit\nto this block. Its files are fixed: `npm run check` refuses a change, and\n`makersclaw add flow-canvas` restores them.\n\n## Credits\n\nReact Flow (https://github.com/xyflow/xyflow), MIT, by webkid GmbH —\ninstalled from npm as a dependency, not copied here. Its attribution link\nstays in the corner unless `hideAttribution`. See THIRD-PARTY-NOTICES.md.\n"
|
|
66
|
+
"content": "# FlowCanvas\n\nA Miro-like board in the workspace theme. Sticky notes, shapes, text,\nframes, cards and images on an infinite dotted canvas. A tool rail on the\nleft (a bar along the bottom on a narrow canvas), double-click anywhere\nfor a sticky, drag from a card's dot to another card to connect them —\nor drop on empty canvas to pick what to add there. A floating toolbar\nover the selection, a right-click menu, undo/redo, copy/paste, snap to\ngrid, a minimap, a list view for keyboards and screen readers, and a\npresentation mode that steps through the frames.\n\nIt is [React Flow](https://reactflow.dev) (`@xyflow/react`, MIT)\nunderneath, with its colours mapped onto the workspace's tokens. The\ncanvas blocks — `account-map`, `process-map`, `journey-map`, `idea-board`\n— are built on this one: they import it, never `@xyflow/react`.\n\n```jsx\nimport { FlowCanvas, boardToText } from \"../components/blocks/flow-canvas\"; // a Makersclaw app (\"./blocks/flow-canvas\" from components/)\nimport { FlowCanvas, boardToText } from \"@/components/makersclaw/blocks/flow-canvas\"; // a shadcn project\n```\n\n`FlowCanvas` is a client component: render it from a `\"use client\"`\ncomponent. The helpers (`boardToText`, `autoLayout`, `createNode`…) are\nplain functions and work on the server too.\n\n`makersclaw add flow-canvas` adds `@xyflow/react` (pinned, exact) to\npackage.json: run `npm install` after. It loads with the page (no\n`import()`): the board mounts after the first render; until then — on the\nserver — the box holds the board as text for a screen reader.\n\n## The data\n\nA board is plain JSON. Store it as it comes out of `onChange` (one `jsonb`\ncolumn).\n\n```js\n{\n nodes: [\n { id: \"s1\", type: \"sticky\", position: { x: 40, y: 80 }, width: 200, height: 200,\n data: { text: \"Interview churned customers\", color: \"warning\", badge: 3, meta: \"Priya\" } },\n { id: \"f1\", type: \"frame\", position: { x: 0, y: 0 }, width: 720, height: 420, zIndex: -1,\n data: { title: \"Discover\", color: \"chart-2\" } },\n ],\n edges: [\n { id: \"e1\", source: \"s1\", target: \"s2\", label: \"feeds\",\n data: { line: \"curved\", arrow: \"end\", dashed: false } },\n ],\n viewport: { x: 0, y: 0, zoom: 1 }, // where the person last looked; optional\n}\n```\n\n- **Positions are absolute**, top-left corner, in board pixels. A card in a\n frame is not stored relative to it: a frame holds whatever lies inside\n its rectangle (`frameContents`, `frameOf`). Dragging a frame moves what\n is inside it.\n- **Colours are token names**, never colour values, so a board repaints\n with the theme: `\"warning\"` (the yellow sticky), `\"chart-1\"` … `\"chart-5\"`,\n `\"success\"`, `\"info\"`, `\"destructive\"`, `\"muted\"`. `COLORS` lists them;\n `STICKY_COLORS` is the sticky palette.\n- **Paint order is list order** (later is on top); frames always sit\n under everything (`zIndex: -1`).\n- `width`/`height` are set by resizing; `text` and `card` nodes leave\n `height` out and grow with their content.\n- A `sticky` or `shape` always has a box: one that arrives without a\n `width` or `height` gets its type's default for the missing side\n (sticky 200×200; a shape its outline's, e.g. pill 180×64). Their words\n are fitted to that box, so give a starter board's stickies the size you\n want them to have (`withFittedSize` is what the canvas applies).\n- A node may carry React Flow's own flags: `draggable: false`,\n `deletable: false`, `selectable: false`, `connectable: false`. Locking\n (the toolbar's lock) sets `data.locked: true` with `draggable` and\n `deletable` false.\n\nThe built-in node types and their `data`:\n\n| type | data |\n| --- | --- |\n| `sticky` | `text`, `color` (default `\"warning\"`), `badge?` (a count in the corner: votes), `meta?` (a small line at the foot: an author) |\n| `shape` | `text`, `shape`: `rect` · `ellipse` · `diamond` · `pill`, `color` (default `\"muted\"`), `textSize`: `sm` · `md` · `lg` · `xl` |\n| `text` | `text`, `textSize`, `align?`: `left` · `center` · `right` |\n| `frame` | `title`, `color?` (a faint wash and edge; none = the card surface) |\n| `card` | `title`, `body?`, `icon?` (a kit icon name), `badge?`, `color?` (a strip on top), `kind?`, anything else your `renderCard` reads |\n| `image` | `src`, `alt` |\n\nConnectors: `data.line` `curved` (default) · `straight` · `elbow`;\n`data.arrow` `end` (default) · `both` · `none`; `data.dashed`. They leave\nand enter by the sides of the two cards that face each other, so they\nre-route as cards move; handle ids are not stored.\n\n## Props\n\n- `value` — the board, controlled. A new `value` that differs from what\n the canvas last sent through `onChange` replaces it (loading another\n board, or a change made elsewhere — a vote count); that replacement can\n be undone. Pass back exactly what `onChange` gave you and nothing resets.\n- `defaultValue` — the starting board, uncontrolled. With neither, it\n starts empty (and shows the empty state).\n- `onChange(board)` — after each committed change (a drag ends, a card is\n placed, words stop being edited, a colour is picked), debounced by\n `changeDelay` (default 400 ms). The board is clean JSON (see above).\n Selection, typing in progress and panning never call it.\n- `onSave(board)` — `async`, return `{ ok: true }` or `{ ok: false, error }`\n (a throw counts as failed). Called after `onChange`, and on ⌘S. The\n canvas shows \"Saving…\", \"Saved\", or the error with a Retry beside\n undo/redo. Leave it out when `onChange` already saves.\n- `changeDelay` — ms, default 400.\n- `readOnly` — `false` (default). `true`: no rail, no editing, no\n connecting; pan, zoom, fit, the list view and `onOpenNode` still work.\n- `presentation` — `false` (default). `true`: read-only, and a bar steps\n through the frames in reading order (← → or the buttons; each fills the\n screen). `onExitPresentation()` — Esc and the bar's Exit call it; turn\n `presentation` off there.\n- `height` — the canvas's height, default `560` (px; any CSS length works:\n `\"100%\"` inside a sized box, such as the kit's `AppLayout\n contentMode=\"fill\"` for a board that is the whole surface, the\n Example). Never a `vh` length: `npm run check` refuses a size taken\n from the window.\n- `bare` — `false` (default: rounded, bordered). `true`: no border or\n radius, for a full-page board.\n- `label` — the board's accessible name, default `\"Board\"`; also the list\n view's heading.\n- `className` — layout around it (margin, width). Never colour.\n- `palette` — the sticky colours offered, token names, default\n `STICKY_COLORS` (six). The toolbar adds a few more for shapes and frames.\n- `snap` — `true` (default): positions and sizes snap to `grid` (default\n `20` px — the dot spacing). The person can turn it off from the view menu.\n- `minimap` — `false` (default): whether the minimap starts open (a toggle\n in the zoom bar either way; hidden on a narrow canvas).\n- `hideAttribution` — `false` (default). React Flow asks that its small\n credit stays; its MIT licence allows hiding it.\n- `canvasRef` — a ref (`useFlowCanvas()`) that receives the handle (below).\n- `onSelectionChange(nodes, edges)` — the selection changed (a side panel\n for the selected person).\n- `onOpenNode(node)` — double-click or Enter on a node that has no words of\n its own to edit: your own node types, cards drawn by `renderCard`, and\n every node in read-only mode. Open your `Sheet` there. Without it,\n double-click edits the words.\n- `describeNode(node)` — a one-line description of one of YOUR node types,\n for the list view, `boardToText` and the server placeholder; return\n nothing for the default (`Sticky: \"…\"`).\n\nExtending it — how a block makes the canvas its own:\n\n- `nodeTypes` — `{ person: PersonNode, … }`: your own node components,\n added to the built-in ones (a key that matches a built-in replaces it).\n See \"Your own node types\".\n- `renderCard(node, ctx)` — draws the inside of every `card` node; return\n `undefined` for the default (title + body). `ctx`: `{ selected, editing,\n update(patch), api }`. Kit parts only (Avatar, Badge, Stat…); give an\n interactive part inside `className=\"nodrag\"` so pressing it does not drag\n the card. The simplest way to put your own card on the board.\n- `tools` — more rail tools: `[{ id, label, icon, shortcut?, create?,\n onSelect?, sizable?, connectable? }]`. See \"Rail tools\".\n- `rail` — the rail's contents, in order: built-in ids (`\"select\"`,\n `\"hand\"`, `\"sticky\"`, `\"shape\"`, `\"text\"`, `\"connector\"`, `\"frame\"`,\n `\"image\"`), your tools' ids, and `\"|\"` for a divider. Default: all the\n built-ins, then your tools (and `\"image\"` when `pickImage` is set). A\n process map drops the sticky: `rail={[\"select\", \"hand\", \"|\", \"shape\", \"connector\", \"step\"]}`.\n- `pickImage()` — `async () => ({ src, alt, width?, height? }) | null`: turns\n on the image tool (your upload, your picker). Without it there is no\n image tool; image nodes in the data still show.\n- `topBar` — a node shown floating at the top centre: a title, a save\n button, a share menu, a timer.\n- `toolbarExtra(selection, api)` — more controls at the end of the\n floating toolbar over a selection (`selection.nodes`): kit\n `IconButton size=\"sm\"` / `Menu` parts. \"Vote\", \"Assign owner\".\n- `contextMenuItems(selection, api)` — more right-click items, kit `Menu`\n shape (`[{ label, onSelect, icon?, shortcut?, tone? }]`); `selection.nodes`\n is empty for a right-click on the empty canvas.\n- `emptyState` — a node in place of the first-run hint; `false` for none.\n- `templates` — the starters on the empty state: `[{ id, label, icon?,\n description?, board }]` (`board` a board or a function returning one).\n Default `starterTemplates()` (Brainstorm, Flowchart, Retro); `[]` for none.\n- `constrain(node, { reason, board })` — runs on every node a change\n touched before it is committed (`reason`: `\"move\"` after a drag or a\n nudge, `\"create\"`, `\"paste\"`, `\"resize\"`, `\"align\"`, `\"layout\"`). Return\n the node adjusted (a new position, size or data), or nothing to keep it.\n This is how a journey map snaps a sticky into its cell and records it.\n- `beforeChange(next, prev, reason)` — runs on the whole board at every\n commit (`next` and `prev` are clean boards). Return `false` to refuse the\n change (the board goes back), a board to commit instead, or nothing to\n accept it.\n- `canConnect(connection, board)` — `false` refuses a connector\n (`connection`: `{ source, target }`). A card never connects to itself.\n\n## The handle\n\n`const board = useFlowCanvas()`, `<FlowCanvas canvasRef={board} />`, then\n`board.current?.…`. Tools' `create`/`onSelect`, `renderCard`,\n`toolbarExtra` and `contextMenuItems` get the same object as `api`, and a\nnode component reaches it with `useCanvas().api`. Every change made through\nit commits (undoable, then `onChange`) unless `{ commit: false }` says not.\n\n- Read: `getBoard()` (clean JSON, with the viewport) · `getSelection()` →\n `{ nodes, edges }` · `toText()` · `toMarkdown({ title })` · `canUndo()` ·\n `canRedo()` · `getTool()` · `readOnly`\n- Nodes: `addNodes(node | nodes, { select = true, edit = false, edges })` →\n ids · `updateNode(id, patch | (node) => patch, { commit })` ·\n `updateNodeData(id, patch | (data) => patch, { commit })` ·\n `updateNodesData(ids, patch)` · `removeNodes(ids)` · `duplicate(ids)` ·\n `lock(ids, on)` · `arrange(ids, \"front\" | \"back\")` · `align(ids, \"left\" |\n \"center\" | \"right\" | \"top\" | \"middle\" | \"bottom\")` · `distribute(ids,\n \"horizontal\" | \"vertical\")` · `frameSelection(ids)`\n- Connectors: `addEdge({ source, target, label?, data? })` → id ·\n `updateEdge(id, patch, { commit })` · `removeEdges(ids)`\n- The whole board: `setBoard(board, { fit = true })` (undoable) ·\n `autoLayout({ direction: \"LR\" | \"TB\", ids?, rankGap?, nodeGap? })` ·\n `undo()` · `redo()` · `save()` (send now, skipping the debounce)\n- Editing: `edit(id)` (start editing a node's words) · `editEdge(id)` ·\n `stopEditing()` · `commit(reason, ids?)` (after `{ commit: false }` changes)\n- View: `select(ids, edgeIds?)` · `focusNode(id)` (select, centre, focus) ·\n `fitView({ nodes?: [{ id }] })` · `zoomIn()` · `zoomOut()` · `zoomTo(z)` ·\n `centerPoint()` (the board point at the middle of the screen — where a\n picker's choice lands) · `screenToFlow({ x, y })` · `setTool(id)`\n\n## Your own node types\n\nA node component gets React Flow's props: `{ id, type, data, selected,\nwidth, height, dragging }`. Wrap its body in `NodeShell` — it draws the\nselection box, the four connection dots (shown on hover), the resize\nhandles, the lock mark and the whole-card drop target while a connector is\ndragged. Without it (or `NodeHandles`), connectors cannot reach the node.\nRead the canvas with `useCanvas()`; change the node through `api`.\n\n```jsx\nimport { FlowCanvas, NodeShell, EditableText, useCanvas, createNode } from \"./blocks/flow-canvas\";\nimport { Avatar, Badge } from \"./ui\";\n\nfunction PersonNode({ id, data, selected }) {\n const { api, editingNode, readOnly } = useCanvas();\n return (\n <NodeShell id={id} selected={selected} locked={data.locked} resizable={false}>\n <div className=\"flex w-64 items-center gap-3 rounded-lg border border-border bg-card p-3 shadow-sm\">\n <Avatar name={data.name} src={data.avatar} />\n <div className=\"min-w-0 flex-1\">\n <EditableText\n singleLine\n editing={!readOnly && editingNode === id}\n value={data.name}\n onChange={(v) => api.updateNodeData(id, { name: v }, { commit: false })}\n onDone={() => api.stopEditing()}\n label=\"Name\"\n className=\"truncate text-sm font-medium\"\n />\n <p className=\"truncate text-xs text-muted-foreground\">{data.title}</p>\n </div>\n <Badge tone={data.role === \"blocker\" ? \"danger\" : \"neutral\"}>{data.role}</Badge>\n </div>\n </NodeShell>\n );\n}\nconst NODE_TYPES = { person: PersonNode }; // module level: a new object each render remounts every node\n\n<FlowCanvas nodeTypes={NODE_TYPES} describeNode={(n) => n.type === \"person\" ? `Person: ${n.data.name} (${n.data.role})` : undefined} … />\n```\n\n- `NodeShell` props: `id`, `selected`, `locked`, `resizable` (default\n true), `minWidth`/`minHeight` (40/30), `keepAspectRatio`, `autoHeight`\n (height follows content), `handles` (false: nothing connects), `ring`\n (false: draw your own selection), `className` (layout only).\n- `EditableText` props: `value`, `editing`, `onChange(text)`, `onDone()`,\n `placeholder`, `singleLine` (Enter finishes), `label`, `className`,\n `style`. Shown and edited in the same box, so nothing jumps.\n- Double-click and Enter call `onOpenNode(node)` for your types when you\n pass it, else they set `editingNode` to the node's id (use it as above).\n- Colour your node from a token in its data with `colorClasses(token)` →\n `{ tint, solid, fill, stroke, wash, edge }` class strings\n (`colorClasses(\"chart-3\").tint` is `bg-chart-3/30`). `Palette`,\n `StickySwatch`, `ColorDot` and `ShapeGlyph` are the canvas's own pickers\n and swatches, for a legend or a side panel.\n- An interactive kit part inside a node takes `className=\"nodrag\"`.\n- Put a node's actions in `toolbarExtra` or `contextMenuItems`, not in a\n kit `Menu`/`Popover` inside the node: the node lives in the zoomed and\n panned viewport (a CSS transform), and the kit's floating panels anchor\n to their button without knowing about it. The canvas's own toolbars sit\n outside the viewport for this reason.\n\n## Rail tools\n\n```js\nconst TOOLS = [\n // A placing tool: pick it (or press P), click the canvas, a node appears there.\n { id: \"person\", label: \"Person\", icon: \"user\", shortcut: \"P\",\n create: (point, { rect, api }) => createNode(\"person\", point, { width: 256, data: { name: \"\", role: \"user\" } }) },\n // An action: pressing it runs onSelect — open your picker, then add.\n { id: \"pick\", label: \"Add from CRM\", icon: \"search\",\n onSelect: (api) => openPicker((p) => api.addNodes(createNode(\"person\", api.centerPoint(), { data: p }))) },\n];\n```\n\n- `create(point, { rect, api, grid })` returns a node or several (use\n `createNode`: it centres on `point` and snaps). With `sizable: true` the\n person can drag out a rectangle and you get it as `rect`.\n- A tool with `create` is also offered in the menu that opens when a\n connector is dropped on empty canvas (the new node arrives connected);\n `connectable: false` leaves it out.\n- The tool returns to Select after one placement. `shortcut` is one key.\n\n## Constraining behaviour — recipes\n\n- **Snap stickies into cells** (journey map): compute the cells from your\n lanes, then\n `constrain={(n) => { if (n.type !== \"sticky\") return; const r = nodeRect(n); const c = snapToCell(r, cells); return c && { position: { x: c.x, y: c.y }, data: { ...n.data, cell: c.cellId } }; }}`.\n- **Lock the lanes**: lanes are `frame` nodes with `draggable: false,\n deletable: false, selectable: false` (and `data.locked: true` to hide the\n resize handles). They still hold what is dropped inside them.\n- **Only steps connect** (process map): `canConnect={(c, b) => isStep(c.source) && isStep(c.target)}`.\n- **At most N votes** (idea board): keep votes in `data.badge` (the sticky\n shows it) and refuse extra ones in your own handler; or\n `beforeChange={(next) => votesOk(next) || false}`.\n- **Tidy a flow**: `api.autoLayout({ direction: \"LR\" })`, or in data\n `autoLayout(nodes, edges, { direction: \"TB\" })` before passing `value`.\n\n## The helpers\n\nImport them from the block; they take a board and never throw on a partial\none.\n\n- `emptyBoard()` — `{ nodes: [], edges: [] }`.\n- `createNode(type, point, { id?, data?, width?, height?, rect?, grid = 20, … })`\n — a node centred on `point` with its type's defaults, snapped. Extra keys\n (`draggable`, `zIndex`…) go on the node.\n- `createEdge(source, target, { id?, label?, data? })`.\n- `cleanBoard(board)` — what is stored (React Flow's working fields gone).\n- `boardToText(board, { describe? })` — plain text for an agent or a screen\n reader: `Board: 12 items, 5 connectors.`, then each frame with what it\n holds (reading order, nested), loose items, and `Connectors:` as\n `\"A\" → \"B\" (label)`.\n- `boardToMarkdown(board, { title?, describe? })` — a heading per frame, a\n bullet per item, a Connections list.\n- `autoLayout(nodes, edges, { direction = \"LR\", rankGap = 80, nodeGap = 40, ids?, grid = 20 })`\n — a layered layout written here (no dagre/elk): cycles broken, ranks by\n longest path, barycentre sweeps against crossings; unconnected nodes in\n a row after; the top-left corner kept. Returns every node, the laid-out\n ones moved. Frames are left alone unless named in `ids`.\n- `alignNodes(nodes, ids, how)`, `distributeNodes(nodes, ids, axis)`,\n `duplicateNodes(board, ids, { offset })` → `{ nodes, edges }`,\n `removeNodes(board, ids)`.\n- `frameContents(board, frameId)` → ids inside it; `frameOf(board)` →\n Map(nodeId → the smallest frame holding it).\n- `nodeRect(node)`, `boardBounds(nodes)`, `snapPoint(p, grid)`,\n `snapToCell(rect, cells, { pad })` → `{ x, y, cellId }`,\n `fitFontSize(text, width, height)` (the sticky's shrinking words).\n- `historyInit(board)`, `historyReducer(state, { type: \"push\" | \"undo\" |\n \"redo\" | \"reset\", present? })` — the undo stack the canvas uses, for a\n block that keeps its own.\n- `starterTemplates()`, `describeNode(node, describe?)`, `nodeText(node)`,\n `COLORS`, `STICKY_COLORS`, `COLOR_NAMES`, `SHAPES`, `GRID`.\n\n## Mouse and keyboard\n\n- Scroll or Space + drag pans; pinch or Ctrl/⌘ + scroll zooms; the middle\n button drags the canvas in any tool.\n- Drag on empty canvas draws a selection box (what it fully encloses);\n Shift-click adds to the selection; drag a frame by its title.\n- Tools: V select, H hand, N sticky, S shape, T text, L connector, F frame,\n and your tools' keys. Shapes, text and frames can be dragged out to a size.\n- Enter edits the selected card's words, Esc stops; Delete/Backspace\n deletes; ⌘D duplicates; ⌘C/⌘X/⌘V copy, cut, paste (paste lines of text\n from elsewhere: a sticky each); ⌘A selects all; ⌘Z / ⇧⌘Z undo, redo;\n arrows nudge 1 px (⇧ 10); ⌘] / ⌘[ front, back; ⇧⌘L lock; ⇧1 fit;\n ⇧2 zoom to selection; ⌘+ / ⌘− / ⌘0 zoom; ? the shortcuts sheet.\n- Tab walks the cards (each selected as it gets focus); Shift+F10 or the\n Menu key opens the actions menu; the List view (zoom bar) shows the board\n as a list, and choosing a row goes back to that card.\n\n## Example\n\n```jsx\n// surfaces/launch.jsx — client: one project's board\n\"use client\";\nimport { useState } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { useRow } from \"../lib/data\";\nimport { updateRow } from \"../lib/actions\";\nimport { AppLayout } from \"../components/ui\";\nimport { FlowCanvas } from \"../components/blocks/flow-canvas\";\n\nexport default function LaunchSurface() {\n const id = useSearchParams().get(\"row\");\n const { row: project, loading } = useRow({ source: \"projects\", id });\n if (loading || !project) return null;\n return <LaunchBoard key={project.id} project={project} />;\n}\n\nfunction LaunchBoard({ project }) {\n const [board, setBoard] = useState(project.board); // jsonb, may be null; read once per project\n return (\n <AppLayout contentMode=\"fill\">\n <FlowCanvas\n value={board ?? undefined}\n onChange={setBoard}\n onSave={(b) => updateRow({ source: \"projects\", id: project.id, patch: { board: b } })}\n label={`${project.name} board`}\n height=\"100%\"\n bare\n />\n </AppLayout>\n );\n}\n```\n\nIn a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.\n\nA card drawn with kit parts, and an agent reading the board:\n\n```jsx\n<FlowCanvas\n value={board}\n onChange={setBoard}\n renderCard={(n) => n.data.kind === \"deal\" ? (\n <div className=\"grid gap-1 p-3\">\n <p className=\"text-sm font-medium\">{n.data.title}</p>\n <Stat size=\"sm\" label=\"Value\" value={n.data.amount} />\n </div>\n ) : undefined}\n/>\n\nconst context = boardToText(board); // hand this to the agent\n```\n\n## What it will not do\n\n- Collaborate live (several cursors at once). Saves are last-write-wins.\n- Draw freehand (that is `sketch-canvas`), or export an image.\n- Nest data under frames: membership is geometric, so a card dragged out\n of a frame simply leaves it.\n- Route connectors around cards; they take the shortest curve between the\n facing sides.\n- Upload images itself: pass `pickImage`.\n- Render on the server: the server shows the board as text until the\n canvas mounts. For a read-only page that must be rich in the HTML, render\n `boardToMarkdown` yourself.\n\nA board it cannot be is your own component in `components/` — not an edit\nto this block. Its files are fixed: `npm run check` refuses a change, and\n`makersclaw add flow-canvas` restores them.\n\n## Credits\n\nReact Flow (https://github.com/xyflow/xyflow), MIT, by webkid GmbH —\ninstalled from npm as a dependency, not copied here. Its attribution link\nstays in the corner unless `hideAttribution`. See THIRD-PARTY-NOTICES.md.\n"
|
|
67
67
|
}
|
|
68
68
|
],
|
|
69
69
|
"categories": [
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"docs": "Installed to components/makersclaw/blocks/flow-canvas. Import: import { FlowCanvas, useFlowCanvas, useCanvas, NodeShell, NodeHandles, EditableText, Palette, StickySwatch, ColorDot, ShapeGlyph, colorClasses, emptyBoard, createNode, createEdge, cleanBoard, boardToText, boardToMarkdown, autoLayout, snapToCell, frameContents, historyReducer, starterTemplates } from \"@/components/makersclaw/blocks/flow-canvas\". Props and an example: components/makersclaw/blocks/flow-canvas/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
87
87
|
"meta": {
|
|
88
88
|
"makersclaw": {
|
|
89
|
-
"version": "0.
|
|
89
|
+
"version": "0.8.1",
|
|
90
90
|
"useWhen": "A screen where people arrange things spatially and connect them: a brainstorm, a flowchart or process map, an account or org map, a journey map, a retro, a planning board. The canvas blocks (account-map, process-map, journey-map, idea-board) are built on it. Not for freehand drawing (sketch-canvas), records in stage columns (board), or a chart of numbers (chart)."
|
|
91
91
|
}
|
|
92
92
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "flow-editor",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "FlowEditor",
|
|
6
|
+
"description": "An automation as a node canvas, in the app's theme: pan, zoom, fit and a map; icon cards with ports and curved connectors with arrowheads; a + on every connector and at every path's end; labelled Yes/No branches that meet again at a join; a dashed for-each box that folds to one card; the app's accent on agent and model steps (solid for the agent, outlined for a model call); a palette of the kinds; undo and redo; and an outline view of the same flow for a thumb or a keyboard. FlowEditor draws a definition and says what was pressed (insert, select, fold); FlowCanvas, StepOutline and Minimap are its parts. In a run it wears an overlay: each card shows how it went, the path taken is lit and the others dimmed, a card the agent fixed says so.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json",
|
|
9
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/automation-model.json",
|
|
10
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/step-picker.json"
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [],
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "registry/blocks/flow-editor/index.js",
|
|
16
|
+
"type": "registry:component",
|
|
17
|
+
"target": "@components/makersclaw/blocks/flow-editor/index.js",
|
|
18
|
+
"content": "// The FlowEditor block — `makersclaw add flow-editor`. Fixed: compose it\n// through its props (README.md beside this file).\nexport { FlowCanvas } from \"./flow-canvas\";\nexport { FlowEditor } from \"./flow-editor\";\nexport { Minimap } from \"./minimap\";\nexport { StepOutline } from \"./outline\";\n"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"path": "registry/blocks/flow-editor/flow-canvas.jsx",
|
|
22
|
+
"type": "registry:component",
|
|
23
|
+
"target": "@components/makersclaw/blocks/flow-editor/flow-canvas.jsx",
|
|
24
|
+
"content": "\"use client\";\n\n// An automation drawn as a real node canvas: pan, zoom, fit, a map, icon\n// cards with ports, curved connectors with arrowheads, a + on every\n// connector and at every path's end, labelled branches that meet again, a\n// dashed box for \"for each\", grouped steps that fold to one card, and a\n// palette.\n//\n// Built by hand, no graph library: the format is a tree, so the layout is\n// exact (automation-model graph.js). It takes a layout and callbacks and\n// paints with the app's tokens.\n//\n// Two uses, one picture:\n// - editing (`editable`): + inserts, a card selects, the palette adds;\n// - a run (`overlay`): each card wears how it went, the path taken is lit\n// and the rest dimmed; nothing can be changed.\n//\n// Input: drag the ground to pan; the wheel pans, Ctrl/Cmd-wheel and pinch\n// zoom; the arrow keys walk the cards, Esc lets go. Everything the canvas\n// does with a pointer is also reachable by keys and by the outline.\n\nimport { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport { Icon, cx } from \"../../ui\";\nimport { Glyph, KINDS, LABEL_W, T, curveBetween, fixTag, nodeIndex, readingOrder, slotWords, triggerGlyph } from \"../automation-model\";\nimport { StepPicker, PaletteBar } from \"../step-picker\";\nimport { MAP_H, MAP_W, Minimap } from \"./minimap\";\n\nconst MIN_K = 0.2;\nconst MAX_K = 1.6;\nconst clampK = (k) => Math.min(MAX_K, Math.max(MIN_K, k));\n\n/** One decimal only when there is one: 3 → \"3\", 2.4 → \"2.4\". */\nconst formatPrice = (n) => String(Math.round(n * 10) / 10);\n\nconst STATE_ICON = { done: \"check\", would: \"pencil\", running: \"loader\", failed: \"x\", fixed: \"wand\", waiting: \"clock\", skipped: \"minus\", unknown: \"circle-help\" };\nconst BADGE = {\n done: \"bg-success\",\n would: \"bg-muted-foreground\",\n running: \"bg-info\",\n failed: \"bg-destructive\",\n fixed: \"bg-primary\",\n waiting: \"bg-warning\",\n skipped: \"bg-muted-foreground\",\n unknown: \"bg-muted-foreground\",\n};\n// A tile: tone first, then how the run left it (the later wins by order).\nconst TONE = {\n plain: \"border-border text-muted-foreground\",\n model: \"border-primary/55 text-primary\",\n agent: \"border-primary bg-primary/15 text-primary\",\n};\nconst RUNSTATE = {\n done: \"border-success/60\",\n // The pencil look: it would run, it did not. Dashed, no solid fill.\n would: \"border-dashed border-primary/70 bg-transparent text-primary\",\n running: \"border-info ring-4 ring-info/20\",\n failed: \"border-destructive\",\n fixed: \"border-primary\",\n waiting: \"border-warning ring-4 ring-warning/20\",\n skipped: \"border-dashed border-muted-foreground/45 bg-transparent text-muted-foreground/60\",\n notreached: \"border-dashed border-muted-foreground/45 bg-transparent text-muted-foreground/60\",\n unknown: \"border-warning\",\n};\nconst EDGE = {\n n: { path: \"fill-none stroke-muted-foreground/55\", head: \"fill-muted-foreground/55\", width: 1.6 },\n t: { path: \"fill-none stroke-success\", head: \"fill-success\", width: 2.6 },\n r: { path: \"fill-none stroke-info\", head: \"fill-info\", width: 2.6 },\n d: { path: \"fill-none stroke-muted-foreground/25\", head: \"fill-muted-foreground/25\", width: 1.6 },\n};\n\n/**\n * layout automation-model's layoutAutomation()\n * selectedId a step id, \"trigger\", or null\n * onSelect(id|null)\n * editable the + buttons, the palette and the add button\n * onInsert(slot, kind) a + was pressed and a kind chosen\n * onAddAtSelection(kind) the palette (or the round add) chose a kind\n * onToggleCollapse(stepId) fold or open a for-each\n * overlay a run's overlayOf(): each card and connector, how it went\n * (in a test run a step that stood in for itself is \"would\":\n * a dashed card with a pencil and one line of what it would do;\n * a step it could not check says \"Not checked\")\n * flagged a Set of step ids with something to fix (a red mark)\n * prices a Map of step id to { credits, max }: the price tag\n * fitKey changes when the picture should fit again\n * revealId pan so this step is in view (after an insert)\n * info floating, top left, after the zoom controls\n * runnable a Set of kinds the platform runs (the picker says \"soon\" for the rest)\n */\nexport function FlowCanvas({ layout, selectedId = null, onSelect, editable = false, onInsert, onAddAtSelection, onToggleCollapse, overlay = null, flagged, prices, fitKey, revealId, info, runnable, className }) {\n const uid = useId().replace(/[^a-zA-Z0-9]/g, \"\");\n const rootRef = useRef(null);\n const [view, setViewState] = useState({ x: 40, y: 40, k: 1 });\n const viewRef = useRef(view);\n const [size, setSize] = useState({ w: 0, h: 0 });\n const [picker, setPicker] = useState(null);\n const fitted = useRef(null);\n\n const setView = useCallback((next) => {\n const v = typeof next === \"function\" ? next(viewRef.current) : next;\n viewRef.current = v;\n setViewState(v);\n }, []);\n\n const index = useMemo(() => nodeIndex(layout), [layout]);\n\n /* ---- measuring and fitting ---------------------------------------- */\n\n const fit = useCallback(() => {\n const el = rootRef.current;\n if (!el) return;\n const w = el.clientWidth;\n const h = el.clientHeight;\n const b = layout.bounds;\n if (!w || !h || b.w <= 0) return;\n const k = clampK(Math.min(w / b.w, h / b.h, 1));\n setView({ k, x: (w - b.w * k) / 2 - b.x * k, y: (h - b.h * k) / 2 - b.y * k });\n }, [layout.bounds, setView]);\n\n useLayoutEffect(() => {\n const el = rootRef.current;\n if (!el) return undefined;\n const measure = () => setSize({ w: el.clientWidth, h: el.clientHeight });\n measure();\n if (typeof ResizeObserver === \"undefined\") return undefined;\n // Measure in the callback, act in the next frame: setting state that\n // changes what is watched, inside its own delivery, is the browser's\n // \"ResizeObserver loop\" notice (check rule 17).\n let raf = 0;\n const ro = new ResizeObserver(() => {\n cancelAnimationFrame(raf);\n raf = requestAnimationFrame(measure);\n });\n ro.observe(el);\n return () => {\n cancelAnimationFrame(raf);\n ro.disconnect();\n };\n }, []);\n\n // Fit once per fitKey, as soon as there is a size to fit into.\n useEffect(() => {\n const key = fitKey ?? \"\";\n if (fitted.current === key || !size.w || !size.h) return;\n fitted.current = key;\n fit();\n }, [fitKey, size.w, size.h, fit]);\n\n // Bring a new step into view without moving the picture if it is already seen.\n useEffect(() => {\n if (!revealId || !size.w) return;\n const n = index.get(revealId);\n if (!n) return;\n const v = viewRef.current;\n const sx = n.x * v.k + v.x;\n const sy = n.y * v.k + v.y;\n const pad = 80;\n const inside = sx > pad && sy > pad && sx + n.w * v.k < size.w - pad && sy + n.h * v.k < size.h - pad;\n if (!inside) setView({ k: v.k, x: size.w / 2 - (n.x + n.w / 2) * v.k, y: size.h / 2 - (n.y + n.h / 2) * v.k });\n }, [revealId, index, size.w, size.h, setView]);\n\n /* ---- zoom and pan -------------------------------------------------- */\n\n const zoomAt = useCallback(\n (factor, cx0, cy0) => {\n setView((v) => {\n const k = clampK(v.k * factor);\n const r = k / v.k;\n return { k, x: cx0 - (cx0 - v.x) * r, y: cy0 - (cy0 - v.y) * r };\n });\n },\n [setView],\n );\n const zoomCentre = (factor) => zoomAt(factor, size.w / 2, size.h / 2);\n\n // The wheel needs a non-passive listener to keep the page from zooming.\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return undefined;\n const onWheel = (e) => {\n if (e.target.closest?.(\"[data-scroll]\")) return;\n e.preventDefault();\n const r = el.getBoundingClientRect();\n if (e.ctrlKey || e.metaKey) zoomAt(Math.exp(-e.deltaY * 0.01), e.clientX - r.left, e.clientY - r.top);\n else setView((v) => ({ ...v, x: v.x - e.deltaX, y: v.y - e.deltaY }));\n };\n el.addEventListener(\"wheel\", onWheel, { passive: false });\n return () => el.removeEventListener(\"wheel\", onWheel);\n }, [zoomAt, setView]);\n\n const pointers = useRef(new Map());\n const drag = useRef(null);\n const pinch = useRef(null);\n\n const onPointerDown = (e) => {\n if (e.target.closest?.(\"button, input, select, textarea, [data-nopan]\")) return;\n pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });\n e.currentTarget.setPointerCapture?.(e.pointerId);\n if (pointers.current.size === 1) drag.current = { x: e.clientX, y: e.clientY, moved: false };\n else {\n drag.current = null;\n const [a, b] = [...pointers.current.values()];\n pinch.current = Math.hypot(a.x - b.x, a.y - b.y);\n }\n };\n const onPointerMove = (e) => {\n if (!pointers.current.has(e.pointerId)) return;\n const prev = pointers.current.get(e.pointerId);\n pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });\n if (pointers.current.size === 2 && pinch.current) {\n const [a, b] = [...pointers.current.values()];\n const dist = Math.hypot(a.x - b.x, a.y - b.y);\n const r = rootRef.current?.getBoundingClientRect();\n if (r && dist > 0) zoomAt(dist / pinch.current, (a.x + b.x) / 2 - r.left, (a.y + b.y) / 2 - r.top);\n pinch.current = dist;\n return;\n }\n const d = drag.current;\n if (!d) return;\n if (!d.moved && Math.hypot(e.clientX - d.x, e.clientY - d.y) < 4) return;\n d.moved = true;\n setView((v) => ({ ...v, x: v.x + (e.clientX - prev.x), y: v.y + (e.clientY - prev.y) }));\n };\n const onPointerUp = (e) => {\n pointers.current.delete(e.pointerId);\n pinch.current = null;\n const d = drag.current;\n drag.current = null;\n // A press on the ground that went nowhere lets go of the selection.\n if (d && !d.moved) {\n onSelect?.(null);\n setPicker(null);\n }\n };\n\n const onKeyDown = (e) => {\n if (e.target.closest?.(\"input, textarea, select\")) return;\n if (e.key === \"Escape\") {\n if (picker) setPicker(null);\n else onSelect?.(null);\n } else if (e.key === \"ArrowRight\" || e.key === \"ArrowLeft\") {\n const order = readingOrder(layout);\n if (!order.length) return;\n const at = order.findIndex((n) => (n.stepId ?? n.id) === selectedId || n.id === selectedId);\n const next = order[Math.min(order.length - 1, Math.max(0, at + (e.key === \"ArrowRight\" ? 1 : -1)))];\n if (next) onSelect?.(next.stepId ?? next.id);\n e.preventDefault();\n } else if (e.key === \"+\" || e.key === \"=\") zoomCentre(1.2);\n else if (e.key === \"-\") zoomCentre(1 / 1.2);\n else if (e.key === \"0\") fit();\n };\n\n /* ---- the picker ---------------------------------------------------- */\n\n const openPicker = (slot, at) => {\n const r = rootRef.current?.getBoundingClientRect();\n const PW = 330;\n const PH = 400;\n const left = Math.max(8, Math.min(at.x - (r?.left ?? 0) - PW / 2, (r?.width ?? 800) - PW - 8));\n const below = at.y - (r?.top ?? 0) + 16;\n const top = Math.max(8, Math.min(below, (r?.height ?? 600) - PH - 8));\n setPicker({ slot, left, top });\n };\n\n const pickedKind = (kind) => {\n const p = picker;\n setPicker(null);\n if (!p) return;\n if (p.slot) onInsert?.(p.slot, kind);\n else onAddAtSelection?.(kind);\n };\n\n /* ---- drawing ------------------------------------------------------- */\n\n const edges = layout.edges.map((e) => {\n const a = index.get(e.from);\n const b = index.get(e.to);\n return a && b ? { e, curve: curveBetween(a, b) } : null;\n });\n\n const edgeKind = (id) => {\n if (!overlay) return \"n\";\n const s = overlay.edges.get(id) ?? \"dim\";\n return s === \"taken\" ? \"t\" : s === \"running\" ? \"r\" : \"d\";\n };\n const stateOf = (n) => (overlay ? (overlay.nodes.get(n.id) ?? \"notreached\") : null);\n\n const small = view.k < 0.45;\n const showMap = size.w > MAP_W * 2 && size.h > MAP_H * 2.4;\n const showPalette = editable && onAddAtSelection && size.w >= 560;\n const dot = 24 * view.k;\n\n return (\n <div\n ref={rootRef}\n role=\"group\"\n aria-label=\"Automation canvas\"\n tabIndex={0}\n onKeyDown={onKeyDown}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n onPointerCancel={onPointerUp}\n className={cx(\"relative isolate min-h-[320px] flex-1 cursor-grab touch-none select-none overflow-hidden bg-background outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring active:cursor-grabbing\", className)}\n >\n {/* The dotted ground travels with the picture. */}\n <span\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-0 -z-10 opacity-30\"\n style={{\n backgroundImage: \"radial-gradient(circle, var(--color-muted-foreground) 1px, transparent 1.4px)\",\n backgroundSize: `${dot}px ${dot}px`,\n backgroundPosition: `${view.x}px ${view.y}px`,\n }}\n />\n <div className=\"absolute left-0 top-0 origin-top-left will-change-transform\" style={{ transform: `translate(${view.x}px, ${view.y}px) scale(${view.k})` }}>\n <svg className=\"pointer-events-none absolute left-0 top-0 overflow-visible\" width=\"1\" height=\"1\" aria-hidden=\"true\">\n <defs>\n {[\"n\", \"t\", \"r\", \"d\"].map((k) => (\n <marker key={k} id={`arr-${uid}-${k}`} viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"8\" markerHeight=\"8\" orient=\"auto\">\n <path d=\"M 1 1 L 9 5 L 1 9 z\" className={EDGE[k].head} />\n </marker>\n ))}\n </defs>\n {edges.map((x) => {\n if (!x) return null;\n const k = edgeKind(x.e.id);\n const toEnd = index.get(x.e.to)?.type === \"end\";\n return <path key={x.e.id} d={x.curve.d} className={EDGE[k].path} strokeWidth={EDGE[k].width} strokeDasharray={k === \"d\" ? \"6 6\" : toEnd ? \"5 5\" : undefined} markerEnd={`url(#arr-${uid}-${k})`} />;\n })}\n </svg>\n\n {layout.groups.map((g) => {\n const state = overlay ? (overlay.nodes.get(g.id) ?? \"notreached\") : null;\n const sel = selectedId === g.stepId;\n return (\n <div\n key={g.id}\n style={{ left: g.x, top: g.y, width: g.w, height: g.h }}\n className={cx(\n \"pointer-events-none absolute rounded-[20px] border-[1.5px] border-dashed bg-muted/55\",\n sel ? \"border-solid border-foreground\" : \"border-muted-foreground/45\",\n (state === \"done\" || state === \"fixed\") && \"border-success/70\",\n state === \"would\" && \"border-primary/60\",\n state === \"running\" && \"border-info\",\n state === \"failed\" && \"border-destructive\",\n state === \"notreached\" && \"opacity-50\",\n )}\n >\n <div data-nopan className=\"pointer-events-auto absolute -top-3.5 left-4 flex h-7 max-w-[calc(100%_-_2rem)] items-center gap-0.5 rounded-full border-[1.5px] border-dashed border-muted-foreground/45 bg-background\">\n <button type=\"button\" aria-pressed={sel} onClick={() => onSelect?.(g.stepId)} className=\"inline-flex h-full min-w-0 items-center gap-1.5 overflow-hidden whitespace-nowrap pl-3 pr-1 text-[13px] font-semibold focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\">\n <Glyph name=\"loop\" size={14} />\n <span className=\"truncate\">{g.title}</span>\n <span className=\"truncate font-medium text-muted-foreground\">{g.sub}</span>\n </button>\n {onToggleCollapse ? (\n <button type=\"button\" aria-label={`Fold ${g.title}`} onClick={() => onToggleCollapse(g.stepId)} className=\"mr-0.5 inline-flex size-6 items-center justify-center rounded-full text-muted-foreground hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\">\n <Icon name=\"chevron-down\" size={14} />\n </button>\n ) : null}\n </div>\n </div>\n );\n })}\n\n {layout.nodes.map((n) => {\n const state = stateOf(n);\n if (n.type === \"join\") return <i key={n.id} style={{ left: n.x, top: n.y, width: n.w, height: n.h }} className=\"absolute rounded-full border-2 border-background bg-muted-foreground/70\" />;\n if (n.type === \"port\") {\n return (\n <i\n key={n.id}\n style={{ left: n.x, top: n.y, width: n.w, height: n.h }}\n className={cx(\"absolute rounded border-2 border-background\", state === \"done\" || state === \"fixed\" ? \"bg-success\" : state === \"would\" ? \"bg-primary/60\" : \"bg-muted-foreground/70\")}\n />\n );\n }\n if (n.type === \"end\") {\n if (!editable || !n.slot) return <i key={n.id} style={{ left: n.x + n.w / 2 - 4, top: n.y + n.h / 2 - 4 }} className=\"absolute size-2 rounded-full bg-muted-foreground/50\" />;\n const slot = n.slot;\n return (\n <button\n key={n.id}\n type=\"button\"\n data-nopan\n style={{ left: n.x, top: n.y, width: n.w, height: n.h }}\n aria-label={`${n.title} ${slotWords(slot)}`}\n title={n.title}\n onClick={(ev) => openPicker(slot, { x: ev.clientX, y: ev.clientY })}\n className=\"absolute flex items-center justify-center rounded-full border-[1.5px] border-dashed border-muted-foreground/70 bg-background text-muted-foreground hover:border-solid hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <Icon name=\"plus\" size={14} />\n {n.title === \"Add the first step\" ? <span className=\"absolute left-[38px] top-[5px] whitespace-nowrap text-[12.5px] text-muted-foreground\">Add the first step</span> : null}\n </button>\n );\n }\n if (n.type === \"card\") {\n const sel = selectedId === n.id;\n return (\n <div\n key={n.id}\n style={{ left: n.x, top: n.y, width: n.w, height: n.h }}\n className={cx(\n \"absolute rounded-2xl border bg-card p-3 text-card-foreground\",\n sel && \"outline outline-2 outline-offset-[3px] outline-foreground\",\n state === \"done\" ? \"border-success/60\" : state === \"would\" ? \"border-dashed border-primary/70\" : state === \"failed\" ? \"border-destructive\" : state === \"notreached\" ? \"border-dashed border-border opacity-60\" : \"border-border\",\n )}\n >\n <i className=\"pointer-events-none absolute -left-1 top-[calc(50%_-_8px)] h-4 w-[7px] rounded-sm bg-border\" />\n <i className=\"pointer-events-none absolute -right-1.5 top-[calc(50%_-_5px)] size-[11px] rounded-full border-2 border-background bg-muted-foreground/70\" />\n <div data-nopan className=\"flex items-center justify-between gap-2\">\n <button type=\"button\" aria-pressed={sel} onClick={() => onSelect?.(n.id)} className=\"inline-flex min-w-0 items-center gap-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\">\n <Glyph name=\"loop\" size={16} />\n <b className=\"truncate\">{n.title}</b>\n </button>\n <button type=\"button\" onClick={() => onToggleCollapse?.(n.id)} className=\"inline-flex items-center gap-0.5 whitespace-nowrap text-xs text-muted-foreground underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\">\n <Icon name=\"chevron-right\" size={14} /> Show steps\n </button>\n </div>\n <p className=\"m-0 mb-2 mt-0.5 text-xs text-muted-foreground\">{n.sub}</p>\n <ol className=\"m-0 flex list-none flex-col gap-1.5 p-0\">\n {(n.items ?? []).map((it) => (\n <li key={it.id} className=\"rounded-lg border border-border bg-background px-2.5 py-1 text-[12.5px]\">\n {it.title}\n </li>\n ))}\n {(n.items ?? []).length === 0 ? <li className=\"rounded-lg border border-border bg-background px-2.5 py-1 text-[12.5px] text-muted-foreground\">No steps yet</li> : null}\n </ol>\n </div>\n );\n }\n // A step or the trigger: an icon tile with its words below.\n const glyph = n.type === \"trigger\" ? triggerGlyph(n.triggerType) : (KINDS[n.kind]?.glyph ?? \"action\");\n const selected = selectedId === (n.stepId ?? n.id);\n const badgeIcon = state ? STATE_ICON[state] : undefined;\n const bad = n.stepId ? flagged?.has(n.stepId) : false;\n const sum = overlay && n.stepId ? overlay.summary.get(n.stepId) : null;\n const price = !overlay && n.stepId ? prices?.get(n.stepId) : null;\n return (\n <div key={n.id} style={{ left: n.x, top: n.y, width: T, height: T }} className={cx(\"absolute\", state === \"notreached\" || state === \"skipped\" ? \"opacity-55\" : undefined)}>\n {n.type !== \"trigger\" ? <i className=\"pointer-events-none absolute -left-1 top-[calc(50%_-_8px)] h-4 w-[7px] rounded-sm bg-border\" /> : null}\n {n.kind !== \"stop\" ? <i className=\"pointer-events-none absolute -right-1.5 top-[calc(50%_-_5px)] z-[1] size-[11px] rounded-full border-2 border-background bg-muted-foreground/70\" /> : null}\n <button\n type=\"button\"\n data-nopan\n aria-pressed={selected}\n aria-label={`${n.title}${state ? `. ${state === \"would\" ? \"would run\" : state}` : \"\"}`}\n onClick={() => onSelect?.(n.stepId ?? n.id)}\n className={cx(\n \"relative flex size-full cursor-pointer items-center justify-center rounded-xl border bg-card transition-colors hover:border-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n n.type === \"trigger\" && \"rounded-l-[36px]\",\n TONE[n.tone] ?? TONE.plain,\n state ? RUNSTATE[state] : null,\n bad && \"border-destructive\",\n selected && \"outline outline-2 outline-offset-[3px] outline-foreground\",\n )}\n >\n <Glyph name={glyph} size={26} />\n {n.mark ? (\n <span className={cx(\"absolute -bottom-2 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-full border px-1.5 py-0.5 text-2xs font-semibold leading-none\", n.tone === \"agent\" ? \"border-transparent bg-primary text-primary-foreground\" : \"border-primary/55 bg-background text-primary\")}>{n.mark}</span>\n ) : null}\n </button>\n {n.num > 0 ? <span className=\"pointer-events-none absolute -left-1 -top-2.5 rounded bg-background px-1 font-mono text-2xs leading-[15px] text-muted-foreground\">{n.num}</span> : null}\n {badgeIcon && state ? (\n <span aria-hidden=\"true\" className={cx(\"pointer-events-none absolute -right-2 -top-2 z-[2] flex size-5 items-center justify-center rounded-full border-2 border-background text-background\", BADGE[state])}>\n <Icon name={badgeIcon} size={11} className={state === \"running\" ? \"animate-spin motion-reduce:animate-none\" : undefined} />\n </span>\n ) : null}\n {bad && !badgeIcon ? (\n <span aria-label=\"Needs a look\" className=\"pointer-events-none absolute -right-2 -top-2 z-[2] flex size-5 items-center justify-center rounded-full border-2 border-background bg-destructive text-xs font-extrabold text-background\">\n !\n </span>\n ) : null}\n <div className=\"pointer-events-none absolute flex flex-col gap-[3px] text-center\" style={{ top: T + 12, left: (T - LABEL_W) / 2, width: LABEL_W }}>\n <b className=\"break-words text-[13px] font-semibold leading-tight text-foreground [overflow-wrap:anywhere]\">{n.title}</b>\n {n.sub ? <span className=\"text-xs leading-snug text-muted-foreground [overflow-wrap:anywhere]\">{n.sub}</span> : null}\n {price ? <span className=\"self-center rounded-full border border-border px-2 font-mono text-2xs text-muted-foreground\">about {formatPrice(price.credits)} cr</span> : null}\n {sum && sum.passes > 1 ? <span className=\"font-mono text-xs text-muted-foreground\">{sum.passes} times</span> : null}\n {sum?.fix ? <span className=\"text-xs text-primary\">{fixTag(sum.fix)}</span> : null}\n {sum && state === \"failed\" && sum.words ? <span className=\"text-xs text-destructive\">{sum.words}</span> : null}\n {sum?.unchecked ? <span className=\"text-xs text-muted-foreground\">Not checked</span> : sum && state === \"would\" && sum.would ? <span className=\"text-xs text-primary\">{sum.would}</span> : null}\n </div>\n </div>\n );\n })}\n\n {edges.map((x) => {\n if (!x) return null;\n const { e, curve } = x;\n const state = overlay?.edges.get(e.id);\n const dim = overlay && state === \"dim\";\n const lab = e.label ? curve.at(0.24) : null;\n return (\n <span key={`m:${e.id}`}>\n {lab ? (\n <span\n style={{ left: lab.x, top: lab.y }}\n className={cx(\n \"pointer-events-none absolute -translate-x-1/2 -translate-y-1/2 whitespace-nowrap rounded-full border bg-background px-2.5 py-px text-xs font-semibold\",\n state === \"taken\" || state === \"running\" ? \"border-success text-success\" : \"border-border text-muted-foreground\",\n dim && \"opacity-50\",\n )}\n >\n {e.label}\n </span>\n ) : null}\n {editable && e.plus && e.slot ? (\n <button\n type=\"button\"\n data-nopan\n style={{ left: curve.mid.x, top: curve.mid.y }}\n aria-label={`Add a step ${slotWords(e.slot)}`}\n title=\"Add a step\"\n onClick={(ev) => openPicker(e.slot, { x: ev.clientX, y: ev.clientY })}\n className=\"absolute flex size-[22px] -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-border bg-background text-muted-foreground hover:bg-foreground hover:text-background focus-visible:bg-foreground focus-visible:text-background focus-visible:outline-none\"\n >\n <Icon name=\"plus\" size={12} />\n </button>\n ) : null}\n </span>\n );\n })}\n </div>\n\n <div data-nopan className=\"pointer-events-none absolute left-3 top-3 z-[6] flex flex-wrap items-center gap-2 pr-16 [&>*]:pointer-events-auto\">\n <div role=\"group\" aria-label=\"Zoom\" className=\"flex items-center gap-px rounded-lg border border-border bg-card p-0.5\">\n <button type=\"button\" aria-label=\"Zoom out\" onClick={() => zoomCentre(1 / 1.25)} className=\"inline-flex size-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\">\n <Icon name=\"minus\" size={15} />\n </button>\n <span aria-live=\"polite\" className=\"min-w-[46px] text-center font-mono text-xs text-card-foreground\">\n {Math.round(view.k * 100)}%\n </span>\n <button type=\"button\" aria-label=\"Zoom in\" onClick={() => zoomCentre(1.25)} className=\"inline-flex size-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\">\n <Icon name=\"plus\" size={15} />\n </button>\n <i className=\"mx-0.5 h-4 w-px bg-border\" />\n <button type=\"button\" aria-label=\"Fit to the screen\" title=\"Fit\" onClick={fit} className=\"inline-flex size-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\">\n <Icon name=\"maximize\" size={15} />\n </button>\n </div>\n {info}\n </div>\n\n {editable && onAddAtSelection && !showPalette ? (\n <button\n type=\"button\"\n data-nopan\n aria-label=\"Add a step\"\n title=\"Add a step\"\n onClick={(ev) => openPicker(null, { x: ev.clientX, y: ev.clientY })}\n className=\"absolute right-3.5 top-3 z-[6] flex size-[38px] items-center justify-center rounded-full bg-foreground text-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <Icon name=\"plus\" size={18} />\n </button>\n ) : null}\n\n {showPalette ? (\n <div data-nopan className=\"absolute bottom-3 left-3 z-[6] overflow-x-auto\" style={{ maxWidth: showMap ? `calc(100% - ${MAP_W + 36}px)` : \"calc(100% - 24px)\" }}>\n <PaletteBar onAdd={onAddAtSelection} />\n </div>\n ) : null}\n\n {showMap ? <Minimap layout={layout} view={view} size={size} overlay={overlay} onCenter={(wx, wy) => setView((v) => ({ ...v, x: size.w / 2 - wx * v.k, y: size.h / 2 - wy * v.k }))} /> : null}\n\n {picker ? <StepPicker style={{ left: picker.left, top: picker.top }} runnable={runnable} onClose={() => setPicker(null)} onPick={pickedKind} /> : null}\n </div>\n );\n}\n"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"path": "registry/blocks/flow-editor/flow-editor.jsx",
|
|
28
|
+
"type": "registry:component",
|
|
29
|
+
"target": "@components/makersclaw/blocks/flow-editor/flow-editor.jsx",
|
|
30
|
+
"content": "\"use client\";\n\n// The editor around the canvas: lays the definition out, folds a for-each\n// on request, and offers the canvas and the outline as two views of one\n// flow. It holds nothing of the definition itself (the screen around it\n// does, as a stack of pure edits from automation-model); it only draws\n// what it is given and says what a person pressed.\n//\n// Editing: `editable` with onInsert / onAddAtSelection. A run:\n// `overlay` (automation-model's overlayOf) and nothing editable.\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { Icon, cx } from \"../../ui\";\nimport { layoutAutomation, triggerWords as wordsOf } from \"../automation-model\";\nimport { FlowCanvas } from \"./flow-canvas\";\nimport { StepOutline } from \"./outline\";\n\n/** Under this width the editor opens on the outline. */\nconst NARROW = 480;\n\nconst BAR_BUTTON = \"inline-flex size-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent disabled:opacity-35 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\";\n\n/** Canvas | Outline, as the head of either view. */\nfunction ViewToggle({ view, onView }) {\n const one = (name, icon, label) => (\n <button\n type=\"button\"\n aria-pressed={view === name}\n onClick={() => onView(name)}\n className={cx(\"inline-flex h-7 items-center gap-1.5 rounded-md px-2 text-[13px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\", view === name ? \"bg-accent font-medium text-accent-foreground\" : \"text-muted-foreground hover:bg-accent\")}\n >\n <Icon name={icon} size={14} /> {label}\n </button>\n );\n return (\n <div role=\"group\" aria-label=\"View\" className=\"flex items-center gap-px rounded-lg border border-border bg-card p-0.5\">\n {one(\"canvas\", \"network\", \"Canvas\")}\n {one(\"outline\", \"list\", \"Outline\")}\n </div>\n );\n}\n\n/**\n * def the definition shown: { trigger, steps, … }\n * selectedId, onSelect(id|null)\n * editable the + buttons and the palette\n * onInsert(slot, kind), onAddAtSelection(kind)\n * overlay a run's overlayOf(), or null\n * flagged a Set of step ids with something to fix\n * prices a Map of step id to { credits, max }\n * fitKey, revealId see FlowCanvas\n * runnable a Set of kinds the platform runs\n * undo { canUndo, canRedo, onUndo, onRedo } or null: the history buttons\n * onOpenFlow the gear: open the automation's own settings (draws it when given)\n */\nexport function FlowEditor({ def, selectedId = null, onSelect, editable = false, onInsert, onAddAtSelection, overlay = null, flagged, prices, fitKey, revealId, runnable, undo = null, onOpenFlow, className }) {\n const rootRef = useRef(null);\n const [narrow, setNarrow] = useState(false);\n const [chosen, setChosen] = useState(null);\n // The canvas is a poor fit for a thumb: under about 480 px of its own\n // width it opens on the outline, until a person asks for the canvas.\n useEffect(() => {\n const el = rootRef.current;\n if (!el) return undefined;\n const measure = () => setNarrow(el.clientWidth > 0 && el.clientWidth < NARROW);\n measure();\n if (typeof ResizeObserver === \"undefined\") return undefined;\n // Measure in the callback, act in the next frame (check rule 17).\n let raf = 0;\n const ro = new ResizeObserver(() => {\n cancelAnimationFrame(raf);\n raf = requestAnimationFrame(measure);\n });\n ro.observe(el);\n return () => {\n cancelAnimationFrame(raf);\n ro.disconnect();\n };\n }, []);\n const view = chosen ?? (narrow ? \"outline\" : \"canvas\");\n const setView = setChosen;\n const [collapsed, setCollapsed] = useState(() => new Set());\n const words = wordsOf(def.trigger);\n const layout = useMemo(() => layoutAutomation(def, { triggerWords: words, collapsed }), [def, words, collapsed]);\n const steps = layout.nodes.filter((n) => n.type === \"step\" || n.type === \"card\").length;\n\n const toggleCollapse = (id) =>\n setCollapsed((c) => {\n const next = new Set(c);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n return next;\n });\n\n const history = undo ? (\n <div role=\"group\" aria-label=\"History\" className=\"flex items-center gap-px rounded-lg border border-border bg-card p-0.5\">\n <button type=\"button\" aria-label=\"Undo\" disabled={!undo.canUndo} onClick={undo.onUndo} className={BAR_BUTTON}>\n <Icon name=\"undo\" size={15} />\n </button>\n <button type=\"button\" aria-label=\"Redo\" disabled={!undo.canRedo} onClick={undo.onRedo} className={BAR_BUTTON}>\n <Icon name=\"redo\" size={15} />\n </button>\n </div>\n ) : null;\n\n const gear = onOpenFlow ? (\n <button type=\"button\" aria-label=\"Settings of this automation\" title=\"Settings of this automation\" onClick={onOpenFlow} className={cx(BAR_BUTTON, \"size-9 rounded-lg border border-border bg-card\")}>\n <Icon name=\"settings\" size={15} />\n </button>\n ) : null;\n\n return (\n <div ref={rootRef} className={cx(\"flex min-h-0 min-w-0 flex-1 flex-col\", className)}>\n {view === \"outline\" ? (\n <>\n <div className=\"flex flex-wrap items-center gap-2 border-b border-border px-3 py-2\">\n {history}\n <ViewToggle view={view} onView={setView} />\n {gear}\n </div>\n <div data-scroll className=\"min-h-0 flex-1 overflow-y-auto\">\n <StepOutline def={def} triggerWords={words} selectedId={selectedId} onSelect={onSelect} overlay={overlay} editable={editable} onInsert={onInsert} flagged={flagged} runnable={runnable} />\n </div>\n </>\n ) : (\n <FlowCanvas\n layout={layout}\n selectedId={selectedId}\n onSelect={onSelect}\n editable={editable}\n onInsert={onInsert}\n onAddAtSelection={onAddAtSelection}\n onToggleCollapse={toggleCollapse}\n overlay={overlay}\n flagged={flagged}\n prices={prices}\n fitKey={fitKey}\n revealId={revealId}\n runnable={runnable}\n info={\n <>\n {history}\n <div className=\"rounded-lg border border-border bg-card px-2.5 py-1 text-[13px] text-muted-foreground\">\n <b className=\"font-semibold text-foreground\">{steps}</b> {steps === 1 ? \"step\" : \"steps\"}\n </div>\n <ViewToggle view={view} onView={setView} />\n {gear}\n </>\n }\n />\n )}\n </div>\n );\n}\n"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"path": "registry/blocks/flow-editor/outline.jsx",
|
|
34
|
+
"type": "registry:component",
|
|
35
|
+
"target": "@components/makersclaw/blocks/flow-editor/outline.jsx",
|
|
36
|
+
"content": "\"use client\";\n\n// The same flow as an outline: the trigger, then every step in order, a\n// condition's branches and a loop's steps nested under it. It is the\n// phone's second view (the canvas is a poor fit for a thumb), and a way in\n// for anyone who works by keyboard or with a screen reader: every step is\n// a button, and every place a step can go has an \"Add a step\" button.\n\nimport { useState } from \"react\";\nimport { Icon, cx } from \"../../ui\";\nimport { Glyph, KINDS, fixTag, stepSub, stepTitle, triggerGlyph, triggerTitle } from \"../automation-model\";\nimport { StepPicker } from \"../step-picker\";\n\nconst STATE_ICON = { done: \"check\", would: \"pencil\", running: \"loader\", failed: \"x\", fixed: \"wand\", waiting: \"clock\", skipped: \"minus\", unknown: \"circle-help\" };\nconst STATE_TONE = { done: \"bg-success\", would: \"bg-muted-foreground\", running: \"bg-info\", failed: \"bg-destructive\", fixed: \"bg-primary\", waiting: \"bg-warning\", skipped: \"bg-muted-foreground\", unknown: \"bg-muted-foreground\" };\nconst FACE = { plain: \"text-muted-foreground\", model: \"text-primary\", agent: \"text-primary bg-primary/15 border-primary\" };\n\nconst slotKey = (s) => `${s.list.at}:${\"step\" in s.list ? s.list.step : \"\"}:${s.index}`;\n\n/**\n * def the definition: { trigger, steps }\n * triggerWords the trigger in words\n * selectedId a step id, \"trigger\", or null\n * onSelect(id)\n * overlay a run's overlayOf(), or null\n * editable, onInsert(slot, kind) the \"Add a step\" buttons\n * flagged a Set of step ids with something to fix\n * runnable a Set of kinds the platform runs\n */\nexport function StepOutline({ def, triggerWords, selectedId = null, onSelect, overlay = null, editable = false, onInsert, flagged, runnable }) {\n const [open, setOpen] = useState(null);\n\n const adder = (slot, label = \"Add a step\") => {\n if (!editable || !onInsert) return null;\n const key = slotKey(slot);\n return open === key ? (\n <StepPicker\n inline\n runnable={runnable}\n onClose={() => setOpen(null)}\n onPick={(kind) => {\n setOpen(null);\n onInsert(slot, kind);\n }}\n />\n ) : (\n <button type=\"button\" onClick={() => setOpen(key)} className=\"inline-flex min-h-9 items-center gap-1.5 rounded-md border border-dashed border-border px-2.5 text-[13px] text-muted-foreground hover:border-solid hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\">\n <span aria-hidden=\"true\">+</span> {label}\n </button>\n );\n };\n\n const row = (step, state) => {\n const info = KINDS[step.kind] ?? KINDS.action;\n const sum = overlay?.summary.get(step.id);\n const sel = selectedId === step.id;\n return (\n <button\n type=\"button\"\n aria-pressed={sel}\n onClick={() => onSelect?.(step.id)}\n className={cx(\n \"flex w-full min-w-0 items-center gap-2.5 rounded-lg border bg-card px-2.5 py-2 text-left text-card-foreground hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n sel ? \"border-foreground\" : flagged?.has(step.id) || state === \"failed\" ? \"border-destructive\" : state === \"would\" ? \"border-primary/70\" : \"border-border\",\n state === \"notreached\" && \"border-dashed opacity-60\",\n state === \"would\" && \"border-dashed\",\n )}\n >\n <span className={cx(\"inline-flex size-9 shrink-0 items-center justify-center rounded-lg border border-border\", FACE[info.tone] ?? FACE.plain)}>\n <Glyph name={info.glyph} size={18} />\n </span>\n <span className=\"grid min-w-0 flex-1 gap-0.5\">\n <b className=\"break-words text-[13px] font-semibold\">{stepTitle(step)}</b>\n <span className=\"break-words text-xs text-muted-foreground\">{stepSub(step)}</span>\n {sum?.fix ? <span className=\"text-xs text-primary\">{fixTag(sum.fix)}</span> : null}\n {sum && state === \"failed\" && sum.words ? <span className=\"text-xs text-destructive\">{sum.words}</span> : null}\n {sum?.unchecked ? <span className=\"text-xs text-muted-foreground\">Not checked</span> : sum && state === \"would\" && sum.would ? <span className=\"text-xs text-primary\">{sum.would}</span> : null}\n </span>\n {state && STATE_ICON[state] ? (\n <span className={cx(\"inline-flex size-5 shrink-0 items-center justify-center rounded-full text-background\", STATE_TONE[state])}>\n <Icon name={STATE_ICON[state]} size={13} className={state === \"running\" ? \"animate-spin motion-reduce:animate-none\" : undefined} />\n </span>\n ) : null}\n </button>\n );\n };\n\n const list = (steps, ref) => (\n <ol className=\"m-0 grid list-none gap-1.5 p-0\">\n {steps.map((step, i) => {\n const state = overlay ? (overlay.nodes.get(step.id) ?? \"notreached\") : null;\n return (\n <li key={step.id} className=\"grid gap-1.5\">\n {adder({ list: ref, index: i })}\n {row(step, state)}\n {step.kind === \"condition\" ? (\n <div className=\"grid gap-2 border-l-2 border-border pl-3\">\n <div className=\"grid gap-1.5\">\n <h5 className=\"m-0 text-xs font-semibold text-muted-foreground\">Yes</h5>\n {list(step.then ?? [], { at: \"then\", step: step.id })}\n </div>\n <div className=\"grid gap-1.5\">\n <h5 className=\"m-0 text-xs font-semibold text-muted-foreground\">No</h5>\n {list(step.else ?? [], { at: \"else\", step: step.id })}\n </div>\n </div>\n ) : null}\n {step.kind === \"loop\" ? <div className=\"grid gap-1.5 border-l-2 border-dashed border-border pl-3\">{list(step.steps ?? [], { at: \"loop\", step: step.id })}</div> : null}\n </li>\n );\n })}\n <li className=\"grid gap-1.5\">{adder({ list: ref, index: steps.length }, steps.length === 0 ? \"Add the first step\" : \"Add a step\")}</li>\n </ol>\n );\n\n const triggerType = def.trigger?.type;\n return (\n <div className=\"grid min-w-0 content-start gap-3 p-3\">\n <div className={cx(\"flex min-w-0 items-center gap-2.5 rounded-lg border bg-card px-2.5 py-2 text-card-foreground\", selectedId === \"trigger\" ? \"border-foreground\" : \"border-border\")}>\n <span className=\"inline-flex size-9 shrink-0 items-center justify-center rounded-lg border border-border text-muted-foreground\">\n <Glyph name={triggerGlyph(triggerType)} size={18} />\n </span>\n <span className=\"grid min-w-0 flex-1 gap-0.5\">\n <b className=\"text-[13px] font-semibold\">{triggerTitle(triggerType)}</b>\n <span className=\"text-xs text-muted-foreground\">{triggerWords}</span>\n </span>\n <button type=\"button\" aria-pressed={selectedId === \"trigger\"} onClick={() => onSelect?.(\"trigger\")} className=\"rounded-md px-2 py-1 text-[13px] text-muted-foreground underline hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\">\n Edit\n </button>\n </div>\n {list(def.steps ?? [], { at: \"root\" })}\n </div>\n );\n}\n"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"path": "registry/blocks/flow-editor/minimap.jsx",
|
|
40
|
+
"type": "registry:component",
|
|
41
|
+
"target": "@components/makersclaw/blocks/flow-editor/minimap.jsx",
|
|
42
|
+
"content": "\"use client\";\n\n// The map in the corner: the whole flow small, the part you are looking at\n// outlined. Press or drag on it to go there. In a run it wears the run's\n// colours, so a long flow shows where it went wrong at a glance.\n\nimport { useRef } from \"react\";\nimport { cx } from \"../../ui\";\n\nexport const MAP_W = 176;\nexport const MAP_H = 104;\n\nconst DOT = {\n idle: \"bg-muted-foreground/60\",\n done: \"bg-success\",\n would: \"bg-primary/60\",\n running: \"bg-info\",\n failed: \"bg-destructive\",\n fixed: \"bg-primary\",\n waiting: \"bg-warning\",\n skipped: \"bg-muted-foreground/30\",\n unknown: \"bg-muted-foreground/60\",\n notreached: \"bg-muted-foreground/25\",\n};\n\n/**\n * layout automation-model's layoutAutomation()\n * view { x, y, k }: where the canvas is\n * size { w, h }: the canvas's size\n * overlay a run's overlayOf(), or null\n * onCenter(worldX, worldY)\n */\nexport function Minimap({ layout, view, size, overlay, onCenter }) {\n const ref = useRef(null);\n const b = layout.bounds;\n if (b.w <= 0 || b.h <= 0) return null;\n const s = Math.min((MAP_W - 16) / b.w, (MAP_H - 24) / b.h);\n const ox = (MAP_W - b.w * s) / 2 - b.x * s;\n const oy = (MAP_H - b.h * s) / 2 + 6 - b.y * s;\n const rect = (x, y, w, h) => ({ left: x * s + ox, top: y * s + oy, width: Math.max(2, w * s), height: Math.max(2, h * s) });\n const vp = rect(-view.x / view.k, -view.y / view.k, size.w / view.k, size.h / view.k);\n\n const at = (e) => {\n const r = ref.current?.getBoundingClientRect();\n if (r) onCenter((e.clientX - r.left - ox) / s, (e.clientY - r.top - oy) / s);\n };\n\n return (\n <div\n ref={ref}\n data-nopan\n role=\"img\"\n aria-label=\"Map of the whole flow\"\n style={{ width: MAP_W, height: MAP_H }}\n className=\"absolute bottom-3 right-3 z-[6] cursor-pointer overflow-hidden rounded-lg border border-border bg-card shadow-sm\"\n onPointerDown={(e) => {\n e.stopPropagation();\n e.currentTarget.setPointerCapture?.(e.pointerId);\n at(e);\n }}\n onPointerMove={(e) => {\n if (e.buttons) at(e);\n }}\n >\n <span className=\"absolute left-2 top-1 text-2xs font-medium text-muted-foreground\">Map</span>\n {layout.groups.map((g) => (\n <span key={g.id} style={rect(g.x, g.y, g.w, g.h)} className=\"absolute rounded-sm border border-dashed border-muted-foreground/40\" />\n ))}\n {layout.nodes\n .filter((n) => n.type === \"step\" || n.type === \"trigger\" || n.type === \"card\")\n .map((n) => {\n const state = overlay ? (overlay.nodes.get(n.id) ?? \"notreached\") : \"idle\";\n return <span key={n.id} style={rect(n.x, n.y, n.w, n.h)} className={cx(\"absolute rounded-[2px]\", DOT[state] ?? DOT.idle)} />;\n })}\n <span style={vp} className=\"absolute rounded-sm border-2 border-foreground/70 bg-foreground/5\" />\n </div>\n );\n}\n"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"path": "registry/blocks/flow-editor/README.md",
|
|
46
|
+
"type": "registry:file",
|
|
47
|
+
"target": "@components/makersclaw/blocks/flow-editor/README.md",
|
|
48
|
+
"content": "# FlowEditor\n\nAn automation as a node canvas, in the app's theme. It draws a definition\nand says what was pressed; the screen around it holds the definition and\napplies the edits (`automation-editor` is that screen, ready made).\n\n- **The canvas** — drag the ground to pan; the wheel pans, Ctrl or ⌘ with\n the wheel and a pinch zoom; **+**, **−**, **0** and the fit button; a map\n in the corner you can press and drag. Icon cards with a port on each\n side, curved connectors with arrowheads, the card's number above it and\n its words below.\n- **A + on every connector** and at the end of every path. A press opens\n the step picker there and the new step lands exactly where the + was\n (`onInsert(slot, kind)`).\n- **Branches** — a condition draws its Yes and No paths one above the\n other, labelled, and they meet again at a small join dot where the next\n step carries on. A Stop ends its path.\n- **For each** — a dashed box around its steps with a door in and a door\n out; fold it to one card that lists them.\n- **The agent and the model call** — an agent step is solid in the app's\n accent (`primary`, as the kit's `AgentMark` draws the agent) with an \"A\"\n mark; a model call is outlined in it with a \"model\" mark; every other step\n is plain. Shape and a word say which, never colour alone.\n- **A palette** along the bottom (wide enough), or a round + (narrow).\n- **The outline** — the same flow as a nested list: the phone's second\n view, and a way in by keyboard or a screen reader. Every step is a\n button; every place a step can go has an \"Add a step\" button.\n- **A run** — with an `overlay` (from `overlayOf`) every card wears how it\n went (done, running, failed, waiting, skipped, fixed by the agent,\n not reached), the path taken is lit and the others dimmed, and a looped\n step says how many times. A **test run** (a dry run) draws a step that\n stood in for itself as a dashed card with a pencil and one line of what\n it would do (\"Would send with send_email.\"); a step that needs what such\n a step would have made says \"Not checked\". Both read off `overlayOf`'s\n `would` state and the summary's `would` and `unchecked`.\n\n```jsx\nimport { FlowEditor } from \"../components/blocks/flow-editor\"; // a Makersclaw app\nimport { FlowEditor } from \"@/components/makersclaw/blocks/flow-editor\"; // a shadcn project\n```\n\n`makersclaw add flow-editor` lays `step-picker` and `automation-model`\nwith it. A client component. It takes the width it is given and its own\nwidth decides its layout; give it a parent with a height (it fills it, and\nis at least 320 px tall). It never scrolls the page.\n\n## Props\n\n### FlowEditor\n\n- `def` — the definition shown (`{ trigger, steps, … }`).\n- `selectedId`, `onSelect(id | null)` — a step id, `\"trigger\"`, or none.\n- `editable` — the + buttons and the palette. Leave it out to look.\n- `onInsert(slot, kind)` — a + was pressed and a kind chosen. `slot` is\n `{ list, index }`: pass it to automation-model's `insertStep` or\n `addStep(def, slot, kind)`.\n- `onAddAtSelection(kind)` — the palette (or the round add) chose a kind.\n- `overlay` — a run's `overlayOf(layout, steps, status)`, or null.\n- `flagged` — a `Set` of step ids with something to fix (a red mark).\n- `prices` — a `Map` of step id → `{ credits, max }`: a paid step's price\n tag (\"about 3 cr\"), from `priceMap(estimate)`.\n- `fitKey` — changes when the picture should fit again (another\n automation, another run). `revealId` — pan so this step is in view.\n- `runnable` — a `Set` of the kinds the platform runs.\n- `undo` — `{ canUndo, canRedo, onUndo, onRedo }`: draws the history\n buttons. `onOpenFlow` — draws the gear (the automation's own settings).\n- `className`.\n\n### The parts\n\n`FlowCanvas` (the canvas alone, given a `layout`), `StepOutline` (the\nlist), `Minimap`.\n\n## Never\n\n- Never edit the definition inside it: it only says what was pressed.\n- Never a state by colour alone: each card has its mark and its word.\n- Never a graph with a free arrow: the format is a tree, so a branch only\n meets again where its condition ends.\n\n## Example\n\n```jsx\n\"use client\";\nimport { useState } from \"react\";\nimport { addStep, blankAutomation } from \"../components/blocks/automation-model\";\nimport { FlowEditor } from \"../components/blocks/flow-editor\";\n\nexport default function Flow() {\n const [def, setDef] = useState(() => ({ ...blankAutomation(\"Monday note\", \"monday-note\", { type: \"schedule\", cron: \"0 9 * * 1\", tz: \"team\" }), steps: [{ id: \"note\", kind: \"notify\", message: \"Look at the week ahead.\" }] }));\n const [selected, setSelected] = useState(null);\n return (\n <div className=\"flex h-[560px] flex-col rounded-lg border border-border\">\n <FlowEditor\n def={def}\n editable\n selectedId={selected}\n onSelect={setSelected}\n onInsert={(slot, kind) => {\n const r = addStep(def, slot, kind);\n setDef(r.def);\n setSelected(r.id);\n }}\n />\n </div>\n );\n}\n```\n"
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"categories": [
|
|
52
|
+
"automation",
|
|
53
|
+
"workflow",
|
|
54
|
+
"canvas",
|
|
55
|
+
"node",
|
|
56
|
+
"flow",
|
|
57
|
+
"graph",
|
|
58
|
+
"trigger",
|
|
59
|
+
"steps",
|
|
60
|
+
"branches",
|
|
61
|
+
"condition",
|
|
62
|
+
"loop",
|
|
63
|
+
"for each",
|
|
64
|
+
"n8n",
|
|
65
|
+
"make",
|
|
66
|
+
"zapier",
|
|
67
|
+
"builder",
|
|
68
|
+
"editor",
|
|
69
|
+
"run",
|
|
70
|
+
"overlay"
|
|
71
|
+
],
|
|
72
|
+
"docs": "Installed to components/makersclaw/blocks/flow-editor. Import: import { FlowCanvas, FlowEditor, Minimap, StepOutline } from \"@/components/makersclaw/blocks/flow-editor\". Props and an example: components/makersclaw/blocks/flow-editor/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
73
|
+
"meta": {
|
|
74
|
+
"makersclaw": {
|
|
75
|
+
"version": "0.8.1",
|
|
76
|
+
"useWhen": "An app shows what an automation does as a flow a member can read and change: a CRM's morning read, an inbox sweep, a nightly import, an event-driven workflow. For the whole page (header, panels, save) place automation-editor; for one run use run-view; for a read-only graph of a job's run top to bottom use workflow-view. Not a free-form board (flow-canvas) and not a mind map."
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
package/r/footer.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"title": "Footer",
|
|
6
6
|
"description": "A public site's footer: brand and tagline, link columns or one row of links, social buttons in the kit's brand marks (X, LinkedIn, GitHub, YouTube, Instagram, Facebook), and the legal line with its links; optionally a newsletter sign-up with consent (validates, saves through an async onSubscribe returning { ok }, shows the failure), a call to action, a large brand statement, language and currency selects, and a version badge. Simple, columns or centred.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json"
|
|
9
9
|
],
|
|
10
10
|
"dependencies": [],
|
|
11
11
|
"files": [
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"path": "registry/blocks/footer/README.md",
|
|
26
26
|
"type": "registry:file",
|
|
27
27
|
"target": "@components/makersclaw/blocks/footer/README.md",
|
|
28
|
-
"content": "# Footer\n\nA public site's footer. It holds the brand and a line about it, link\ncolumns (or one row of links), social buttons drawn with the kit's brand\nmarks, and the legal line with its links. Optional parts: a newsletter\nsign-up (with a consent box), a call to action, a large brand statement\nor quote, language and currency selects, and a version badge.\n\nThree layouts:\n- **columns** (default) — the brand, tagline and socials beside the link\n columns (and an inline sign-up), then the legal row;\n- **simple** — one row: brand, links, socials; then the legal row;\n- **centred** — everything stacked in the middle.\n\nThe sign-up checks the email (and the consent, when asked) in place,\nthen calls `newsletter.onSubscribe(email, { consent })`, which is async\nand returns `{ ok }`. While it runs the button spins. `{ ok: false,\nerror }` (or a throw) shows the error and keeps the email. `{ ok: true }`\nshows \"You're subscribed\". It is a full-width section: it pads itself\nand centres its content.\n\n```jsx\nimport { Footer } from \"../components/blocks/footer\"; // a Makersclaw app (\"./blocks/footer\" from components/)\nimport { Footer } from \"@/components/makersclaw/blocks/footer\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component (a\nlayout is the usual place).\n\n## The data\n\n```js\n// a link, anywhere: columns, links, legalLinks\n{ label: \"Pricing\", href: \"/pricing\", external?: true } // external opens a new tab\n\n// columns\n[{ title: \"Product\", links: [link, …] }, …] // up to 5 across; 2 on a phone\n\n// socials\n[{ network: \"x\", href: \"https://x.com/fieldline\", label? }, …]\n```\n\n`network` is one of `x`, `linkedin`, `github`, `youtube`, `instagram`,\n`facebook`, `google`, `apple`, `microsoft`, `email`, `rss`. Anything\nelse needs an `icon` (a kit Icon name) and a `label`. Each is an icon\nbutton, named to a screen reader (\"X\", \"LinkedIn\"), opening a new tab.\n\n## Props\n\n- `layout` — `columns` (default) · `simple` · `centered`.\n- `brand` — a node: your logo, linked home if you like.\n- `tagline` — a line under the brand (columns and centred).\n- `columns` — link columns (columns layout).\n- `links` — one row of links (simple and centred; under the socials in\n columns).\n- `socials` — as above.\n- `newsletter` — `{ onSubscribe, title?, body?, label?, placeholder?,\n consent?, look?, doneTitle?, doneBody? }`. `onSubscribe(email, {\n consent })` is async and returns `{ ok: true }` or `{ ok: false,\n error? }`; without it there is no sign-up. `consent` is the checkbox's\n label (a string or a node with a link), and must be ticked. `look`:\n `inline` (default: a column beside the links) · `card` (a full-width\n card under them). `label` is the button, default \"Subscribe\".\n- `cta` — `{ title?, body?, actions: [{ label, href?, onClick?, icon?,\n variant? }] }`: \"Sign in\", \"Get the app\". A band above the legal row;\n the first action is primary unless it says otherwise.\n- `statement` — a large line at the top: a string, or `{ text, by }` for\n a quote.\n- `locales` — `[{ id, label, options: [{ value, label }], value?,\n defaultValue?, onChange? }]`: language, currency, country selects in\n the legal row. Uncontrolled unless you pass `value`.\n- `version` — \"v4.2.0\": a code badge in the legal row.\n- `legal` — the copyright line: \"© 2026 Fieldline Ltd.\". Pass the year;\n the block does not read the clock.\n- `legalLinks` — privacy, terms, cookies.\n- `onNavigate(link, event)` — called when any link or a `cta` action with\n an `href` is followed, before navigation; `event.preventDefault()`\n stops it.\n- `label` — what a screen reader calls the footer and its link row.\n Default \"Site\".\n- `className` — the footer's spacing. Never colour.\n\n## Example\n\n```jsx\n// components/SiteFooter.jsx\n\"use client\";\nimport { Icon } from \"./ui\";\nimport { Footer } from \"./blocks/footer\";\nimport { insertRow } from \"../lib/actions\";\n\nexport default function SiteFooter() {\n return (\n <Footer\n brand={<span className=\"inline-flex items-center gap-2\"><Icon name=\"zap\" /> Fieldline</span>}\n tagline=\"The pipeline small sales teams actually keep up to date.\"\n columns={[\n { title: \"Product\", links: [{ label: \"Pricing\", href: \"/pricing\" }, { label: \"Changelog\", href: \"/changelog\" }] },\n { title: \"Company\", links: [{ label: \"About\", href: \"/about\" }, { label: \"Careers\", href: \"/careers\" }] },\n ]}\n socials={[{ network: \"x\", href: \"https://x.com/fieldline\" }, { network: \"linkedin\", href: \"https://linkedin.com/company/fieldline\" }]}\n newsletter={{\n body: \"One email a month.\",\n consent: \"I agree to the privacy policy.\",\n onSubscribe: (email) => insertRow({ source: \"subscribers\", values: { email } }), // { ok, id } or { ok: false, error }\n }}\n legal=\"© 2026 Fieldline Ltd. All rights reserved.\"\n legalLinks={[{ label: \"Privacy\", href: \"/privacy\" }, { label: \"Terms\", href: \"/terms\" }]}\n />\n );\n}\n```\n\nIn a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.\n\n## What it will not do\n\n- Change the language or currency by itself: `locales[].onChange` is\n yours.\n- Stick to the bottom of a short page: that is your layout's\n `
|
|
28
|
+
"content": "# Footer\n\nA public site's footer. It holds the brand and a line about it, link\ncolumns (or one row of links), social buttons drawn with the kit's brand\nmarks, and the legal line with its links. Optional parts: a newsletter\nsign-up (with a consent box), a call to action, a large brand statement\nor quote, language and currency selects, and a version badge.\n\nThree layouts:\n- **columns** (default) — the brand, tagline and socials beside the link\n columns (and an inline sign-up), then the legal row;\n- **simple** — one row: brand, links, socials; then the legal row;\n- **centred** — everything stacked in the middle.\n\nThe sign-up checks the email (and the consent, when asked) in place,\nthen calls `newsletter.onSubscribe(email, { consent })`, which is async\nand returns `{ ok }`. While it runs the button spins. `{ ok: false,\nerror }` (or a throw) shows the error and keeps the email. `{ ok: true }`\nshows \"You're subscribed\". It is a full-width section: it pads itself\nand centres its content.\n\n```jsx\nimport { Footer } from \"../components/blocks/footer\"; // a Makersclaw app (\"./blocks/footer\" from components/)\nimport { Footer } from \"@/components/makersclaw/blocks/footer\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component (a\nlayout is the usual place).\n\n## The data\n\n```js\n// a link, anywhere: columns, links, legalLinks\n{ label: \"Pricing\", href: \"/pricing\", external?: true } // external opens a new tab\n\n// columns\n[{ title: \"Product\", links: [link, …] }, …] // up to 5 across; 2 on a phone\n\n// socials\n[{ network: \"x\", href: \"https://x.com/fieldline\", label? }, …]\n```\n\n`network` is one of `x`, `linkedin`, `github`, `youtube`, `instagram`,\n`facebook`, `google`, `apple`, `microsoft`, `email`, `rss`. Anything\nelse needs an `icon` (a kit Icon name) and a `label`. Each is an icon\nbutton, named to a screen reader (\"X\", \"LinkedIn\"), opening a new tab.\n\n## Props\n\n- `layout` — `columns` (default) · `simple` · `centered`.\n- `brand` — a node: your logo, linked home if you like.\n- `tagline` — a line under the brand (columns and centred).\n- `columns` — link columns (columns layout).\n- `links` — one row of links (simple and centred; under the socials in\n columns).\n- `socials` — as above.\n- `newsletter` — `{ onSubscribe, title?, body?, label?, placeholder?,\n consent?, look?, doneTitle?, doneBody? }`. `onSubscribe(email, {\n consent })` is async and returns `{ ok: true }` or `{ ok: false,\n error? }`; without it there is no sign-up. `consent` is the checkbox's\n label (a string or a node with a link), and must be ticked. `look`:\n `inline` (default: a column beside the links) · `card` (a full-width\n card under them). `label` is the button, default \"Subscribe\".\n- `cta` — `{ title?, body?, actions: [{ label, href?, onClick?, icon?,\n variant? }] }`: \"Sign in\", \"Get the app\". A band above the legal row;\n the first action is primary unless it says otherwise.\n- `statement` — a large line at the top: a string, or `{ text, by }` for\n a quote.\n- `locales` — `[{ id, label, options: [{ value, label }], value?,\n defaultValue?, onChange? }]`: language, currency, country selects in\n the legal row. Uncontrolled unless you pass `value`.\n- `version` — \"v4.2.0\": a code badge in the legal row.\n- `legal` — the copyright line: \"© 2026 Fieldline Ltd.\". Pass the year;\n the block does not read the clock.\n- `legalLinks` — privacy, terms, cookies.\n- `onNavigate(link, event)` — called when any link or a `cta` action with\n an `href` is followed, before navigation; `event.preventDefault()`\n stops it.\n- `label` — what a screen reader calls the footer and its link row.\n Default \"Site\".\n- `className` — the footer's spacing. Never colour.\n\n## Example\n\n```jsx\n// components/SiteFooter.jsx\n\"use client\";\nimport { Icon } from \"./ui\";\nimport { Footer } from \"./blocks/footer\";\nimport { insertRow } from \"../lib/actions\";\n\nexport default function SiteFooter() {\n return (\n <Footer\n brand={<span className=\"inline-flex items-center gap-2\"><Icon name=\"zap\" /> Fieldline</span>}\n tagline=\"The pipeline small sales teams actually keep up to date.\"\n columns={[\n { title: \"Product\", links: [{ label: \"Pricing\", href: \"/pricing\" }, { label: \"Changelog\", href: \"/changelog\" }] },\n { title: \"Company\", links: [{ label: \"About\", href: \"/about\" }, { label: \"Careers\", href: \"/careers\" }] },\n ]}\n socials={[{ network: \"x\", href: \"https://x.com/fieldline\" }, { network: \"linkedin\", href: \"https://linkedin.com/company/fieldline\" }]}\n newsletter={{\n body: \"One email a month.\",\n consent: \"I agree to the privacy policy.\",\n onSubscribe: (email) => insertRow({ source: \"subscribers\", values: { email } }), // { ok, id } or { ok: false, error }\n }}\n legal=\"© 2026 Fieldline Ltd. All rights reserved.\"\n legalLinks={[{ label: \"Privacy\", href: \"/privacy\" }, { label: \"Terms\", href: \"/terms\" }]}\n />\n );\n}\n```\n\nIn a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.\n\n## What it will not do\n\n- Change the language or currency by itself: `locales[].onChange` is\n yours.\n- Stick to the bottom of a short page: that is your layout's. The kit's\n `AppLayout` content is a flex column, so a `flex-1` above the footer\n pushes it down on a short page and lets it follow a long one.\n- Paint itself in the primary colour: the look is the workspace\n theme's.\n- Draw store badges: \"Get the app\" is two buttons with the Apple and\n Google marks.\n\nA footer it cannot be is your own component in `components/`, built\nfrom the kit — not an edit to this block. Its files are fixed: `npm run\ncheck` refuses a change, and `makersclaw add footer` restores them.\n\n## Credits\n\nLayouts ported from Creative Tim UI's footer blocks\n(https://github.com/creativetimofficial/ui, MIT), rewritten on the\nMakersclaw kit.\n"
|
|
29
29
|
}
|
|
30
30
|
],
|
|
31
31
|
"categories": [
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"docs": "Installed to components/makersclaw/blocks/footer. Import: import { Footer } from \"@/components/makersclaw/blocks/footer\". Props and an example: components/makersclaw/blocks/footer/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
46
46
|
"meta": {
|
|
47
47
|
"makersclaw": {
|
|
48
|
-
"version": "0.
|
|
48
|
+
"version": "0.8.1",
|
|
49
49
|
"useWhen": "The bottom of every page of a public site: a marketing site, a docs hub, a shop, a customer portal. Not for an app's own status bar or a toolbar pinned to the bottom of a screen."
|
|
50
50
|
}
|
|
51
51
|
}
|
package/r/gantt.json
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"title": "Gantt",
|
|
6
6
|
"description": "The Gantt view of work, like Linear's timeline or TeamGantt: a task list on the left (groups that fold, subtasks indented under a summary bar) beside a timeline with a two-tier date header, bars coloured by project or status with their progress, milestones as diamonds, dependency arrows (red when the dates disagree), a today line and weekend shading. Drag a bar to move it, an edge to change its start or end, the dot past its end onto another bar to link them; tasks waiting on a moved one slide along while you drag and the finish date shows what the move cost. Zoom day, week, month or quarter; highlight the critical path; click an empty row to schedule an undated task; quick-add per group; arrow keys move a focused bar; search, filters, group and sort from the shared toolbar. One async onChange saves every change; failures roll back with a notice.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
9
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json",
|
|
9
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/task-model.json"
|
|
10
10
|
],
|
|
11
11
|
"dependencies": [],
|
|
12
12
|
"files": [
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"docs": "Installed to components/makersclaw/blocks/gantt. Import: import { Gantt, ZOOMS, ganttRange, headerTiers, planMove, afterBlockersPatch, isMilestone, summarySpan, arrowPath } from \"@/components/makersclaw/blocks/gantt\". Props and an example: components/makersclaw/blocks/gantt/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
64
64
|
"meta": {
|
|
65
65
|
"makersclaw": {
|
|
66
|
-
"version": "0.
|
|
66
|
+
"version": "0.8.1",
|
|
67
67
|
"useWhen": "A screen that plans dated work and shows what waits on what: a launch plan, a project schedule, a content calendar with lead times, an office move, a release. Not for an undated to-do list (task-list), stage columns (board), or days on a month grid (calendar)."
|
|
68
68
|
}
|
|
69
69
|
}
|
package/r/job-panel.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"title": "JobPanel",
|
|
6
6
|
"description": "The panel that follows work the founder asked the app's agent for (useJob): each step's label as it runs, Stop, \"Needs your answer\" with Answer while the agent waits on the founder, \"Queued · after …\" while it waits its turn, then done or the reason it stopped — and nothing on screen when no job runs. variant=\"generator\" wraps it in a prompt, the surface's options, Generate and a results shelf, with no model, provider or API key of its own.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json"
|
|
9
9
|
],
|
|
10
10
|
"dependencies": [],
|
|
11
11
|
"files": [
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"docs": "Installed to components/makersclaw/blocks/job-panel. Import: import { JobPanel } from \"@/components/makersclaw/blocks/job-panel\". Props and an example: components/makersclaw/blocks/job-panel/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
53
53
|
"meta": {
|
|
54
54
|
"makersclaw": {
|
|
55
|
-
"version": "0.
|
|
55
|
+
"version": "0.8.1",
|
|
56
56
|
"useWhen": "A surface has a verb that asks the agent to make or find something (\"Generate a launch plan\", \"Find 20 leads\", \"Plan next sprint\") and the founder should watch it happen where the result will land. variant=\"generator\" when the screen IS the place to ask: a prompt and a few options, the results under them (product photos, ad copy, names)."
|
|
57
57
|
}
|
|
58
58
|
}
|
package/r/json-editor.json
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"title": "JsonEditor",
|
|
6
6
|
"description": "A JSON editor in the workspace theme with two views of one value: a Tree of folding rows (edit a value or rename a key in place, add keys and items, change a type, copy a path, delete with Undo, arrow keys throughout) and Code (the code-editor block in JSON mode). Checked as it changes: the status line says what is wrong in plain words and which line, and clicking it goes there. A Format button, copy as pretty or one-line JSON, a friendly empty state with starters, and an optional Save that is optimistic and rolls back with a notice when it fails.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
9
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json",
|
|
9
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/code-editor.json"
|
|
10
10
|
],
|
|
11
11
|
"dependencies": [],
|
|
12
12
|
"files": [
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"docs": "Installed to components/makersclaw/blocks/json-editor. Import: import { JsonEditor, JsonTree, typeOf, pathLabel, getAt, setAt, removeAt, insertAt, renameKey, flattenTree, expandedTo, summary, preview } from \"@/components/makersclaw/blocks/json-editor\". Props and an example: components/makersclaw/blocks/json-editor/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
60
60
|
"meta": {
|
|
61
61
|
"makersclaw": {
|
|
62
|
-
"version": "0.
|
|
62
|
+
"version": "0.8.1",
|
|
63
63
|
"useWhen": "A screen edits or inspects a JSON value: an integration's settings, a webhook payload, a record's metadata or custom fields column (jsonb), an API response, feature flags. Not for editing SQL or other code (code-editor), a form of known fields (record-form), or a table of rows (data-table)."
|
|
64
64
|
}
|
|
65
65
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "library-shelf",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "LibraryShelf",
|
|
6
|
+
"description": "A shelf of starting points from the Library, in a few named columns (sales processes, follow-up emails, saved searches): each item its mark or cover, its name, one muted fact (\"6 stages\", \"PDF\") and a Use at its end. Use goes out through an async callback that answers { ok }; the item says it is being used, then Used, and the reason in place when it fails. It takes sections you shaped, or what useLibraryItems() returns (lib/hooks.js) with onUse(item): the collection's items with their covers, Show more, \"N more come with a paid plan\" on a preview, locked items without a Use, and a quiet \"not connected yet\".",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": [],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "registry/blocks/library-shelf/index.js",
|
|
14
|
+
"type": "registry:component",
|
|
15
|
+
"target": "@components/makersclaw/blocks/library-shelf/index.js",
|
|
16
|
+
"content": "// The LibraryShelf block — `makersclaw add library-shelf`. Fixed: compose\n// it through its props (README.md beside this file).\nexport { LibraryShelf, libraryShelfItems } from \"./library-shelf\";\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "registry/blocks/library-shelf/library-shelf.jsx",
|
|
20
|
+
"type": "registry:component",
|
|
21
|
+
"target": "@components/makersclaw/blocks/library-shelf/library-shelf.jsx",
|
|
22
|
+
"content": "\"use client\";\n\n// A shelf of starting points from the Library, in named columns, each\n// with a Use (the app kit, D15; grown in the CRM, 0.7.0). Two ways in:\n// `sections` you shaped, or `library` — what useLibraryItems() returns\n// (lib/hooks.js) — with `onUse(item)`, which draws one column of the\n// collection's items, each with its cover, and the hook's paging, preview\n// and \"not connected yet\". Props in, callbacks out; the block never reads\n// the Library itself. Tokens only.\n\nimport { useState } from \"react\";\nimport { Button, Icon, Image, Skeleton, cx } from \"../../ui\";\n\n/** A Library item's format, as a word and a glyph. */\nconst FORMATS = {\n pdf: { word: \"PDF\", icon: \"file\" },\n image: { word: \"Picture\", icon: \"image\" },\n html: { word: \"Web page\", icon: \"globe\" },\n slides: { word: \"Slides\", icon: \"layers\" },\n document: { word: \"Document\", icon: \"file\" },\n sheet: { word: \"Sheet\", icon: \"table\" },\n archive: { word: \"Archive\", icon: \"archive\" },\n};\n\n/** Library items (useLibraryItems' `items`) as shelf items. */\nexport function libraryShelfItems(items, { onUse, useLabel, once } = {}) {\n return (items ?? []).filter(Boolean).map((item) => {\n const f = FORMATS[item.format] ?? { word: null, icon: \"file\" };\n return {\n id: item.id,\n label: item.title ?? \"Untitled\",\n meta: item.locked ? \"Paid plan\" : f.word,\n mark: item.preview_url ? (\n <Image src={item.preview_url} alt=\"\" ratio=\"square\" rounded=\"sm\" className=\"size-7 shrink-0\" />\n ) : (\n <Icon name={f.icon} size={14} className=\"shrink-0 text-muted-foreground\" />\n ),\n locked: Boolean(item.locked),\n onUse: onUse && !item.locked ? () => onUse(item) : undefined,\n useLabel,\n once,\n };\n });\n}\n\nfunction ShelfItem({ item }) {\n const [state, setState] = useState(item.used ? \"used\" : \"idle\");\n const [error, setError] = useState(null);\n async function use() {\n if (!item.onUse || state === \"busy\") return;\n setState(\"busy\");\n setError(null);\n let r;\n try {\n r = await item.onUse();\n } catch (e) {\n r = { ok: false, error: e?.message ?? String(e) };\n }\n if (r && r.ok === false) {\n setState(\"idle\");\n setError(r.error || \"Couldn't use it. Try again.\");\n return;\n }\n setState(item.once === false ? \"idle\" : \"used\");\n }\n const used = state === \"used\" || item.used;\n return (\n <li className=\"grid grid-cols-1 min-w-0 gap-0.5 border-b border-border py-1 last:border-b-0\" data-shelf-item>\n <div className=\"flex min-h-[30px] min-w-0 items-center gap-2\">\n {item.mark ?? null}\n {/* The meta gives way, to an ellipsis, before the label does. It was\n shrink-0, and a long meta ran the label down to nothing (UI\n review X3). Neither may be wider than the row. */}\n <span className=\"min-w-0 truncate\" title={typeof item.label === \"string\" ? item.label : undefined} data-shelf-label>\n {item.label}\n </span>\n {item.meta ? (\n <span className=\"min-w-0 shrink-[4] truncate text-xs text-muted-foreground\" title={typeof item.meta === \"string\" ? item.meta : undefined}>\n {item.meta}\n </span>\n ) : null}\n <span className=\"ml-auto shrink-0\">\n {item.locked ? (\n <Icon name=\"lock\" size={12} className=\"text-muted-foreground\" label=\"Comes with a paid plan\" />\n ) : used ? (\n <span className=\"inline-flex items-center gap-1 text-xs text-success\">\n <Icon name=\"check\" size={12} />\n {item.usedLabel ?? \"Used\"}\n </span>\n ) : item.onUse ? (\n <Button size=\"sm\" variant=\"quiet\" loading={state === \"busy\"} onClick={use} aria-label={`${item.useLabel ?? \"Use\"}: ${typeof item.label === \"string\" ? item.label : \"this\"}`}>\n {item.useLabel ?? \"Use\"}\n </Button>\n ) : null}\n </span>\n </div>\n {item.description ? <p className=\"m-0 min-w-0 break-words pb-1 text-xs text-muted-foreground\">{item.description}</p> : null}\n {error ? (\n <p role=\"alert\" className=\"pb-1 text-xs text-destructive\">\n {error}\n </p>\n ) : null}\n </li>\n );\n}\n\n/** What a library read says under its items: loading, more, a preview, refusals. */\nfunction LibraryFoot({ library, count }) {\n const l = library ?? {};\n if (l.unavailable) return <p className=\"text-[13px] text-muted-foreground\">The Library is not connected yet.</p>;\n if (l.error) return <p role=\"alert\" className=\"text-[13px] text-destructive\">{l.error}</p>;\n const hidden = l.previewOnly && typeof l.total === \"number\" ? l.total - count : 0;\n return (\n <>\n {hidden > 0 ? (\n <p className=\"flex items-center gap-1.5 text-[13px] text-muted-foreground\">\n <Icon name=\"lock\" size={12} className=\"shrink-0\" />\n {hidden} more come with a paid plan.\n </p>\n ) : null}\n {l.hasMore && l.loadMore ? (\n <Button size=\"sm\" variant=\"quiet\" className=\"justify-self-start\" loading={l.loadingMore} onClick={() => l.loadMore()}>\n Show more\n </Button>\n ) : null}\n </>\n );\n}\n\n/**\n * The shelf.\n *\n * title \"Start from a template\"\n * source \"CRM starting points, from the Library\"\n * icon a kit Icon name before the title (default \"layers\")\n * sections [{ key, title, items: [{ id, label, meta?, description?, mark?, onUse?, useLabel?,\n * used?, usedLabel?, once?, locked? }] }]\n * meta is ONE short fact (\"6 stages\", \"PDF\"); it truncates, and\n * so does the label, so no row is ever wider than its box. A\n * sentence is a `description`: it sits under the label and wraps.\n *\n * Or straight from the Library:\n *\n * library what useLibraryItems(id) returns ({ items, total,\n * previewOnly, hasMore, loadMore, loading, loadingMore,\n * error, unavailable })\n * onUse(item) async → { ok } | { ok: false, error }: what Use does\n * with a Library item (ask the agent to copy it in, open\n * it as a draft); a locked item has no Use\n * sectionTitle the column's title (default none)\n * useLabel default \"Use\"; once — false: an item can be used again\n * empty the line when the collection has nothing\n */\nexport function LibraryShelf({\n title = \"Start from a template\",\n source,\n icon = \"layers\",\n sections: given,\n library,\n onUse,\n sectionTitle,\n useLabel,\n once,\n empty = \"Nothing on this shelf yet.\",\n className,\n}) {\n const fromLibrary = !given && Boolean(library);\n const libItems = fromLibrary ? libraryShelfItems(library.items, { onUse, useLabel, once }) : [];\n const sections = given ?? (fromLibrary && libItems.length ? [{ key: \"library\", title: sectionTitle, items: libItems }] : []);\n const loading = fromLibrary && library.loading && !libItems.length;\n return (\n <section data-library-shelf aria-label={title} className={cx(\"grid grid-cols-1 min-w-0 gap-3\", className)}>\n <div className=\"flex flex-wrap items-center gap-2 text-[13px]\">\n <Icon name={icon} size={16} className=\"text-muted-foreground\" />\n <b className=\"font-semibold\">{title}</b>\n {source ? <span className=\"text-muted-foreground\">· {source}</span> : null}\n </div>\n {loading ? (\n <div aria-busy=\"true\" aria-label=\"Loading\" className=\"grid grid-cols-1 gap-2 rounded-[var(--radius)] border border-border px-3.5 py-3\">\n <Skeleton className=\"w-2/3\" />\n <Skeleton className=\"w-1/2\" />\n <Skeleton className=\"w-3/5\" />\n </div>\n ) : fromLibrary && !libItems.length && !library.unavailable && !library.error ? (\n <p className=\"text-[13px] text-muted-foreground\">{empty}</p>\n ) : null}\n {sections.length ? (\n <>\n {/* Side by side by the shelf's own width, not the window's: an app on\n the desk is often 800px with a rail beside it, where three columns\n cut every name to its first letter (CRM 0.7.1). */}\n <div className=\"@container min-w-0\">\n <div className=\"grid grid-cols-1 min-w-0 rounded-[var(--radius)] border border-border @3xl:grid-cols-[repeat(auto-fit,minmax(14rem,1fr))]\">\n {sections.map((s) => (\n <div key={s.key ?? s.title} className=\"grid grid-cols-1 min-w-0 content-start gap-0.5 border-b border-border px-3.5 py-3 last:border-b-0 @3xl:border-b-0 @3xl:border-r @3xl:last:border-r-0\">\n {s.title ? <h3 className=\"text-xs font-semibold text-muted-foreground\">{s.title}</h3> : null}\n <ul className=\"m-0 grid grid-cols-1 list-none p-0 text-[13px]\">\n {s.items.map((it) => (\n <ShelfItem key={it.id} item={it} />\n ))}\n </ul>\n </div>\n ))}\n </div>\n </div>\n </>\n ) : null}\n {fromLibrary ? <LibraryFoot library={library} count={libItems.length} /> : null}\n </section>\n );\n}\n"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "registry/blocks/library-shelf/probe-rules.js",
|
|
26
|
+
"type": "registry:component",
|
|
27
|
+
"target": "@components/makersclaw/blocks/library-shelf/probe-rules.js",
|
|
28
|
+
"content": "// The layout rules this block ships for the store's probe (makersclaw-store\n// scripts/probe.mjs --suite); see top-bar/probe-rules.js. Nothing imports this.\nexport default {\n block: \"library-shelf\",\n rules: [\n {\n id: \"label-never-cut-to-nothing\",\n kind: \"visible-text\",\n note: \"UI review X3: the meta ran the label down to a letter\",\n scope: \"[data-library-shelf]\",\n target: \"[data-shelf-label], li > div > span.truncate:not(.text-xs)\",\n min: 3,\n },\n ],\n};\n"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"path": "registry/blocks/library-shelf/README.md",
|
|
32
|
+
"type": "registry:file",
|
|
33
|
+
"target": "@components/makersclaw/blocks/library-shelf/README.md",
|
|
34
|
+
"content": "# LibraryShelf\n\n\"Start from a template\": a shelf of starting points from the Library, in\na few named columns. Each item is its mark or cover, its name, one muted\nfact (\"6 stages\", \"PDF\") and a **Use** at its end.\n\nUse is your async callback. While it runs the button spins; then the\nitem says **Used**; a failure shows its reason under the item. A heading\nnames the shelf and where its items come from.\n\nGrown in the CRM (0.7.0) as \"Start from a template · CRM starting points,\nfrom the Library\". Every app ships its first Library collection; this is\nwhere it shows.\n\n```jsx\nimport { LibraryShelf } from \"../components/blocks/library-shelf\"; // a Makersclaw app (\"./blocks/library-shelf\" from components/)\nimport { LibraryShelf } from \"@/components/makersclaw/blocks/library-shelf\"; // a shadcn project\n```\n\nA client component. The Example is at the end.\n\n## Which hook feeds it\n\n```js\nconst library = useLibraryItems(\"crm-starting-points\"); // lib/hooks.js\n// → { items, total, previewOnly, hasMore, loadMore, loading, loadingMore, error, unavailable, … }\n```\n\n`useLibraryItems(id)` reads `GET _claw/library/collections/<id>/items`:\na files collection's items, `{ id, title, description, tags, format,\npreview_url, locked, … }`, a page at a time. Give the whole answer to\n`library`. On a Free plan a paid collection is a preview: the shelf says\nhow many more come with a paid plan, and a `locked` item has no Use.\n\n## Props\n\nTwo ways in: `library` with `onUse(item)`, or `sections` you shaped.\n\n### From the Library\n\n- `library` — what `useLibraryItems()` returns. The shelf draws one\n column of its items, each with its cover (`preview_url`) or its\n format's glyph, and the format as its fact.\n- `onUse(item)` — async, answers `{ ok: true }` or `{ ok: false, error }`\n (a throw counts as failed). What Use does with a Library item: ask the\n agent to copy it in, open it as a draft. The Library door never writes;\n a copy into the workspace is the agent's.\n- `sectionTitle` — the column's title. Default none.\n- `useLabel` — default \"Use\". `once` — `false`: an item can be used again.\n- `empty` — the line when the collection has nothing. Default \"Nothing on\n this shelf yet.\"\n\nUnder the items: \"Show more\" when `hasMore`, \"N more come with a paid\nplan\" on a preview, the error's words, or \"The Library is not connected\nyet.\" Skeleton lines while the first page is out.\n\n### Sections you shaped\n\n- `sections` — `[{ key, title, items }]`, side by side from 48rem of the\n shelf's own width, stacked below. An item:\n - `id`, `label` — the name.\n - `meta` — one short muted fact after it (\"6 stages\"). It truncates, and\n the label too: no row is wider than its box. Never a sentence.\n - `description` — a sentence under the label, muted, wrapping onto as\n many lines as it needs (\"Turn a developer's line into a customer's\").\n - `mark` — a node before it (a stage's dot, an icon).\n - `onUse()` — async `{ ok }`. Without it the item has no Use.\n - `useLabel` — default \"Use\". `used` — already used. `usedLabel` —\n default \"Used\". `once: false` — usable again.\n - `locked` — a lock instead of Use.\n\n`libraryShelfItems(items, { onUse, useLabel, once })` turns Library items\ninto shelf items, for a shelf of several collections.\n\n### Both\n\n- `title` — default \"Start from a template\".\n- `source` — where the items come from (\"CRM starting points, from the\n Library\"), muted after the title.\n- `icon` — a kit `Icon` name before the title. Default `\"layers\"`.\n- `className` — layout around it. Never colour.\n\n## Who may do what\n\nAny member who can open the app reads the shelf. A paid collection on a\nFree plan shows its preview; the Upgrade is on the desk's Library window,\nnever in the app.\n\n## Never\n\n- Never read the Library inside the block: the items come in as props.\n- Never keep a `file_url`: Use asks for the item when it is pressed.\n- Never offer an Upgrade or a price here: money is the desk's.\n\n## Example\n\nThe CRM's starting points on an empty board, straight from its\ncollection.\n\n```jsx\n\"use client\";\nimport { ask } from \"../lib/platform\";\nimport { useLibraryItems } from \"../lib/hooks\";\nimport { LibraryShelf } from \"../components/blocks/library-shelf\";\n\nexport default function StartFrom() {\n const library = useLibraryItems(\"crm-starting-points\", { limit: 12 });\n return (\n <LibraryShelf\n source=\"CRM starting points, from the Library\"\n library={library}\n sectionTitle=\"Sales processes\"\n onUse={(item) => ask({ text: `Set up the sales process \"${item.title}\" from the Library (item ${item.id}).` })}\n />\n );\n}\n```\n"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"categories": [
|
|
38
|
+
"library",
|
|
39
|
+
"templates",
|
|
40
|
+
"starting points",
|
|
41
|
+
"shelf",
|
|
42
|
+
"empty state",
|
|
43
|
+
"use",
|
|
44
|
+
"collection",
|
|
45
|
+
"onboarding",
|
|
46
|
+
"examples",
|
|
47
|
+
"start from"
|
|
48
|
+
],
|
|
49
|
+
"docs": "Installed to components/makersclaw/blocks/library-shelf. Import: import { LibraryShelf, libraryShelfItems } from \"@/components/makersclaw/blocks/library-shelf\". Props and an example: components/makersclaw/blocks/library-shelf/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
50
|
+
"meta": {
|
|
51
|
+
"makersclaw": {
|
|
52
|
+
"version": "0.8.1",
|
|
53
|
+
"useWhen": "An empty or new screen that offers the app's own starting points from the Library (\"Start from a template\"): a CRM's starting processes and follow-up emails, a planner's templates, a deck app's starter decks, a growth app's examples. Not for a gallery of large previews (a card grid over useLibraryItems), a picker inside a form (Combobox) or the records themselves."
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
package/r/map.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"title": "MapView",
|
|
6
6
|
"description": "An interactive map (MapLibre) in the workspace theme: places from rows as markers in token tones or your own kit content, clustered when they crowd, a popup with kit parts, a searchable list beside the map that shares one selection and hover with it, fit-to-all, zoom controls, draggable markers with an optimistic save, and tinted areas for territories. Light and dark tiles follow the workspace scheme (OpenFreeMap by default, no key); `styleUrl` swaps them. Helpers for bounds, clustering and distances run anywhere.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json"
|
|
9
9
|
],
|
|
10
10
|
"dependencies": [
|
|
11
11
|
"maplibre-gl@6.11.1",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"docs": "Installed to components/makersclaw/blocks/map. Import: import { MapView, useMapView, MAP_STYLES, DEFAULT_STYLE, TONES, readPosition, boundsOf, inBounds, clusterPoints, zoomToReveal, distanceKm, formatDistance, sortByDistance } from \"@/components/makersclaw/blocks/map\". Props and an example: components/makersclaw/blocks/map/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
83
83
|
"meta": {
|
|
84
84
|
"makersclaw": {
|
|
85
|
-
"version": "0.
|
|
85
|
+
"version": "0.8.1",
|
|
86
86
|
"useWhen": "A screen shows records that have a place: customers, stores, offices, field visits, deliveries, events, territories. The location-map and store-locator blocks and the Map view of work-views are built on it. Not for a chart of numbers by region (chart) or a hand-drawn diagram (sketch-canvas, flow-canvas)."
|
|
87
87
|
}
|
|
88
88
|
}
|