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:
@@ -20,6 +20,7 @@ import {
|
|||||||
} from "@tanstack/react-query"
|
} from "@tanstack/react-query"
|
||||||
import { useNavigate } from "@tanstack/react-router"
|
import { useNavigate } from "@tanstack/react-router"
|
||||||
import { Workflow } from "lucide-react"
|
import { Workflow } from "lucide-react"
|
||||||
|
import { motion } from "motion/react"
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -40,6 +41,7 @@ import {
|
|||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import useCustomToast from "@/hooks/useCustomToast"
|
import useCustomToast from "@/hooks/useCustomToast"
|
||||||
import { useIsMobile } from "@/hooks/useMobile"
|
import { useIsMobile } from "@/hooks/useMobile"
|
||||||
|
import { scaleIn } from "@/lib/motion"
|
||||||
import { inCodeEditor, useShortcuts } from "@/lib/shortcuts"
|
import { inCodeEditor, useShortcuts } from "@/lib/shortcuts"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { CanvasTitle } from "./CanvasTitle"
|
import { CanvasTitle } from "./CanvasTitle"
|
||||||
@@ -507,9 +509,18 @@ function FlowEditorInner({
|
|||||||
|
|
||||||
// A relayout can put a new node outside the viewport, and turning the graph
|
// 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.
|
// 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.
|
// biome-ignore lint/correctness/useExhaustiveDependencies: refit when the shape changes, not on every render.
|
||||||
useEffect(() => {
|
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])
|
}, [direction, definitions.length, external.nodes.length, fitView])
|
||||||
|
|
||||||
const runMutation = useMutation({
|
const runMutation = useMutation({
|
||||||
@@ -877,6 +888,27 @@ function FlowEditorInner({
|
|||||||
|
|
||||||
return (
|
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
|
<ReactFlow
|
||||||
nodes={shownNodes}
|
nodes={shownNodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
@@ -939,6 +971,7 @@ function FlowEditorInner({
|
|||||||
>
|
>
|
||||||
<CanvasBackground />
|
<CanvasBackground />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
* The bars keep their own lane rather than making way for a panel: they
|
* The bars keep their own lane rather than making way for a panel: they
|
||||||
|
|||||||
@@ -116,7 +116,10 @@ function Dashboard() {
|
|||||||
// Every section here is free to shrink instead. See DESIGN-GUIDELINES.md
|
// Every section here is free to shrink instead. See DESIGN-GUIDELINES.md
|
||||||
// → Responsive.
|
// → Responsive.
|
||||||
<div className="grid gap-6 [&>*]:min-w-0">
|
<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">
|
<Card className="gap-0 py-0">
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user