makersclaw 0.8.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 +9 -3
- 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 +3 -3
- 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 +2 -2
- package/r/chat-pane.json +84 -0
- package/r/chat.json +2 -2
- 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 +13 -7
- 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 +2 -2
- package/r/flow-editor.json +79 -0
- package/r/footer.json +2 -2
- 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 +3 -3
- 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 +3 -3
- package/r/pace-bar.json +69 -0
- package/r/page-builder.json +2 -2
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +2 -2
- 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 +4086 -1512
- 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 +2 -2
- package/r/signature-pad.json +2 -2
- package/r/sketch-canvas.json +2 -2
- 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 +3 -3
- 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/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/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-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/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/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/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/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/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/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/install.mjs +77 -16
- package/src/registry.mjs +3 -0
- package/templates/next/browser/lib/data.js +6 -1
- package/templates/next/files/README.md +627 -24
- 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 +968 -9
- 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 +2 -1
- package/templates/next/files/components/ui/layout.jsx +77 -2
- 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 +241 -5
- package/templates/next/files/lib/door.js +92 -0
- package/templates/next/files/lib/hooks.js +1627 -8
- package/templates/next/files/lib/platform.js +529 -21
- package/templates/next/template.json +13 -3
|
@@ -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.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.1/r/core.json"
|
|
9
9
|
],
|
|
10
10
|
"dependencies": [],
|
|
11
11
|
"files": [
|
|
@@ -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.8.
|
|
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.8.
|
|
9
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.
|
|
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.8.
|
|
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.
|
|
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.8.
|
|
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.8.
|
|
9
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.
|
|
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.8.
|
|
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.
|
|
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.8.
|
|
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
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "margin-notes",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "MarginNotes",
|
|
6
|
+
"description": "Passages of text with notes in the margin, level with the words they are about (stacked under each passage when the container is narrow). Four kinds of note, each with its tone and its buttons: source (where the words came from: a title, a line of detail, the words read), flag (\"No source. Check this line.\" with It's true, Cut it, Ask where it came from), cut (the words struck through, with the reason, Put back, or a lock when nobody may put them back) and comment. The words a note is about are marked in the passage; the selected passage and its note are linked, and J and K walk the notes that still need a person. A settled note folds to one line with what was decided and a Reopen. Saves go out through one callback, onAction(note, actionId).",
|
|
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/agent-marks.json"
|
|
10
|
+
],
|
|
11
|
+
"dependencies": [],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "registry/blocks/margin-notes/index.js",
|
|
15
|
+
"type": "registry:component",
|
|
16
|
+
"target": "@components/makersclaw/blocks/margin-notes/index.js",
|
|
17
|
+
"content": "// The MarginNotes block — `makersclaw add margin-notes`. Fixed: compose it\n// through its props (README.md beside this file). The model's helpers are\n// plain functions; they work on the server too.\nexport { MarginNotes } from \"./margin-notes\";\nexport {\n MARGIN_KINDS,\n TONES,\n MARKS,\n resolveKinds,\n noteLook,\n actionsOf,\n isSettled,\n arrangeNotes,\n walkNext,\n cycleNote,\n findQuote,\n inlineMarks,\n countNotes,\n countWords,\n} from \"./margin-model\";\n"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"path": "registry/blocks/margin-notes/margin-notes.jsx",
|
|
21
|
+
"type": "registry:component",
|
|
22
|
+
"target": "@components/makersclaw/blocks/margin-notes/margin-notes.jsx",
|
|
23
|
+
"content": "\"use client\";\n\n// Passages of text with notes in the margin, level with their words. Each\n// passage is a row: the words on one side, the notes about them on the\n// other (stacked under the words when the container is narrow). A note says\n// where the words came from (source), that they need a look (flag), that\n// they were cut and why (cut), or what someone thought (comment); an app\n// can add kinds. The words a note is about are marked in the passage, and\n// the selected note and its passage are linked: click either, or press J\n// and K to walk the notes that still need a person.\n//\n// Props in, callbacks out: the block never reads or writes data. A press on\n// a note's button calls onAction(note, actionId); the app does the work and\n// hands back changed notes. README.md beside this file is the contract.\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { Badge, Button, Empty, Icon, Kbd, Notice, Skeleton, cx } from \"../../ui\";\nimport { AgentMark } from \"../agent-marks\";\nimport { actionsOf, arrangeNotes, countNotes, countWords, cycleNote, inlineMarks, isSettled, noteLook, resolveKinds, walkNext } from \"./margin-model\";\n\n// Literal class names, so Tailwind sees every one of them.\nconst GRID = {\n right: {\n sm: \"@2xl:grid-cols-[minmax(0,1fr)_15rem]\",\n md: \"@2xl:grid-cols-[minmax(0,1fr)_18rem]\",\n lg: \"@2xl:grid-cols-[minmax(0,1fr)_22rem]\",\n },\n left: {\n sm: \"@2xl:grid-cols-[15rem_minmax(0,1fr)]\",\n md: \"@2xl:grid-cols-[18rem_minmax(0,1fr)]\",\n lg: \"@2xl:grid-cols-[22rem_minmax(0,1fr)]\",\n },\n};\n\nconst TEXT = { sm: \"text-sm leading-relaxed\", md: \"text-base leading-relaxed\", lg: \"text-lg leading-relaxed\" };\n\n// The coloured bar down a note's inline-start edge.\nconst BAR = {\n neutral: \"bg-muted-foreground\",\n primary: \"bg-primary\",\n info: \"bg-info\",\n success: \"bg-success\",\n warning: \"bg-warning\",\n danger: \"bg-destructive\",\n};\nconst BAR_HUE = { 1: \"bg-chart-1\", 2: \"bg-chart-2\", 3: \"bg-chart-3\", 4: \"bg-chart-4\", 5: \"bg-chart-5\" };\n\nconst DECO = {\n neutral: \"decoration-muted-foreground\",\n primary: \"decoration-primary\",\n info: \"decoration-info\",\n success: \"decoration-success\",\n warning: \"decoration-warning\",\n danger: \"decoration-destructive\",\n};\nconst DECO_HUE = { 1: \"decoration-chart-1\", 2: \"decoration-chart-2\", 3: \"decoration-chart-3\", 4: \"decoration-chart-4\", 5: \"decoration-chart-5\" };\n\nconst WASH = {\n neutral: \"bg-muted\",\n primary: \"bg-primary/15\",\n info: \"bg-info/15\",\n success: \"bg-success/15\",\n warning: \"bg-warning/15\",\n danger: \"bg-destructive/15\",\n};\nconst WASH_HUE = { 1: \"bg-chart-1/15\", 2: \"bg-chart-2/15\", 3: \"bg-chart-3/15\", 4: \"bg-chart-4/15\", 5: \"bg-chart-5/15\" };\n\nconst deco = (look) => (look.hue ? DECO_HUE[look.hue] : DECO[look.tone]);\nconst wash = (look) => (look.hue ? WASH_HUE[look.hue] : WASH[look.tone]);\n\nconst domId = (root, kind, id) => `${root}-${kind}-${String(id).replace(/[^a-zA-Z0-9_-]/g, \"_\")}`;\n\nconst reducedMotion = () => typeof window !== \"undefined\" && window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches;\n\n/** Words typed into, or a key meant for something else: J and K are left alone. */\nfunction isTyping(el) {\n if (!el || el.nodeType !== 1) return false;\n return Boolean(el.closest?.(\"input, textarea, select, [contenteditable=''], [contenteditable='true']\"));\n}\n\n/** The words of a passage, with the marks the notes make on them. */\nfunction Words({ passage, notes, kinds, selectedId, rootId, as: Tag = \"p\" }) {\n const looks = useMemo(() => new Map(notes.map((n) => [n.id, noteLook(n, kinds)])), [notes, kinds]);\n const { segments } = useMemo(() => inlineMarks(passage.text, notes, (n) => looks.get(n.id)?.mark ?? \"none\"), [passage.text, notes, looks]);\n if (passage.node != null) {\n const struck = notes.find((n) => !isSettled(n) && looks.get(n.id)?.mark === \"strike\");\n const look = struck ? looks.get(struck.id) : null;\n return <div className={cx(\"min-w-0 break-words\", look && cx(\"text-muted-foreground line-through decoration-2\", deco(look)))}>{passage.node}</div>;\n }\n return (\n <Tag className={cx(\"m-0 min-w-0 break-words\", Tag !== \"p\" && \"text-[1.05em] font-semibold leading-snug\")}>\n {segments.map((seg, i) => {\n if (!seg.ids.length) return <span key={i}>{seg.text}</span>;\n const id = seg.ids.includes(selectedId) ? selectedId : seg.ids[0];\n const look = looks.get(id);\n const on = seg.ids.includes(selectedId);\n const common = { \"data-note\": id, \"data-selected\": on ? \"\" : undefined, \"aria-describedby\": domId(rootId, \"note\", id) };\n if (look.mark === \"strike\")\n return (\n <del key={i} {...common} className={cx(\"rounded-sm text-muted-foreground line-through decoration-2\", deco(look), on && wash(look))}>\n <span className=\"sr-only\">{look.label}: </span>\n {seg.text}\n </del>\n );\n return (\n <mark\n key={i}\n {...common}\n className={cx(\n \"rounded-sm text-inherit\",\n look.mark === \"highlight\" || on ? wash(look) : \"bg-transparent\",\n look.mark === \"underline\" && \"underline decoration-dotted decoration-2 underline-offset-4\",\n look.mark === \"wavy\" && \"underline decoration-wavy decoration-1 underline-offset-4\",\n look.mark !== \"highlight\" && deco(look),\n )}\n >\n <span className=\"sr-only\">{look.label}: </span>\n {seg.text}\n </mark>\n );\n })}\n </Tag>\n );\n}\n\n/** One note. */\nfunction Note({ note, kinds, selected, busy, error, readOnly, canAct, onSelect, onPress, rootId, cardRef, quoteShown }) {\n const look = noteLook(note, kinds);\n const settled = isSettled(note);\n const actions = canAct ? actionsOf(note, kinds, { readOnly }) : [];\n const agent = note.by === \"agent\" || note.by?.agent === true;\n const byName = typeof note.by === \"string\" ? (note.by === \"agent\" ? \"\" : note.by) : (note.by?.name ?? \"\");\n const showReopen = settled && canAct && !readOnly && look.reopen;\n\n return (\n <article\n ref={cardRef}\n id={domId(rootId, \"note\", note.id)}\n tabIndex={-1}\n data-note={note.id}\n data-kind={note.kind}\n data-selected={selected ? \"\" : undefined}\n aria-current={selected ? \"true\" : undefined}\n aria-label={note.title && note.title !== look.label ? `${look.label}: ${note.title}` : look.label}\n onClick={() => onSelect(note.id)}\n className={cx(\n \"relative grid min-w-0 gap-1.5 overflow-hidden rounded-lg border border-border py-3 pe-3 ps-4 text-sm text-card-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n selected && \"ring-2 ring-ring\",\n settled ? \"bg-muted\" : \"bg-card\",\n )}\n >\n <span aria-hidden=\"true\" className={cx(\"absolute inset-y-0 start-0 w-1\", look.hue ? BAR_HUE[look.hue] : BAR[look.tone], settled && \"opacity-40\")} />\n {settled ? (\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n <Icon name=\"check\" size={14} className=\"shrink-0 text-success\" />\n <span className=\"text-[13px] font-medium\">{note.answer || \"Settled\"}</span>\n {note.at ? <span className=\"text-xs text-muted-foreground\">{note.at}</span> : null}\n {showReopen ? (\n <Button size=\"sm\" variant=\"link\" tone=\"muted\" loading={busy === \"reopen\"} aria-label={`${look.reopen.label ?? \"Reopen\"}: ${look.label}`} onClick={() => onPress(note, \"reopen\")}>\n {look.reopen.label ?? \"Reopen\"}\n </Button>\n ) : null}\n </div>\n ) : (\n <>\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n <Badge variant=\"soft\" tone={look.hue ? undefined : look.tone} hue={look.hue ?? undefined} className=\"gap-1\">\n <Icon name={look.icon} size={12} aria-hidden=\"true\" />\n {look.label}\n </Badge>\n {agent ? <AgentMark label=\"Raised by the agent\" /> : null}\n {byName ? <span className=\"text-xs text-muted-foreground\">{byName}</span> : null}\n {note.at ? <span className=\"text-xs tabular-nums text-muted-foreground\">{note.at}</span> : null}\n </div>\n {note.title ? <p className=\"m-0 font-medium\">{note.title}</p> : null}\n {note.meta ? <p className=\"m-0 text-xs text-muted-foreground\">{note.meta}</p> : null}\n {quoteShown && note.quote ? <p className=\"m-0 break-words text-[13px] italic text-muted-foreground\">{`“${note.quote}”`}</p> : null}\n {note.text ? <p className=\"m-0 break-words\">{note.text}</p> : null}\n {note.detail ? <p className=\"m-0 mt-0.5 break-words border-t border-dashed border-border pt-1.5 text-[13px] leading-snug text-muted-foreground\">{note.detail}</p> : null}\n {note.href ? (\n <a href={note.href} target=\"_blank\" rel=\"noreferrer\" className=\"inline-flex items-center gap-1 justify-self-start rounded-sm text-xs font-medium text-primary underline-offset-4 hover:underline focus-visible:ring-2 focus-visible:ring-ring\">\n {note.hrefLabel || \"Open the source\"}\n <Icon name=\"external\" size={12} aria-hidden=\"true\" />\n </a>\n ) : null}\n {note.locked ? (\n <p className=\"m-0 flex items-start gap-1.5 text-xs text-muted-foreground\">\n <Icon name=\"lock\" size={12} className=\"mt-0.5 shrink-0\" />\n <span className=\"min-w-0\">{note.locked}</span>\n </p>\n ) : null}\n {actions.length ? (\n <div className=\"flex flex-wrap items-center gap-1.5 pt-0.5\">\n {actions.map((a) => (\n <Button\n key={a.id}\n size=\"sm\"\n variant={a.variant || \"secondary\"}\n icon={a.icon}\n loading={busy === a.id}\n disabled={Boolean(busy) && busy !== a.id}\n aria-label={`${a.label}: ${look.label}${note.title ? `, ${note.title}` : \"\"}`}\n onClick={() => onPress(note, a.id)}\n >\n {a.label}\n </Button>\n ))}\n </div>\n ) : null}\n </>\n )}\n {error ? (\n <p role=\"alert\" className=\"m-0 text-xs text-destructive\">\n {error}\n </p>\n ) : null}\n </article>\n );\n}\n\n/** Rows of grey bars where the passages and their notes will be. */\nfunction Loading({ cols }) {\n return (\n <div role=\"status\" aria-label=\"Loading\" aria-busy=\"true\" className={cx(\"grid grid-cols-[minmax(0,1fr)] gap-x-6 gap-y-4\", cols)}>\n {[0, 1, 2].map((i) => (\n <div key={i} className=\"contents\">\n <div className=\"grid min-w-0 gap-2 py-2\">\n <Skeleton className=\"w-full\" />\n <Skeleton className=\"w-11/12\" />\n <Skeleton className=\"w-2/3\" />\n </div>\n <div className=\"grid min-w-0 content-start py-2\">\n <Skeleton shape=\"rect\" className=\"h-16 w-full\" />\n </div>\n </div>\n ))}\n </div>\n );\n}\n\n/**\n * passages [{ id, text | node, lead?, label?, level? }]: the words. `lead` is a\n * short figure before them (a time, a number); `label` a name above\n * (a speaker, a section); `level` 2 · 3 · 4 draws the text as a heading.\n * notes [{ id, passage, kind, title?, meta?, text?, detail?, quote?, by?, at?,\n * href?, hrefLabel?, locked?, state?, answer?, actions?, tone?, hue?,\n * mark?, label? }]\n * selected / defaultSelected / onSelect(id | null) the selected note\n * onAction(note, actionId) async → { ok } | { ok: false, error }\n * kinds changes to the built-in kinds, or new ones\n * side right (default) · left: which side of the words the margin is on\n * marginWidth sm · md (default) · lg\n * marginTitle the margin's heading\n * showNotes false hides the margin (default true)\n * textSize sm · md (default) · lg\n * status ready (default) · loading · failed, with `error` and onRetry\n * readOnly notes to read: no buttons\n * keys J and K walk the notes (default true)\n */\nexport function MarginNotes({\n passages = [],\n notes = [],\n selected,\n defaultSelected = null,\n onSelect,\n onAction,\n kinds: kindChanges,\n side = \"right\",\n marginWidth = \"md\",\n marginTitle,\n showNotes = true,\n textSize = \"md\",\n status = \"ready\",\n error,\n onRetry,\n readOnly = false,\n keys = true,\n noNotes = \"Nothing in the margin.\",\n orphanTitle = \"Not tied to a passage\",\n emptyTitle = \"Nothing to read yet\",\n emptyBody,\n countLabels,\n label = \"Passages with notes in the margin\",\n className,\n}) {\n const rootId = useRef(`mn${Math.random().toString(36).slice(2, 8)}`).current;\n const kinds = useMemo(() => resolveKinds(kindChanges), [kindChanges]);\n const { byPassage, ordered, orphans } = useMemo(() => arrangeNotes(passages, notes), [passages, notes]);\n const counts = useMemo(() => countNotes(notes), [notes]);\n\n const [inner, setInner] = useState(defaultSelected);\n const raw = selected === undefined ? inner : selected;\n const current = raw != null && ordered.some((n) => n.id === raw) ? raw : null;\n const position = current == null ? 0 : ordered.findIndex((n) => n.id === current) + 1;\n\n const [busy, setBusy] = useState({});\n const [errors, setErrors] = useState({});\n const cards = useRef(new Map());\n const rows = useRef(new Map());\n const frame = useRef(0);\n useEffect(() => () => cancelAnimationFrame(frame.current), []);\n\n const choose = (id, bring = false) => {\n if (selected === undefined) setInner(id);\n onSelect?.(id);\n if (bring && id != null) {\n cancelAnimationFrame(frame.current);\n frame.current = requestAnimationFrame(() => {\n cards.current.get(id)?.focus({ preventScroll: true });\n (rows.current.get(ordered.find((n) => n.id === id)?.passage) ?? cards.current.get(id))?.scrollIntoView({ block: \"nearest\", behavior: reducedMotion() ? \"auto\" : \"smooth\" });\n });\n }\n };\n\n async function press(note, actionId) {\n if (!onAction) return;\n const key = note.id;\n if (busy[key]) return;\n setBusy((b) => ({ ...b, [key]: actionId }));\n setErrors((e) => ({ ...e, [key]: null }));\n let r;\n try {\n r = await onAction(note, actionId);\n } catch (e) {\n r = { ok: false, error: e?.message ?? String(e) };\n }\n setBusy((b) => ({ ...b, [key]: null }));\n if (r && r.ok === false) setErrors((e) => ({ ...e, [key]: r.error || \"That did not work. Try again.\" }));\n }\n\n function onKeyDown(e) {\n if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.altKey || isTyping(e.target)) return;\n if (e.key === \"Escape\" && current != null) {\n e.preventDefault();\n choose(null);\n } else if (keys && showNotes && (e.key === \"j\" || e.key === \"J\" || e.key === \"k\" || e.key === \"K\")) {\n const next = walkNext(ordered, current, e.key.toLowerCase() === \"j\" ? 1 : -1);\n if (next == null) return;\n e.preventDefault();\n choose(next, true);\n }\n }\n\n const cols = showNotes ? (GRID[side] ?? GRID.right)[marginWidth] ?? GRID.right.md : \"\";\n const marginFirst = side === \"left\";\n const typeSize = TEXT[textSize] ?? TEXT.md;\n const summary = countWords(counts, countLabels);\n\n const noteList = (list) =>\n list.map((n) => (\n <Note\n key={n.id}\n note={n}\n kinds={kinds}\n selected={n.id === current}\n busy={busy[n.id]}\n error={errors[n.id]}\n readOnly={readOnly}\n canAct={Boolean(onAction)}\n onSelect={(id) => choose(id)}\n onPress={press}\n rootId={rootId}\n cardRef={(el) => (el ? cards.current.set(n.id, el) : cards.current.delete(n.id))}\n quoteShown={passages.find((p) => p.id === n.passage)?.node != null}\n />\n ));\n\n let body;\n if (status === \"loading\") body = <Loading cols={cols} />;\n else if (status === \"failed\")\n body = (\n <Notice tone=\"danger\" title=\"The notes did not load\" action={onRetry ? <Button size=\"sm\" icon=\"refresh\" onClick={onRetry}>Try again</Button> : null}>\n {error || \"Something went wrong. Try again.\"}\n </Notice>\n );\n else if (!passages.length) body = <Empty icon=\"message-square\" title={emptyTitle} body={emptyBody} />;\n else\n body = (\n <>\n <ol className=\"m-0 grid list-none gap-y-1 p-0\">\n {passages.map((p) => {\n const mine = byPassage.get(p.id) ?? [];\n const selectedHere = mine.some((n) => n.id === current);\n const Heading = p.level >= 2 && p.level <= 4 ? `h${p.level}` : null;\n const onPassage = mine.length ? () => choose(cycleNote(mine, current)) : undefined;\n return (\n <li\n key={p.id}\n ref={(el) => (el ? rows.current.set(p.id, el) : rows.current.delete(p.id))}\n data-passage={p.id}\n data-selected={selectedHere ? \"\" : undefined}\n className={cx(\"grid grid-cols-[minmax(0,1fr)] gap-x-6 gap-y-2 py-1.5\", cols)}\n >\n <div\n {...(mine.length\n ? {\n tabIndex: 0,\n onClick: onPassage,\n onKeyDown: (e) => {\n if ((e.key === \"Enter\" || e.key === \" \") && e.target === e.currentTarget) {\n e.preventDefault();\n onPassage();\n }\n },\n \"aria-label\": `${p.label ? `${p.label}. ` : \"\"}${mine.length} ${mine.length === 1 ? \"note\" : \"notes\"}. Press Enter to select.`,\n role: \"group\",\n }\n : {})}\n className={cx(\n \"flex min-w-0 gap-3 rounded-md px-2 py-1.5 outline-none\",\n typeSize,\n marginFirst && showNotes && \"@2xl:order-last\",\n mine.length && \"cursor-pointer focus-visible:ring-2 focus-visible:ring-ring\",\n selectedHere ? \"bg-accent\" : mine.length ? \"hover:bg-accent/50\" : \"\",\n )}\n >\n {p.lead ? <span className=\"w-12 shrink-0 pt-0.5 text-xs tabular-nums text-muted-foreground\">{p.lead}</span> : null}\n <div className=\"grid min-w-0 grow gap-0.5\">\n {p.label ? <span className=\"text-[13px] font-semibold leading-snug\">{p.label}</span> : null}\n <Words passage={p} notes={mine} kinds={kinds} selectedId={current} rootId={rootId} as={Heading ?? \"p\"} />\n </div>\n </div>\n {showNotes ? <div className={cx(\"grid min-w-0 content-start gap-2\", marginFirst && \"@2xl:order-first\")}>{noteList(mine)}</div> : null}\n </li>\n );\n })}\n </ol>\n {showNotes && orphans.length ? (\n <section aria-label={orphanTitle} className=\"mt-2 grid gap-2 border-t border-border pt-3\">\n <h3 className=\"m-0 text-xs font-medium uppercase tracking-wide text-muted-foreground\">{orphanTitle}</h3>\n <div className=\"grid grid-cols-[minmax(0,1fr)] gap-2 @2xl:grid-cols-2\">{noteList(orphans)}</div>\n </section>\n ) : null}\n </>\n );\n\n return (\n <section\n aria-label={label}\n tabIndex={0}\n onKeyDown={onKeyDown}\n className={cx(\"@container grid min-w-0 grid-cols-[minmax(0,1fr)] gap-3 rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-ring\", className)}\n >\n {showNotes && status === \"ready\" && passages.length > 0 && Boolean(marginTitle || summary || (keys && ordered.length > 0) || !notes.length) ? (\n <header className=\"flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1\">\n {marginTitle ? <h2 className=\"m-0 text-base font-semibold leading-tight\">{marginTitle}</h2> : null}\n <p className=\"m-0 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground\">\n {summary ? <span>{summary}</span> : !notes.length ? <span>{noNotes}</span> : null}\n {keys && ordered.length ? (\n <span className=\"hidden items-center gap-1 @2xl:inline-flex\">\n <Kbd>J</Kbd>\n <Kbd>K</Kbd>\n <span>next and previous note</span>\n </span>\n ) : null}\n </p>\n </header>\n ) : null}\n <p role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {current != null ? `Note ${position} of ${ordered.length}` : \"\"}\n </p>\n {body}\n </section>\n );\n}\n"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"path": "registry/blocks/margin-notes/margin-model.js",
|
|
27
|
+
"type": "registry:component",
|
|
28
|
+
"target": "@components/makersclaw/blocks/margin-notes/margin-model.js",
|
|
29
|
+
"content": "// The margin-notes block's pure model — no React, no DOM.\n//\n// A passage is a stretch of words with an id. A note belongs to one passage\n// (note.passage) and says something about it: where it came from, that it\n// needs checking, that it was cut, a comment. This file holds the rules the\n// component and its tests share: which kinds exist and how each looks and\n// acts, which notes sit by which passage, which words of a passage a note\n// marks, what J and K walk to, and the counts.\n//\n// README.md beside this file is the contract; this file is the proof.\n\n/** The tones a kind or a note may use (the kit's status tones). */\nexport const TONES = [\"neutral\", \"primary\", \"info\", \"success\", \"warning\", \"danger\"];\n\n/** How the words a note is about are drawn in the passage. */\nexport const MARKS = [\"underline\", \"wavy\", \"strike\", \"highlight\", \"none\"];\n\n/**\n * The four kinds a margin knows. An app may change any field of one, or add\n * its own kind, through the block's `kinds` prop.\n *\n * label the chip's words\n * icon a kit icon name\n * tone neutral · primary · info · success · warning · danger\n * mark underline · wavy · strike · highlight · none: the words in the\n * passage that the note is about\n * actions [{ id, label, variant?, icon? }]: the buttons on an open note\n * reopen the button on a settled note (false: none)\n */\nexport const MARGIN_KINDS = {\n source: { label: \"Source\", icon: \"link\", tone: \"neutral\", mark: \"underline\", actions: [], reopen: false },\n flag: {\n label: \"Check this line\",\n icon: \"flag\",\n tone: \"warning\",\n mark: \"wavy\",\n actions: [\n { id: \"confirm\", label: \"It’s true\", icon: \"check\", variant: \"secondary\" },\n { id: \"cut\", label: \"Cut it\", icon: \"x\", variant: \"secondary\" },\n { id: \"ask\", label: \"Ask where it came from\", variant: \"quiet\" },\n ],\n reopen: { label: \"Reopen\" },\n },\n cut: {\n label: \"Cut\",\n icon: \"eraser\",\n tone: \"danger\",\n mark: \"strike\",\n actions: [{ id: \"putback\", label: \"Put back\", icon: \"undo\", variant: \"secondary\" }],\n reopen: false,\n },\n comment: { label: \"Comment\", icon: \"message-square\", tone: \"info\", mark: \"highlight\", actions: [], reopen: false },\n};\n\nconst FALLBACK = { label: \"Note\", icon: \"message-circle\", tone: \"neutral\", mark: \"highlight\", actions: [], reopen: false };\n\n/** \"needs_review\" -> \"Needs review\". */\nexport function humanize(id) {\n const words = String(id ?? \"\").replace(/[_-]+/g, \" \").trim();\n return words ? words[0].toUpperCase() + words.slice(1) : \"\";\n}\n\n/** The built-in kinds with an app's changes laid over them, field by field. */\nexport function resolveKinds(overrides) {\n const out = {};\n for (const [id, def] of Object.entries(MARGIN_KINDS)) out[id] = { ...def };\n for (const [id, def] of Object.entries(overrides ?? {})) {\n if (!def || typeof def !== \"object\") continue;\n out[id] = { ...(out[id] ?? { ...FALLBACK, label: humanize(id) }), ...def };\n }\n return out;\n}\n\n/** What a note looks like and offers: its kind's definition with the note's own words over it. */\nexport function noteLook(note, kinds) {\n const kind = kinds?.[note?.kind] ?? { ...FALLBACK, label: humanize(note?.kind) || FALLBACK.label };\n const tone = TONES.includes(note?.tone) ? note.tone : TONES.includes(kind.tone) ? kind.tone : \"neutral\";\n const hue = Number.isInteger(note?.hue) && note.hue >= 1 && note.hue <= 5 ? note.hue : null;\n const mark = MARKS.includes(note?.mark) ? note.mark : MARKS.includes(kind.mark) ? kind.mark : \"highlight\";\n return {\n label: note?.label || kind.label,\n icon: kind.icon || FALLBACK.icon,\n tone,\n hue,\n mark,\n actions: Array.isArray(note?.actions) ? note.actions : kind.actions ?? [],\n reopen: kind.reopen,\n };\n}\n\n/** A note nobody needs to act on any more. */\nexport const isSettled = (note) => note?.state === \"settled\";\n\n/** The buttons an open note shows; none when it is locked, settled or read-only. */\nexport function actionsOf(note, kinds, { readOnly = false } = {}) {\n if (readOnly || isSettled(note) || note?.locked) return [];\n return noteLook(note, kinds).actions.filter((a) => a && a.id && a.label);\n}\n\n/**\n * Which notes sit by which passage.\n * byPassage Map: passage id -> its notes, in the order given\n * ordered every note, in the passages' order (a passage's notes keep their order)\n * orphans notes whose passage is not in the list\n */\nexport function arrangeNotes(passages, notes) {\n const byPassage = new Map();\n for (const p of passages ?? []) {\n if (!p || p.id == null || byPassage.has(p.id)) continue;\n byPassage.set(p.id, []);\n }\n const orphans = [];\n const seen = new Set();\n for (const n of notes ?? []) {\n if (!n || n.id == null || seen.has(n.id)) continue;\n seen.add(n.id);\n if (byPassage.has(n.passage)) byPassage.get(n.passage).push(n);\n else orphans.push(n);\n }\n const ordered = [];\n for (const list of byPassage.values()) ordered.push(...list);\n ordered.push(...orphans);\n return { byPassage, ordered, orphans };\n}\n\n/**\n * What J (dir 1) and K (dir -1) walk to: the next note that still needs a\n * person, after the selected one. With nothing selected, the first (or the\n * last, going back). At the end it stays where it is. Null when no note is\n * left to walk.\n */\nexport function walkNext(ordered, currentId, dir = 1) {\n const list = ordered ?? [];\n const step = dir < 0 ? -1 : 1;\n const open = (n) => !isSettled(n);\n if (!list.some(open)) return null;\n const at = list.findIndex((n) => n.id === currentId);\n if (at < 0) {\n const pick = step > 0 ? list.find(open) : [...list].reverse().find(open);\n return pick.id;\n }\n for (let i = at + step; i >= 0 && i < list.length; i += step) if (open(list[i])) return list[i].id;\n return currentId;\n}\n\n/** A press on a passage: its first note; pressed again, its next; after the last, none. */\nexport function cycleNote(passageNotes, currentId) {\n const list = passageNotes ?? [];\n if (!list.length) return null;\n const at = list.findIndex((n) => n.id === currentId);\n if (at < 0) return list[0].id;\n return list[at + 1]?.id ?? null;\n}\n\nconst escapeRe = (s) => s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\n/** Where a quote sits in a text, ignoring case: [start, end] or null. */\nexport function findQuote(text, quote) {\n const q = typeof quote === \"string\" ? quote.trim() : \"\";\n if (!q || typeof text !== \"string\") return null;\n const m = new RegExp(escapeRe(q).replace(/\\s+/g, \"\\\\s+\"), \"iu\").exec(text);\n return m ? [m.index, m.index + m[0].length] : null;\n}\n\n/**\n * The words of a passage, cut into pieces by the notes that mark them.\n *\n * segments [{ text, ids }]: in order, joined they are the text; ids are\n * the notes whose quote covers the piece (empty: plain words)\n * whole the first open note whose mark is \"strike\" and whose quote\n * is empty or not in the text: it strikes the whole passage\n * (then `segments` is that one piece)\n *\n * `markOf(note)` names how a note marks its words; a settled note marks\n * nothing (a cut that was put back reads as plain words again).\n */\nexport function inlineMarks(text, notes, markOf = () => \"highlight\") {\n const body = typeof text === \"string\" ? text : \"\";\n const ranges = [];\n let whole = null;\n for (const n of notes ?? []) {\n if (isSettled(n)) continue;\n const mark = markOf(n);\n if (mark === \"none\") continue;\n const at = findQuote(body, n.quote);\n if (at) ranges.push({ id: n.id, from: at[0], to: at[1] });\n else if (mark === \"strike\" && !whole) whole = n;\n }\n if (whole) return { segments: [{ text: body, ids: [whole.id] }], whole };\n if (!ranges.length) return { segments: [{ text: body, ids: [] }], whole: null };\n const cuts = [...new Set([0, body.length, ...ranges.flatMap((r) => [r.from, r.to])])].sort((a, b) => a - b);\n const segments = [];\n for (let i = 0; i < cuts.length - 1; i++) {\n const [a, b] = [cuts[i], cuts[i + 1]];\n if (a === b) continue;\n segments.push({ text: body.slice(a, b), ids: ranges.filter((r) => r.from <= a && r.to >= b).map((r) => r.id) });\n }\n return { segments, whole: null };\n}\n\n/** How many notes there are, how many still need a person, and by kind. */\nexport function countNotes(notes) {\n const out = { total: 0, open: 0, settled: 0, byKind: {} };\n for (const n of notes ?? []) {\n if (!n) continue;\n out.total += 1;\n if (isSettled(n)) out.settled += 1;\n else out.open += 1;\n const k = (out.byKind[n.kind] ??= { total: 0, open: 0 });\n k.total += 1;\n if (!isSettled(n)) k.open += 1;\n }\n return out;\n}\n\n/** \"3 open · 1 settled\" — the margin's count line. */\nexport function countWords(counts, words = {}) {\n const open = words.open ?? \"open\";\n const settled = words.settled ?? \"settled\";\n const parts = [];\n if (counts.open) parts.push(`${counts.open} ${open}`);\n if (counts.settled) parts.push(`${counts.settled} ${settled}`);\n return parts.join(\" · \");\n}\n"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"path": "registry/blocks/margin-notes/README.md",
|
|
33
|
+
"type": "registry:file",
|
|
34
|
+
"target": "@components/makersclaw/blocks/margin-notes/README.md",
|
|
35
|
+
"content": "# MarginNotes\n\nPassages of text with notes in the margin, **level with the words they are\nabout**. On a wide container each passage is a row: the words on one side,\nits notes on the other. On a narrow one (under about 42rem) the notes stack\nunder the passage they belong to.\n\nA note is one of four kinds, each with its own tone and its own buttons:\n\n- **source** — where the words came from: a title, a line of detail, the\n words that were read, a link.\n- **flag** — \"No source. Check this line.\" with **It's true**, **Cut it**,\n **Ask where it came from**.\n- **cut** — the words struck through where they stand, the reason in the\n margin, **Put back** (or a lock when nobody may put them back).\n- **comment** — what someone thought of the line.\n\nAn app can change any of them or add its own kind.\n\nThe words a note is about (`quote`) are marked in the passage: dotted for a\nsource, wavy for a flag, struck for a cut, washed for a comment. **The\nselected note and its passage are linked**: click a passage or a note, or\npress <kbd>J</kbd> and <kbd>K</kbd> to walk the notes that still need a\nperson. A settled note folds to one line with what was decided and a\n**Reopen**.\n\n```jsx\nimport { MarginNotes } from \"../components/blocks/margin-notes\"; // a Makersclaw app (\"./blocks/margin-notes\" from components/)\nimport { MarginNotes } from \"@/components/makersclaw/blocks/margin-notes\"; // a shadcn project\n```\n\nA client component. `makersclaw add margin-notes` lays `agent-marks` with\nit. The helpers (`walkNext`, `inlineMarks`, `countNotes`…) are plain\nfunctions and work on the server.\n\n## Which hook feeds it\n\nNone. The passages are the app's words (a draft, a transcript); the notes are\nrows the agent or a person wrote beside them. Keep them in the app's tables\nand pass them in. The block never reads or writes: a press goes out through\n`onAction`, the app does the write, and hands back the changed notes.\n\n## Props\n\n- `passages` — the words, in reading order: `[{ id, text, lead?, label?,\n level?, node? }]`. `text` is plain words. `lead` is a short figure before\n them (a time `00:12`, a number); `label` a name above them (a speaker, a\n section); `level` `2` · `3` · `4` draws the passage as a heading. `node`\n (a React node) draws your own markup instead of `text`: a note's `quote`\n cannot mark inside it (the card shows the quote instead), but a cut still\n strikes it whole.\n- `notes` — `[{ id, passage, kind, … }]`. `passage` is the id of the\n passage it sits by; a note whose passage is not in the list is drawn\n after the last row under `orphanTitle`. The rest:\n - `kind` — `source` · `flag` · `cut` · `comment`, or one of your `kinds`.\n - `title` — the note's heading (\"v3.0.0\", \"No source. Check this line.\").\n - `meta` — a muted line under it (\"tern-app/tern · shipped Tue 22 Sep\").\n - `text` — the note's words (a reason, a comment).\n - `detail` — the proof: the words that were read, under a dashed rule.\n - `quote` — the words **in the passage** the note is about. The first\n place they appear (ignoring case) is marked. Without one a `cut` strikes\n the whole passage; the other kinds mark nothing in the words.\n - `by` — `\"agent\"` or `{ agent: true }` draws the agent's mark; any other\n string or `{ name }` is shown as a name.\n - `at` — when, in words (\"12:31\", \"Tue 22 Sep\").\n - `href`, `hrefLabel` — \"Open the source\" (default) as a link that opens\n in a new tab.\n - `locked` — words for why nothing can be done to this note (\"On the\n never-list: margins\"). A locked note shows a lock and no buttons.\n - `state` — `\"settled\"` folds the note to a line: `answer` (\"You said\n it's true\") and `at`, and the kind's **Reopen** if it has one. Settled\n notes mark nothing in the words and are skipped by <kbd>J</kbd> and\n <kbd>K</kbd>.\n - `actions` — `[{ id, label, variant?, icon? }]`, replacing the kind's\n buttons for this note.\n - `label`, `tone`, `hue`, `mark` — this note's chip words, status tone\n (`neutral` · `primary` · `info` · `success` · `warning` · `danger`),\n categorical colour `1`–`5` (for a family of cuts: pricing, other people,\n in confidence, length), and mark (`underline` · `wavy` · `strike` ·\n `highlight` · `none`) over the kind's.\n- `selected`, `defaultSelected`, `onSelect(id | null)` — the selected\n note's id. Pass `selected` to control it (from the address, say); leave it\n out and the block keeps it. <kbd>Esc</kbd> clears it.\n- `onAction(note, actionId)` — `async`, returns `{ ok: true }` or\n `{ ok: false, error? }`. Called when a button is pressed; the button shows\n its spinner until it answers, and a failure prints its `error` under the\n note, where it happened. A throw counts as a failure. A settled note's\n Reopen calls it with `\"reopen\"`. **Without `onAction` no buttons are\n drawn.**\n- `kinds` — changes to the built-ins, or new kinds, by id: `{ flag: {\n label: \"Check\" }, aside: { label: \"Aside\", tone: \"primary\", icon:\n \"message-circle\", mark: \"highlight\", actions: [] } }`. Fields are `label`,\n `icon` (a kit icon), `tone`, `mark`, `actions`, and `reopen` (`false`, or\n `{ label }`). Each field you give replaces that one field.\n- `side` — `right` (default) or `left`: which side of the words the margin\n is on. Narrow, the notes are always under the words.\n- `marginWidth` — `sm` (15rem) · `md` (18rem, default) · `lg` (22rem).\n- `marginTitle` — a heading over the whole thing (\"Where each paragraph\n came from\"), with the count line beside it.\n- `showNotes` — `false` hides the margin and its marks: the words alone.\n- `textSize` — `sm` · `md` (default) · `lg`.\n- `status` — `ready` (default) · `loading` (rows of grey bars) · `failed`\n (a notice with `error`, and Try again if you pass `onRetry`).\n- `readOnly` — notes to read: no buttons, no Reopen.\n- `keys` — <kbd>J</kbd> and <kbd>K</kbd> walk the notes (default `true`).\n- `noNotes`, `orphanTitle`, `emptyTitle`, `emptyBody`, `countLabels` — the\n block's few words, to be changed (`countLabels`: `{ open, settled }`).\n- `label` — the region's accessible name.\n- `className` — layout: width, margin, height. Never colour.\n\n### Keys\n\nOn the region (it takes focus with Tab): <kbd>J</kbd> the next note that\nstill needs a person, <kbd>K</kbd> the one before, <kbd>Esc</kbd> clears the\nselection. On a passage with notes: <kbd>Enter</kbd> or <kbd>Space</kbd>\nselects its first note, again its next, after the last none. The walk moves\nfocus to the note, scrolls it into view (without animation when the person\nhas asked for less motion) and a hidden line says \"Note 2 of 5\". The keys\nare left alone while someone is typing in a field.\n\n### Helpers\n\nPlain functions (`import` them from the same folder):\n\n- `walkNext(notes, currentId, dir)` — the id J (`1`) or K (`-1`) goes to.\n- `inlineMarks(text, notes, markOf)` — a passage's words cut into pieces\n by the notes' quotes: `{ segments: [{ text, ids }], whole }`.\n- `arrangeNotes(passages, notes)` — `{ byPassage, ordered, orphans }`.\n- `countNotes(notes)`, `countWords(counts)` — `{ total, open, settled,\n byKind }` and \"3 open · 1 settled\".\n- `resolveKinds(changes)`, `noteLook(note, kinds)`, `actionsOf(note, kinds)`\n — the kinds, and what a note looks like and offers.\n- `MARGIN_KINDS` — the built-in kinds.\n\n## Example\n\nAn issue with a source beside each paragraph, and a line the agent could\nnot source:\n\n```jsx\n\"use client\";\nimport { useRows } from \"../lib/data\";\nimport { updateRow } from \"../lib/actions\";\nimport { MarginNotes } from \"../components/blocks/margin-notes\";\n\nexport default function Issue({ issueId }) {\n const paragraphs = useRows({ source: \"issue_paragraphs\", filter: { issue_id: issueId }, orderBy: \"position asc\" });\n const notes = useRows({ source: \"paragraph_notes\", filter: { issue_id: issueId } });\n if (paragraphs.loading || notes.loading) return <MarginNotes status=\"loading\" />;\n return (\n <MarginNotes\n marginTitle=\"Where each paragraph came from\"\n passages={(paragraphs.rows ?? []).map((p) => ({ id: p.id, text: p.body, level: p.is_heading ? 3 : undefined }))}\n notes={(notes.rows ?? []).map((n) => ({\n id: n.id,\n passage: n.paragraph_id,\n kind: n.kind, // \"source\" · \"flag\"\n title: n.title,\n meta: n.meta,\n detail: n.excerpt,\n quote: n.quote,\n by: n.by_agent ? \"agent\" : undefined,\n state: n.settled_at ? \"settled\" : undefined,\n answer: n.answer,\n }))}\n onAction={(note, action) =>\n updateRow({ source: \"paragraph_notes\", id: note.id, patch: { answer: action, settled_at: action === \"reopen\" ? null : new Date().toISOString() } })\n }\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 transcript with the cut words struck, a family colour per reason, and a\nlock for what may never come back:\n\n```jsx\n<MarginNotes\n side=\"left\"\n marginWidth=\"lg\"\n textSize=\"sm\"\n passages={lines.map((l) => ({ id: l.id, lead: l.clock, label: l.speaker, text: l.words }))}\n notes={cuts.map((c) => ({\n id: c.id,\n passage: c.line_id,\n kind: \"cut\",\n hue: c.family, // 1..5, one colour per reason family\n label: c.reason, // \"Pricing\", \"A third party\"…\n text: c.note,\n locked: c.on_never_list ? `On the never-list: ${c.on_never_list}.` : undefined,\n state: c.put_back ? \"settled\" : undefined,\n answer: \"Put back\",\n }))}\n onAction={(note, action) => putBack(note.id)}\n/>\n```\n\n## States\n\nDrawn in `demo.jsx`: sources and a flag that settles in place, cuts with\nthe margin on the left, 320 px (notes under the words), one passage, very\nlong words, passages with an empty margin, no passages, loading, failed,\nread-only.\n\n## What it will not do\n\n- Edit the words. A passage is read; to change it, open it in your editor\n (a flag's \"Cut it\" goes out through `onAction` and your app changes the\n text).\n- Mark the same quote twice in one passage, or a quote that spans two\n passages: the first place it appears is marked, in the passage the note\n is on.\n- Put a note anywhere but beside its passage's row. A long note makes its\n row taller.\n- Keep the app's data. Settled, cut, put back: all come back in through\n `notes`.\n\nA margin 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 margin-notes` restores them.\n"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"categories": [
|
|
39
|
+
"margin",
|
|
40
|
+
"notes",
|
|
41
|
+
"marginalia",
|
|
42
|
+
"sources",
|
|
43
|
+
"citations",
|
|
44
|
+
"provenance",
|
|
45
|
+
"fact check",
|
|
46
|
+
"flag",
|
|
47
|
+
"claim",
|
|
48
|
+
"check",
|
|
49
|
+
"cut",
|
|
50
|
+
"struck",
|
|
51
|
+
"redact",
|
|
52
|
+
"transcript",
|
|
53
|
+
"minutes",
|
|
54
|
+
"review",
|
|
55
|
+
"comment",
|
|
56
|
+
"proof",
|
|
57
|
+
"annotation",
|
|
58
|
+
"agent"
|
|
59
|
+
],
|
|
60
|
+
"docs": "Installed to components/makersclaw/blocks/margin-notes. Import: import { MarginNotes, MARGIN_KINDS, TONES, MARKS, resolveKinds, noteLook, actionsOf, isSettled, arrangeNotes, walkNext, cycleNote, findQuote, inlineMarks, countNotes, countWords } from \"@/components/makersclaw/blocks/margin-notes\". Props and an example: components/makersclaw/blocks/margin-notes/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
61
|
+
"meta": {
|
|
62
|
+
"makersclaw": {
|
|
63
|
+
"version": "0.8.1",
|
|
64
|
+
"useWhen": "A piece of writing is read against what stands behind it: a newsletter issue with a source beside every paragraph, a blog post with the claims that need a person, minutes whose transcript shows what was cut and why, a review with a reviewer's comments beside the lines. Not a thread of comments with no text beside it (comment-box), not one note log for a record (record-notes), not suggested edits inside an editor (doc-editor), not a screenshot with pins (annotate)."
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|