makersclaw 0.8.0 → 0.8.2
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 +234 -0
- package/registry/blocks/agent-pane/agent-pane.jsx +246 -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 +1677 -8
- package/templates/next/files/lib/platform.js +529 -21
- package/templates/next/template.json +13 -3
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// An automation drawn as a real node canvas: pan, zoom, fit, a map, icon
|
|
4
|
+
// cards with ports, curved connectors with arrowheads, a + on every
|
|
5
|
+
// connector and at every path's end, labelled branches that meet again, a
|
|
6
|
+
// dashed box for "for each", grouped steps that fold to one card, and a
|
|
7
|
+
// palette.
|
|
8
|
+
//
|
|
9
|
+
// Built by hand, no graph library: the format is a tree, so the layout is
|
|
10
|
+
// exact (automation-model graph.js). It takes a layout and callbacks and
|
|
11
|
+
// paints with the app's tokens.
|
|
12
|
+
//
|
|
13
|
+
// Two uses, one picture:
|
|
14
|
+
// - editing (`editable`): + inserts, a card selects, the palette adds;
|
|
15
|
+
// - a run (`overlay`): each card wears how it went, the path taken is lit
|
|
16
|
+
// and the rest dimmed; nothing can be changed.
|
|
17
|
+
//
|
|
18
|
+
// Input: drag the ground to pan; the wheel pans, Ctrl/Cmd-wheel and pinch
|
|
19
|
+
// zoom; the arrow keys walk the cards, Esc lets go. Everything the canvas
|
|
20
|
+
// does with a pointer is also reachable by keys and by the outline.
|
|
21
|
+
|
|
22
|
+
import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
23
|
+
import { Icon, cx } from "../../ui";
|
|
24
|
+
import { Glyph, KINDS, LABEL_W, T, curveBetween, fixTag, nodeIndex, readingOrder, slotWords, triggerGlyph } from "../automation-model";
|
|
25
|
+
import { StepPicker, PaletteBar } from "../step-picker";
|
|
26
|
+
import { MAP_H, MAP_W, Minimap } from "./minimap";
|
|
27
|
+
|
|
28
|
+
const MIN_K = 0.2;
|
|
29
|
+
const MAX_K = 1.6;
|
|
30
|
+
const clampK = (k) => Math.min(MAX_K, Math.max(MIN_K, k));
|
|
31
|
+
|
|
32
|
+
/** One decimal only when there is one: 3 → "3", 2.4 → "2.4". */
|
|
33
|
+
const formatPrice = (n) => String(Math.round(n * 10) / 10);
|
|
34
|
+
|
|
35
|
+
const STATE_ICON = { done: "check", would: "pencil", running: "loader", failed: "x", fixed: "wand", waiting: "clock", skipped: "minus", unknown: "circle-help" };
|
|
36
|
+
const BADGE = {
|
|
37
|
+
done: "bg-success",
|
|
38
|
+
would: "bg-muted-foreground",
|
|
39
|
+
running: "bg-info",
|
|
40
|
+
failed: "bg-destructive",
|
|
41
|
+
fixed: "bg-primary",
|
|
42
|
+
waiting: "bg-warning",
|
|
43
|
+
skipped: "bg-muted-foreground",
|
|
44
|
+
unknown: "bg-muted-foreground",
|
|
45
|
+
};
|
|
46
|
+
// A tile: tone first, then how the run left it (the later wins by order).
|
|
47
|
+
const TONE = {
|
|
48
|
+
plain: "border-border text-muted-foreground",
|
|
49
|
+
model: "border-primary/55 text-primary",
|
|
50
|
+
agent: "border-primary bg-primary/15 text-primary",
|
|
51
|
+
};
|
|
52
|
+
const RUNSTATE = {
|
|
53
|
+
done: "border-success/60",
|
|
54
|
+
// The pencil look: it would run, it did not. Dashed, no solid fill.
|
|
55
|
+
would: "border-dashed border-primary/70 bg-transparent text-primary",
|
|
56
|
+
running: "border-info ring-4 ring-info/20",
|
|
57
|
+
failed: "border-destructive",
|
|
58
|
+
fixed: "border-primary",
|
|
59
|
+
waiting: "border-warning ring-4 ring-warning/20",
|
|
60
|
+
skipped: "border-dashed border-muted-foreground/45 bg-transparent text-muted-foreground/60",
|
|
61
|
+
notreached: "border-dashed border-muted-foreground/45 bg-transparent text-muted-foreground/60",
|
|
62
|
+
unknown: "border-warning",
|
|
63
|
+
};
|
|
64
|
+
const EDGE = {
|
|
65
|
+
n: { path: "fill-none stroke-muted-foreground/55", head: "fill-muted-foreground/55", width: 1.6 },
|
|
66
|
+
t: { path: "fill-none stroke-success", head: "fill-success", width: 2.6 },
|
|
67
|
+
r: { path: "fill-none stroke-info", head: "fill-info", width: 2.6 },
|
|
68
|
+
d: { path: "fill-none stroke-muted-foreground/25", head: "fill-muted-foreground/25", width: 1.6 },
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* layout automation-model's layoutAutomation()
|
|
73
|
+
* selectedId a step id, "trigger", or null
|
|
74
|
+
* onSelect(id|null)
|
|
75
|
+
* editable the + buttons, the palette and the add button
|
|
76
|
+
* onInsert(slot, kind) a + was pressed and a kind chosen
|
|
77
|
+
* onAddAtSelection(kind) the palette (or the round add) chose a kind
|
|
78
|
+
* onToggleCollapse(stepId) fold or open a for-each
|
|
79
|
+
* overlay a run's overlayOf(): each card and connector, how it went
|
|
80
|
+
* (in a test run a step that stood in for itself is "would":
|
|
81
|
+
* a dashed card with a pencil and one line of what it would do;
|
|
82
|
+
* a step it could not check says "Not checked")
|
|
83
|
+
* flagged a Set of step ids with something to fix (a red mark)
|
|
84
|
+
* prices a Map of step id to { credits, max }: the price tag
|
|
85
|
+
* fitKey changes when the picture should fit again
|
|
86
|
+
* revealId pan so this step is in view (after an insert)
|
|
87
|
+
* info floating, top left, after the zoom controls
|
|
88
|
+
* runnable a Set of kinds the platform runs (the picker says "soon" for the rest)
|
|
89
|
+
*/
|
|
90
|
+
export function FlowCanvas({ layout, selectedId = null, onSelect, editable = false, onInsert, onAddAtSelection, onToggleCollapse, overlay = null, flagged, prices, fitKey, revealId, info, runnable, className }) {
|
|
91
|
+
const uid = useId().replace(/[^a-zA-Z0-9]/g, "");
|
|
92
|
+
const rootRef = useRef(null);
|
|
93
|
+
const [view, setViewState] = useState({ x: 40, y: 40, k: 1 });
|
|
94
|
+
const viewRef = useRef(view);
|
|
95
|
+
const [size, setSize] = useState({ w: 0, h: 0 });
|
|
96
|
+
const [picker, setPicker] = useState(null);
|
|
97
|
+
const fitted = useRef(null);
|
|
98
|
+
|
|
99
|
+
const setView = useCallback((next) => {
|
|
100
|
+
const v = typeof next === "function" ? next(viewRef.current) : next;
|
|
101
|
+
viewRef.current = v;
|
|
102
|
+
setViewState(v);
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
const index = useMemo(() => nodeIndex(layout), [layout]);
|
|
106
|
+
|
|
107
|
+
/* ---- measuring and fitting ---------------------------------------- */
|
|
108
|
+
|
|
109
|
+
const fit = useCallback(() => {
|
|
110
|
+
const el = rootRef.current;
|
|
111
|
+
if (!el) return;
|
|
112
|
+
const w = el.clientWidth;
|
|
113
|
+
const h = el.clientHeight;
|
|
114
|
+
const b = layout.bounds;
|
|
115
|
+
if (!w || !h || b.w <= 0) return;
|
|
116
|
+
const k = clampK(Math.min(w / b.w, h / b.h, 1));
|
|
117
|
+
setView({ k, x: (w - b.w * k) / 2 - b.x * k, y: (h - b.h * k) / 2 - b.y * k });
|
|
118
|
+
}, [layout.bounds, setView]);
|
|
119
|
+
|
|
120
|
+
useLayoutEffect(() => {
|
|
121
|
+
const el = rootRef.current;
|
|
122
|
+
if (!el) return undefined;
|
|
123
|
+
const measure = () => setSize({ w: el.clientWidth, h: el.clientHeight });
|
|
124
|
+
measure();
|
|
125
|
+
if (typeof ResizeObserver === "undefined") return undefined;
|
|
126
|
+
// Measure in the callback, act in the next frame: setting state that
|
|
127
|
+
// changes what is watched, inside its own delivery, is the browser's
|
|
128
|
+
// "ResizeObserver loop" notice (check rule 17).
|
|
129
|
+
let raf = 0;
|
|
130
|
+
const ro = new ResizeObserver(() => {
|
|
131
|
+
cancelAnimationFrame(raf);
|
|
132
|
+
raf = requestAnimationFrame(measure);
|
|
133
|
+
});
|
|
134
|
+
ro.observe(el);
|
|
135
|
+
return () => {
|
|
136
|
+
cancelAnimationFrame(raf);
|
|
137
|
+
ro.disconnect();
|
|
138
|
+
};
|
|
139
|
+
}, []);
|
|
140
|
+
|
|
141
|
+
// Fit once per fitKey, as soon as there is a size to fit into.
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
const key = fitKey ?? "";
|
|
144
|
+
if (fitted.current === key || !size.w || !size.h) return;
|
|
145
|
+
fitted.current = key;
|
|
146
|
+
fit();
|
|
147
|
+
}, [fitKey, size.w, size.h, fit]);
|
|
148
|
+
|
|
149
|
+
// Bring a new step into view without moving the picture if it is already seen.
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (!revealId || !size.w) return;
|
|
152
|
+
const n = index.get(revealId);
|
|
153
|
+
if (!n) return;
|
|
154
|
+
const v = viewRef.current;
|
|
155
|
+
const sx = n.x * v.k + v.x;
|
|
156
|
+
const sy = n.y * v.k + v.y;
|
|
157
|
+
const pad = 80;
|
|
158
|
+
const inside = sx > pad && sy > pad && sx + n.w * v.k < size.w - pad && sy + n.h * v.k < size.h - pad;
|
|
159
|
+
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 });
|
|
160
|
+
}, [revealId, index, size.w, size.h, setView]);
|
|
161
|
+
|
|
162
|
+
/* ---- zoom and pan -------------------------------------------------- */
|
|
163
|
+
|
|
164
|
+
const zoomAt = useCallback(
|
|
165
|
+
(factor, cx0, cy0) => {
|
|
166
|
+
setView((v) => {
|
|
167
|
+
const k = clampK(v.k * factor);
|
|
168
|
+
const r = k / v.k;
|
|
169
|
+
return { k, x: cx0 - (cx0 - v.x) * r, y: cy0 - (cy0 - v.y) * r };
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
[setView],
|
|
173
|
+
);
|
|
174
|
+
const zoomCentre = (factor) => zoomAt(factor, size.w / 2, size.h / 2);
|
|
175
|
+
|
|
176
|
+
// The wheel needs a non-passive listener to keep the page from zooming.
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
const el = rootRef.current;
|
|
179
|
+
if (!el) return undefined;
|
|
180
|
+
const onWheel = (e) => {
|
|
181
|
+
if (e.target.closest?.("[data-scroll]")) return;
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
const r = el.getBoundingClientRect();
|
|
184
|
+
if (e.ctrlKey || e.metaKey) zoomAt(Math.exp(-e.deltaY * 0.01), e.clientX - r.left, e.clientY - r.top);
|
|
185
|
+
else setView((v) => ({ ...v, x: v.x - e.deltaX, y: v.y - e.deltaY }));
|
|
186
|
+
};
|
|
187
|
+
el.addEventListener("wheel", onWheel, { passive: false });
|
|
188
|
+
return () => el.removeEventListener("wheel", onWheel);
|
|
189
|
+
}, [zoomAt, setView]);
|
|
190
|
+
|
|
191
|
+
const pointers = useRef(new Map());
|
|
192
|
+
const drag = useRef(null);
|
|
193
|
+
const pinch = useRef(null);
|
|
194
|
+
|
|
195
|
+
const onPointerDown = (e) => {
|
|
196
|
+
if (e.target.closest?.("button, input, select, textarea, [data-nopan]")) return;
|
|
197
|
+
pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
198
|
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
199
|
+
if (pointers.current.size === 1) drag.current = { x: e.clientX, y: e.clientY, moved: false };
|
|
200
|
+
else {
|
|
201
|
+
drag.current = null;
|
|
202
|
+
const [a, b] = [...pointers.current.values()];
|
|
203
|
+
pinch.current = Math.hypot(a.x - b.x, a.y - b.y);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
const onPointerMove = (e) => {
|
|
207
|
+
if (!pointers.current.has(e.pointerId)) return;
|
|
208
|
+
const prev = pointers.current.get(e.pointerId);
|
|
209
|
+
pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
210
|
+
if (pointers.current.size === 2 && pinch.current) {
|
|
211
|
+
const [a, b] = [...pointers.current.values()];
|
|
212
|
+
const dist = Math.hypot(a.x - b.x, a.y - b.y);
|
|
213
|
+
const r = rootRef.current?.getBoundingClientRect();
|
|
214
|
+
if (r && dist > 0) zoomAt(dist / pinch.current, (a.x + b.x) / 2 - r.left, (a.y + b.y) / 2 - r.top);
|
|
215
|
+
pinch.current = dist;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const d = drag.current;
|
|
219
|
+
if (!d) return;
|
|
220
|
+
if (!d.moved && Math.hypot(e.clientX - d.x, e.clientY - d.y) < 4) return;
|
|
221
|
+
d.moved = true;
|
|
222
|
+
setView((v) => ({ ...v, x: v.x + (e.clientX - prev.x), y: v.y + (e.clientY - prev.y) }));
|
|
223
|
+
};
|
|
224
|
+
const onPointerUp = (e) => {
|
|
225
|
+
pointers.current.delete(e.pointerId);
|
|
226
|
+
pinch.current = null;
|
|
227
|
+
const d = drag.current;
|
|
228
|
+
drag.current = null;
|
|
229
|
+
// A press on the ground that went nowhere lets go of the selection.
|
|
230
|
+
if (d && !d.moved) {
|
|
231
|
+
onSelect?.(null);
|
|
232
|
+
setPicker(null);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const onKeyDown = (e) => {
|
|
237
|
+
if (e.target.closest?.("input, textarea, select")) return;
|
|
238
|
+
if (e.key === "Escape") {
|
|
239
|
+
if (picker) setPicker(null);
|
|
240
|
+
else onSelect?.(null);
|
|
241
|
+
} else if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
|
|
242
|
+
const order = readingOrder(layout);
|
|
243
|
+
if (!order.length) return;
|
|
244
|
+
const at = order.findIndex((n) => (n.stepId ?? n.id) === selectedId || n.id === selectedId);
|
|
245
|
+
const next = order[Math.min(order.length - 1, Math.max(0, at + (e.key === "ArrowRight" ? 1 : -1)))];
|
|
246
|
+
if (next) onSelect?.(next.stepId ?? next.id);
|
|
247
|
+
e.preventDefault();
|
|
248
|
+
} else if (e.key === "+" || e.key === "=") zoomCentre(1.2);
|
|
249
|
+
else if (e.key === "-") zoomCentre(1 / 1.2);
|
|
250
|
+
else if (e.key === "0") fit();
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/* ---- the picker ---------------------------------------------------- */
|
|
254
|
+
|
|
255
|
+
const openPicker = (slot, at) => {
|
|
256
|
+
const r = rootRef.current?.getBoundingClientRect();
|
|
257
|
+
const PW = 330;
|
|
258
|
+
const PH = 400;
|
|
259
|
+
const left = Math.max(8, Math.min(at.x - (r?.left ?? 0) - PW / 2, (r?.width ?? 800) - PW - 8));
|
|
260
|
+
const below = at.y - (r?.top ?? 0) + 16;
|
|
261
|
+
const top = Math.max(8, Math.min(below, (r?.height ?? 600) - PH - 8));
|
|
262
|
+
setPicker({ slot, left, top });
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const pickedKind = (kind) => {
|
|
266
|
+
const p = picker;
|
|
267
|
+
setPicker(null);
|
|
268
|
+
if (!p) return;
|
|
269
|
+
if (p.slot) onInsert?.(p.slot, kind);
|
|
270
|
+
else onAddAtSelection?.(kind);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/* ---- drawing ------------------------------------------------------- */
|
|
274
|
+
|
|
275
|
+
const edges = layout.edges.map((e) => {
|
|
276
|
+
const a = index.get(e.from);
|
|
277
|
+
const b = index.get(e.to);
|
|
278
|
+
return a && b ? { e, curve: curveBetween(a, b) } : null;
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const edgeKind = (id) => {
|
|
282
|
+
if (!overlay) return "n";
|
|
283
|
+
const s = overlay.edges.get(id) ?? "dim";
|
|
284
|
+
return s === "taken" ? "t" : s === "running" ? "r" : "d";
|
|
285
|
+
};
|
|
286
|
+
const stateOf = (n) => (overlay ? (overlay.nodes.get(n.id) ?? "notreached") : null);
|
|
287
|
+
|
|
288
|
+
const small = view.k < 0.45;
|
|
289
|
+
const showMap = size.w > MAP_W * 2 && size.h > MAP_H * 2.4;
|
|
290
|
+
const showPalette = editable && onAddAtSelection && size.w >= 560;
|
|
291
|
+
const dot = 24 * view.k;
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<div
|
|
295
|
+
ref={rootRef}
|
|
296
|
+
role="group"
|
|
297
|
+
aria-label="Automation canvas"
|
|
298
|
+
tabIndex={0}
|
|
299
|
+
onKeyDown={onKeyDown}
|
|
300
|
+
onPointerDown={onPointerDown}
|
|
301
|
+
onPointerMove={onPointerMove}
|
|
302
|
+
onPointerUp={onPointerUp}
|
|
303
|
+
onPointerCancel={onPointerUp}
|
|
304
|
+
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)}
|
|
305
|
+
>
|
|
306
|
+
{/* The dotted ground travels with the picture. */}
|
|
307
|
+
<span
|
|
308
|
+
aria-hidden="true"
|
|
309
|
+
className="pointer-events-none absolute inset-0 -z-10 opacity-30"
|
|
310
|
+
style={{
|
|
311
|
+
backgroundImage: "radial-gradient(circle, var(--color-muted-foreground) 1px, transparent 1.4px)",
|
|
312
|
+
backgroundSize: `${dot}px ${dot}px`,
|
|
313
|
+
backgroundPosition: `${view.x}px ${view.y}px`,
|
|
314
|
+
}}
|
|
315
|
+
/>
|
|
316
|
+
<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})` }}>
|
|
317
|
+
<svg className="pointer-events-none absolute left-0 top-0 overflow-visible" width="1" height="1" aria-hidden="true">
|
|
318
|
+
<defs>
|
|
319
|
+
{["n", "t", "r", "d"].map((k) => (
|
|
320
|
+
<marker key={k} id={`arr-${uid}-${k}`} viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
|
|
321
|
+
<path d="M 1 1 L 9 5 L 1 9 z" className={EDGE[k].head} />
|
|
322
|
+
</marker>
|
|
323
|
+
))}
|
|
324
|
+
</defs>
|
|
325
|
+
{edges.map((x) => {
|
|
326
|
+
if (!x) return null;
|
|
327
|
+
const k = edgeKind(x.e.id);
|
|
328
|
+
const toEnd = index.get(x.e.to)?.type === "end";
|
|
329
|
+
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})`} />;
|
|
330
|
+
})}
|
|
331
|
+
</svg>
|
|
332
|
+
|
|
333
|
+
{layout.groups.map((g) => {
|
|
334
|
+
const state = overlay ? (overlay.nodes.get(g.id) ?? "notreached") : null;
|
|
335
|
+
const sel = selectedId === g.stepId;
|
|
336
|
+
return (
|
|
337
|
+
<div
|
|
338
|
+
key={g.id}
|
|
339
|
+
style={{ left: g.x, top: g.y, width: g.w, height: g.h }}
|
|
340
|
+
className={cx(
|
|
341
|
+
"pointer-events-none absolute rounded-[20px] border-[1.5px] border-dashed bg-muted/55",
|
|
342
|
+
sel ? "border-solid border-foreground" : "border-muted-foreground/45",
|
|
343
|
+
(state === "done" || state === "fixed") && "border-success/70",
|
|
344
|
+
state === "would" && "border-primary/60",
|
|
345
|
+
state === "running" && "border-info",
|
|
346
|
+
state === "failed" && "border-destructive",
|
|
347
|
+
state === "notreached" && "opacity-50",
|
|
348
|
+
)}
|
|
349
|
+
>
|
|
350
|
+
<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">
|
|
351
|
+
<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">
|
|
352
|
+
<Glyph name="loop" size={14} />
|
|
353
|
+
<span className="truncate">{g.title}</span>
|
|
354
|
+
<span className="truncate font-medium text-muted-foreground">{g.sub}</span>
|
|
355
|
+
</button>
|
|
356
|
+
{onToggleCollapse ? (
|
|
357
|
+
<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">
|
|
358
|
+
<Icon name="chevron-down" size={14} />
|
|
359
|
+
</button>
|
|
360
|
+
) : null}
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
);
|
|
364
|
+
})}
|
|
365
|
+
|
|
366
|
+
{layout.nodes.map((n) => {
|
|
367
|
+
const state = stateOf(n);
|
|
368
|
+
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" />;
|
|
369
|
+
if (n.type === "port") {
|
|
370
|
+
return (
|
|
371
|
+
<i
|
|
372
|
+
key={n.id}
|
|
373
|
+
style={{ left: n.x, top: n.y, width: n.w, height: n.h }}
|
|
374
|
+
className={cx("absolute rounded border-2 border-background", state === "done" || state === "fixed" ? "bg-success" : state === "would" ? "bg-primary/60" : "bg-muted-foreground/70")}
|
|
375
|
+
/>
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
if (n.type === "end") {
|
|
379
|
+
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" />;
|
|
380
|
+
const slot = n.slot;
|
|
381
|
+
return (
|
|
382
|
+
<button
|
|
383
|
+
key={n.id}
|
|
384
|
+
type="button"
|
|
385
|
+
data-nopan
|
|
386
|
+
style={{ left: n.x, top: n.y, width: n.w, height: n.h }}
|
|
387
|
+
aria-label={`${n.title} ${slotWords(slot)}`}
|
|
388
|
+
title={n.title}
|
|
389
|
+
onClick={(ev) => openPicker(slot, { x: ev.clientX, y: ev.clientY })}
|
|
390
|
+
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"
|
|
391
|
+
>
|
|
392
|
+
<Icon name="plus" size={14} />
|
|
393
|
+
{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}
|
|
394
|
+
</button>
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
if (n.type === "card") {
|
|
398
|
+
const sel = selectedId === n.id;
|
|
399
|
+
return (
|
|
400
|
+
<div
|
|
401
|
+
key={n.id}
|
|
402
|
+
style={{ left: n.x, top: n.y, width: n.w, height: n.h }}
|
|
403
|
+
className={cx(
|
|
404
|
+
"absolute rounded-2xl border bg-card p-3 text-card-foreground",
|
|
405
|
+
sel && "outline outline-2 outline-offset-[3px] outline-foreground",
|
|
406
|
+
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",
|
|
407
|
+
)}
|
|
408
|
+
>
|
|
409
|
+
<i className="pointer-events-none absolute -left-1 top-[calc(50%_-_8px)] h-4 w-[7px] rounded-sm bg-border" />
|
|
410
|
+
<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" />
|
|
411
|
+
<div data-nopan className="flex items-center justify-between gap-2">
|
|
412
|
+
<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">
|
|
413
|
+
<Glyph name="loop" size={16} />
|
|
414
|
+
<b className="truncate">{n.title}</b>
|
|
415
|
+
</button>
|
|
416
|
+
<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">
|
|
417
|
+
<Icon name="chevron-right" size={14} /> Show steps
|
|
418
|
+
</button>
|
|
419
|
+
</div>
|
|
420
|
+
<p className="m-0 mb-2 mt-0.5 text-xs text-muted-foreground">{n.sub}</p>
|
|
421
|
+
<ol className="m-0 flex list-none flex-col gap-1.5 p-0">
|
|
422
|
+
{(n.items ?? []).map((it) => (
|
|
423
|
+
<li key={it.id} className="rounded-lg border border-border bg-background px-2.5 py-1 text-[12.5px]">
|
|
424
|
+
{it.title}
|
|
425
|
+
</li>
|
|
426
|
+
))}
|
|
427
|
+
{(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}
|
|
428
|
+
</ol>
|
|
429
|
+
</div>
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
// A step or the trigger: an icon tile with its words below.
|
|
433
|
+
const glyph = n.type === "trigger" ? triggerGlyph(n.triggerType) : (KINDS[n.kind]?.glyph ?? "action");
|
|
434
|
+
const selected = selectedId === (n.stepId ?? n.id);
|
|
435
|
+
const badgeIcon = state ? STATE_ICON[state] : undefined;
|
|
436
|
+
const bad = n.stepId ? flagged?.has(n.stepId) : false;
|
|
437
|
+
const sum = overlay && n.stepId ? overlay.summary.get(n.stepId) : null;
|
|
438
|
+
const price = !overlay && n.stepId ? prices?.get(n.stepId) : null;
|
|
439
|
+
return (
|
|
440
|
+
<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)}>
|
|
441
|
+
{n.type !== "trigger" ? <i className="pointer-events-none absolute -left-1 top-[calc(50%_-_8px)] h-4 w-[7px] rounded-sm bg-border" /> : null}
|
|
442
|
+
{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}
|
|
443
|
+
<button
|
|
444
|
+
type="button"
|
|
445
|
+
data-nopan
|
|
446
|
+
aria-pressed={selected}
|
|
447
|
+
aria-label={`${n.title}${state ? `. ${state === "would" ? "would run" : state}` : ""}`}
|
|
448
|
+
onClick={() => onSelect?.(n.stepId ?? n.id)}
|
|
449
|
+
className={cx(
|
|
450
|
+
"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",
|
|
451
|
+
n.type === "trigger" && "rounded-l-[36px]",
|
|
452
|
+
TONE[n.tone] ?? TONE.plain,
|
|
453
|
+
state ? RUNSTATE[state] : null,
|
|
454
|
+
bad && "border-destructive",
|
|
455
|
+
selected && "outline outline-2 outline-offset-[3px] outline-foreground",
|
|
456
|
+
)}
|
|
457
|
+
>
|
|
458
|
+
<Glyph name={glyph} size={26} />
|
|
459
|
+
{n.mark ? (
|
|
460
|
+
<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>
|
|
461
|
+
) : null}
|
|
462
|
+
</button>
|
|
463
|
+
{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}
|
|
464
|
+
{badgeIcon && state ? (
|
|
465
|
+
<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])}>
|
|
466
|
+
<Icon name={badgeIcon} size={11} className={state === "running" ? "animate-spin motion-reduce:animate-none" : undefined} />
|
|
467
|
+
</span>
|
|
468
|
+
) : null}
|
|
469
|
+
{bad && !badgeIcon ? (
|
|
470
|
+
<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">
|
|
471
|
+
!
|
|
472
|
+
</span>
|
|
473
|
+
) : null}
|
|
474
|
+
<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 }}>
|
|
475
|
+
<b className="break-words text-[13px] font-semibold leading-tight text-foreground [overflow-wrap:anywhere]">{n.title}</b>
|
|
476
|
+
{n.sub ? <span className="text-xs leading-snug text-muted-foreground [overflow-wrap:anywhere]">{n.sub}</span> : null}
|
|
477
|
+
{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}
|
|
478
|
+
{sum && sum.passes > 1 ? <span className="font-mono text-xs text-muted-foreground">{sum.passes} times</span> : null}
|
|
479
|
+
{sum?.fix ? <span className="text-xs text-primary">{fixTag(sum.fix)}</span> : null}
|
|
480
|
+
{sum && state === "failed" && sum.words ? <span className="text-xs text-destructive">{sum.words}</span> : null}
|
|
481
|
+
{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}
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
);
|
|
485
|
+
})}
|
|
486
|
+
|
|
487
|
+
{edges.map((x) => {
|
|
488
|
+
if (!x) return null;
|
|
489
|
+
const { e, curve } = x;
|
|
490
|
+
const state = overlay?.edges.get(e.id);
|
|
491
|
+
const dim = overlay && state === "dim";
|
|
492
|
+
const lab = e.label ? curve.at(0.24) : null;
|
|
493
|
+
return (
|
|
494
|
+
<span key={`m:${e.id}`}>
|
|
495
|
+
{lab ? (
|
|
496
|
+
<span
|
|
497
|
+
style={{ left: lab.x, top: lab.y }}
|
|
498
|
+
className={cx(
|
|
499
|
+
"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",
|
|
500
|
+
state === "taken" || state === "running" ? "border-success text-success" : "border-border text-muted-foreground",
|
|
501
|
+
dim && "opacity-50",
|
|
502
|
+
)}
|
|
503
|
+
>
|
|
504
|
+
{e.label}
|
|
505
|
+
</span>
|
|
506
|
+
) : null}
|
|
507
|
+
{editable && e.plus && e.slot ? (
|
|
508
|
+
<button
|
|
509
|
+
type="button"
|
|
510
|
+
data-nopan
|
|
511
|
+
style={{ left: curve.mid.x, top: curve.mid.y }}
|
|
512
|
+
aria-label={`Add a step ${slotWords(e.slot)}`}
|
|
513
|
+
title="Add a step"
|
|
514
|
+
onClick={(ev) => openPicker(e.slot, { x: ev.clientX, y: ev.clientY })}
|
|
515
|
+
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"
|
|
516
|
+
>
|
|
517
|
+
<Icon name="plus" size={12} />
|
|
518
|
+
</button>
|
|
519
|
+
) : null}
|
|
520
|
+
</span>
|
|
521
|
+
);
|
|
522
|
+
})}
|
|
523
|
+
</div>
|
|
524
|
+
|
|
525
|
+
<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">
|
|
526
|
+
<div role="group" aria-label="Zoom" className="flex items-center gap-px rounded-lg border border-border bg-card p-0.5">
|
|
527
|
+
<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">
|
|
528
|
+
<Icon name="minus" size={15} />
|
|
529
|
+
</button>
|
|
530
|
+
<span aria-live="polite" className="min-w-[46px] text-center font-mono text-xs text-card-foreground">
|
|
531
|
+
{Math.round(view.k * 100)}%
|
|
532
|
+
</span>
|
|
533
|
+
<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">
|
|
534
|
+
<Icon name="plus" size={15} />
|
|
535
|
+
</button>
|
|
536
|
+
<i className="mx-0.5 h-4 w-px bg-border" />
|
|
537
|
+
<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">
|
|
538
|
+
<Icon name="maximize" size={15} />
|
|
539
|
+
</button>
|
|
540
|
+
</div>
|
|
541
|
+
{info}
|
|
542
|
+
</div>
|
|
543
|
+
|
|
544
|
+
{editable && onAddAtSelection && !showPalette ? (
|
|
545
|
+
<button
|
|
546
|
+
type="button"
|
|
547
|
+
data-nopan
|
|
548
|
+
aria-label="Add a step"
|
|
549
|
+
title="Add a step"
|
|
550
|
+
onClick={(ev) => openPicker(null, { x: ev.clientX, y: ev.clientY })}
|
|
551
|
+
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"
|
|
552
|
+
>
|
|
553
|
+
<Icon name="plus" size={18} />
|
|
554
|
+
</button>
|
|
555
|
+
) : null}
|
|
556
|
+
|
|
557
|
+
{showPalette ? (
|
|
558
|
+
<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)" }}>
|
|
559
|
+
<PaletteBar onAdd={onAddAtSelection} />
|
|
560
|
+
</div>
|
|
561
|
+
) : null}
|
|
562
|
+
|
|
563
|
+
{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}
|
|
564
|
+
|
|
565
|
+
{picker ? <StepPicker style={{ left: picker.left, top: picker.top }} runnable={runnable} onClose={() => setPicker(null)} onPick={pickedKind} /> : null}
|
|
566
|
+
</div>
|
|
567
|
+
);
|
|
568
|
+
}
|