import { useReactFlow } from "@xyflow/react" import { AlertCircle, Check, Loader2, Maximize2, Pause, Pencil, Play, PlayCircle, Plus, StepForward, WifiOff, X, ZoomIn, ZoomOut, } from "lucide-react" import { motion } from "motion/react" import type { ValidationIssue } from "@/client" import { Button } from "@/components/ui/button" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { Separator } from "@/components/ui/separator" import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip" import { slideUp, transitions } from "@/lib/motion" import { cn } from "@/lib/utils" import { type LogsFilter, LogsPanel } from "./LogsPanel" import { useLiveConnection } from "./liveStore" /** * What "fit" means on this canvas: the view a flow opens with, and the one the * fit button returns to. The generous padding keeps the graph clear of the * chrome floating over it, and the cap stops a two-node flow from being blown * up past legibility. */ export const FIT_VIEW = { padding: 0.25, maxZoom: 1.2 } /** * The action bar, floating bottom-centre. Run is the one brand-secondary * affordance on this view; everything else stays quiet. * * Everything the flow bar used to carry is here too — whether the work is * saved, the flow's own settings, and putting it live or throwing it away — * so the top of the canvas is left to say which flow this is. * * It wraps rather than overflows, and drops the zoom controls on a phone; see * DESIGN-GUIDELINES.md → Responsive. */ export function FlowDock({ flow, issues, running, enabled, paused, saving, hasDraft, publishing, logs, onAddNode, onRun, onTogglePause, onStep, stepping, onFocusNode, onEditFlow, onPublish, onDiscard, }: { flow: string issues: ValidationIssue[] running: boolean enabled: boolean paused: boolean saving: boolean hasDraft: boolean publishing: boolean /** Owned by the editor, because a failing node can open it too. */ logs: LogsFilter onAddNode: () => void onRun: () => void onTogglePause: () => void onStep: () => void stepping: boolean onFocusNode: (nodeId: string) => void onEditFlow: () => void onPublish: () => void onDiscard: () => void }) { const { zoomIn, zoomOut, fitView } = useReactFlow() const connected = useLiveConnection() return ( Add a node (⌘K) {/* A phone pinches to zoom and the graph fits itself, so these three would only be taking room the rest of the bar needs. */} Fit to screen {issues.length > 0 ? ( <>

Needs attention

    {issues.map((issue) => (
  • ))}
) : null} {!enabled ? "This flow is stopped" : paused ? "Let the flow carry on" : "Hold the nodes; values still arrive"} {paused && enabled ? ( Let one held-back item through ) : null} {enabled ? "Run every node once" : "Start the flow to run it"} Flow settings {/* Throwing the edit away sits next to putting it live, and only exists while there is something to throw away. */} {hasDraft ? ( Discard the unpublished changes ) : null} {/* * Saved state and publish are one control: the glyph never moves, it * simply stops being something you can press once there is nothing left * to put live. A button that appears and disappears moved everything * beside it just as the work was finished. */} {!connected ? "Reconnecting to the engine" : publishing ? "Publishing" : saving ? "Saving" : hasDraft ? "Saved — publish to put it live" : "All changes saved"}
) }