Grow the flow editor in from the centre, hide the empty brain band

The editor's mount-time fitView animated from React Flow's default viewport,
which read as the graph swiping in from the corner on every open. The first
fit is instant now, later ones stay animated, and a scaleIn wrapper gives the
same entrance the brain view has — with a re-measure on completion so the
handle bounds are not stored mid-scale.

Home only renders the brain band once some flow has nodes, so a fresh install
no longer reserves a screenful of empty space above the flows card.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTsT1isxUjw5gtkJk8WhuA
This commit is contained in:
2026-08-20 08:43:16 +02:00
co-authored by Claude Opus 5
parent 010e0c7e9b
commit a9f91db619
2 changed files with 97 additions and 61 deletions
+34 -1
View File
@@ -20,6 +20,7 @@ import {
} from "@tanstack/react-query"
import { useNavigate } from "@tanstack/react-router"
import { Workflow } from "lucide-react"
import { motion } from "motion/react"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import {
@@ -40,6 +41,7 @@ import {
} from "@/components/ui/dialog"
import useCustomToast from "@/hooks/useCustomToast"
import { useIsMobile } from "@/hooks/useMobile"
import { scaleIn } from "@/lib/motion"
import { inCodeEditor, useShortcuts } from "@/lib/shortcuts"
import { cn } from "@/lib/utils"
import { CanvasTitle } from "./CanvasTitle"
@@ -507,9 +509,18 @@ function FlowEditorInner({
// A relayout can put a new node outside the viewport, and turning the graph
// on its side moves everything. Both want the whole flow back in view.
// The first fit is instant: an animated one travels from React Flow's
// default viewport to the content, which is the whole flow visibly sliding
// in from the corner every time one is opened. Later fits move from
// somewhere the user was already looking, so those stay animated.
const fitted = useRef(false)
// biome-ignore lint/correctness/useExhaustiveDependencies: refit when the shape changes, not on every render.
useEffect(() => {
fitView({ ...FIT_VIEW, duration: 300 })
const frame = requestAnimationFrame(() => {
fitView(fitted.current ? { ...FIT_VIEW, duration: 300 } : FIT_VIEW)
fitted.current = true
})
return () => cancelAnimationFrame(frame)
}, [direction, definitions.length, external.nodes.length, fitView])
const runMutation = useMutation({
@@ -877,6 +888,27 @@ function FlowEditorInner({
return (
<>
{/*
* Grows in from the centre rather than arriving mid-pan, the same
* entrance the brain view uses. React Flow reads a node's handle bounds
* out of the DOM once and never again, and a reading taken mid-scale is
* stored a few percent short for good — `LiveEdge` draws off the
* `sourceX`/`targetX` that come from those handles, so every edge would
* land short of its port forever. Remeasuring once the wrapper is back
* at `scale: 1` is what makes the entrance safe.
*/}
<motion.div
variants={scaleIn}
initial="hidden"
animate="visible"
onAnimationComplete={() =>
updateNodeInternals([
...definitions.map((node) => node.id),
...external.nodes.map((node) => node.id),
])
}
className="h-full w-full"
>
<ReactFlow
nodes={shownNodes}
edges={edges}
@@ -939,6 +971,7 @@ function FlowEditorInner({
>
<CanvasBackground />
</ReactFlow>
</motion.div>
{/*
* The bars keep their own lane rather than making way for a panel: they
+4 -1
View File
@@ -116,7 +116,10 @@ function Dashboard() {
// Every section here is free to shrink instead. See DESIGN-GUIDELINES.md
// → Responsive.
<div className="grid gap-6 [&>*]:min-w-0">
<BrainView />
{/* Nothing wired up yet means nothing to draw, and the band would still
hold a screenful of empty space above the flows card. Node count
rather than flow count: a flow made a minute ago has none. */}
{flows.some((flow) => (flow.node_count ?? 0) > 0) ? <BrainView /> : null}
<Card className="gap-0 py-0">
{isPending ? (