Computed flow layout, and mobile written into the design
The canvas lays itself out: a layered graph, left to right on a desktop and top to bottom on a phone, with room reserved for the value each edge carries. Nodes cannot be dragged and `NodeDef.position` is gone from the document — a graph nobody can arrange is one worth keeping small, which is what keeps flows atomic. Endpoints join the same layout, so their lanes and the localStorage that remembered where they were dragged go too. Mobile, per the new Responsive section of DESIGN-GUIDELINES.md: the dock caps its width and wraps instead of running off the screen, the dashboard stacks into one column rather than shrinking a wall panel to a fifth of its size, and Home stops widening its grid track past the viewport. A Playwright project at a phone's width fails the build when a screen no longer fits. Along the way: publish is the checkmark that was already there rather than a button that appears and disappears, with discard beside it on both the flow and the dashboard; the brain reveals a neuron's name on the first tap; and the port sparklines get room to breathe. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDSXaRhvqHYNevgDGmNAto
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
Plus,
|
||||
StepForward,
|
||||
WifiOff,
|
||||
X,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
} from "lucide-react"
|
||||
@@ -47,8 +48,11 @@ export const FIT_VIEW = { padding: 0.25, maxZoom: 1.2 }
|
||||
* 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 — so the top of the
|
||||
* canvas is left to say which flow this 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,
|
||||
@@ -68,6 +72,7 @@ export function FlowDock({
|
||||
onFocusNode,
|
||||
onEditFlow,
|
||||
onPublish,
|
||||
onDiscard,
|
||||
}: {
|
||||
flow: string
|
||||
issues: ValidationIssue[]
|
||||
@@ -87,6 +92,7 @@ export function FlowDock({
|
||||
onFocusNode: (nodeId: string) => void
|
||||
onEditFlow: () => void
|
||||
onPublish: () => void
|
||||
onDiscard: () => void
|
||||
}) {
|
||||
const { zoomIn, zoomOut, fitView } = useReactFlow()
|
||||
const connected = useLiveConnection()
|
||||
@@ -98,7 +104,10 @@ export function FlowDock({
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
transition={transitions.emphasized}
|
||||
className="pointer-events-auto absolute bottom-4 left-1/2 z-10 flex -translate-x-1/2 items-center gap-1 rounded-full border border-border bg-card/80 px-1.5 py-1 shadow-e2 backdrop-blur-md pb-[max(0.25rem,env(safe-area-inset-bottom))]"
|
||||
// Capped and wrapping: the canvas shell clips, so an uncapped row would
|
||||
// put the buttons at its ends out of reach on a phone rather than merely
|
||||
// look wrong. See DESIGN-GUIDELINES.md → Responsive.
|
||||
className="pointer-events-auto absolute bottom-4 left-1/2 z-10 flex max-w-[calc(100vw-2rem)] -translate-x-1/2 flex-wrap items-center justify-center gap-1 rounded-full border border-border bg-card/80 px-1.5 py-1 shadow-e2 backdrop-blur-md pb-[max(0.25rem,env(safe-area-inset-bottom))]"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -116,12 +125,17 @@ export function FlowDock({
|
||||
<TooltipContent>Add a node (⌘K)</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Separator orientation="vertical" className="mx-0.5 !h-5" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-0.5 !h-5 hidden md:block"
|
||||
/>
|
||||
|
||||
{/* A phone pinches to zoom and the graph fits itself, so these three
|
||||
would only be taking room the rest of the bar needs. */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-11 text-muted-foreground md:size-8"
|
||||
className="hidden text-muted-foreground md:inline-flex md:size-8"
|
||||
onClick={() => zoomOut()}
|
||||
aria-label="Zoom out"
|
||||
>
|
||||
@@ -132,7 +146,7 @@ export function FlowDock({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-11 text-muted-foreground md:size-8"
|
||||
className="hidden text-muted-foreground md:inline-flex md:size-8"
|
||||
onClick={() => fitView({ ...FIT_VIEW, duration: 300 })}
|
||||
aria-label="Fit the flow to the screen"
|
||||
>
|
||||
@@ -144,7 +158,7 @@ export function FlowDock({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-11 text-muted-foreground md:size-8"
|
||||
className="hidden text-muted-foreground md:inline-flex md:size-8"
|
||||
onClick={() => zoomIn()}
|
||||
aria-label="Zoom in"
|
||||
>
|
||||
@@ -153,7 +167,10 @@ export function FlowDock({
|
||||
|
||||
{issues.length > 0 ? (
|
||||
<>
|
||||
<Separator orientation="vertical" className="mx-0.5 !h-5" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-0.5 !h-5 hidden md:block"
|
||||
/>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
@@ -189,7 +206,10 @@ export function FlowDock({
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<Separator orientation="vertical" className="mx-0.5 !h-5" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-0.5 !h-5 hidden md:block"
|
||||
/>
|
||||
|
||||
<LogsPanel flow={flow} {...logs} />
|
||||
|
||||
@@ -263,7 +283,10 @@ export function FlowDock({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Separator orientation="vertical" className="mx-0.5 !h-5" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-0.5 !h-5 hidden md:block"
|
||||
/>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -281,41 +304,66 @@ export function FlowDock({
|
||||
<TooltipContent>Flow settings</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Throwing the edit away sits next to putting it live, and only exists
|
||||
while there is something to throw away. */}
|
||||
{hasDraft ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-11 text-muted-foreground md:size-8"
|
||||
onClick={onDiscard}
|
||||
aria-label="Discard the unpublished changes"
|
||||
data-testid="discard-draft"
|
||||
>
|
||||
<X />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Discard the unpublished changes</TooltipContent>
|
||||
</Tooltip>
|
||||
) : 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.
|
||||
*/}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="flex size-8 shrink-0 items-center justify-center text-muted-foreground">
|
||||
{!connected ? (
|
||||
<WifiOff className="size-3.5" />
|
||||
) : saving ? (
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
) : (
|
||||
<Check className="size-3.5" />
|
||||
)}
|
||||
<span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-11 text-muted-foreground md:size-8"
|
||||
onClick={onPublish}
|
||||
disabled={!hasDraft || publishing || saving || !connected}
|
||||
aria-label="Publish this flow"
|
||||
data-testid="publish-flow"
|
||||
>
|
||||
{!connected ? (
|
||||
<WifiOff className="size-3.5" />
|
||||
) : saving || publishing ? (
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
) : (
|
||||
<Check className="size-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{!connected
|
||||
? "Reconnecting to the engine"
|
||||
: saving
|
||||
? "Saving"
|
||||
: hasDraft
|
||||
? "Saved — publish to put it live"
|
||||
: "All changes saved"}
|
||||
: publishing
|
||||
? "Publishing"
|
||||
: saving
|
||||
? "Saving"
|
||||
: hasDraft
|
||||
? "Saved — publish to put it live"
|
||||
: "All changes saved"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{hasDraft ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-11 shrink-0 rounded-full md:h-8"
|
||||
onClick={onPublish}
|
||||
disabled={publishing}
|
||||
data-testid="publish-flow"
|
||||
>
|
||||
{publishing ? "Publishing…" : "Publish"}
|
||||
</Button>
|
||||
) : null}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user