Give the pulse its own duration, and let the editor fill the screen
The shared duration tokens had been stretched to slow the pulses down, which also slowed every panel and dock animation and left `motion.ts` out of step with the CSS. Both are back to 200/300 ms, and the pulses use a new `--duration-pulse` (500 ms): a message arriving is a signal, not a state change, and it is the only thing here that should linger. The edge pulse read its own 300 ms constant, so it now takes the token through `motion.ts` and the two sides cannot drift again. The code editor also expands: the same node panel fills the content region beside the sidebar, the canvas chrome steps aside rather than floating on top, and the settings above the editor keep a readable width. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WzrvW7rjQbynnhF6pxh6i
This commit is contained in:
co-authored by
Claude Fable 5
parent
f05831b4a5
commit
aa0e760615
@@ -121,6 +121,8 @@ function FlowEditorInner({ flowName }: { flowName: string }) {
|
||||
const [inspected, setInspected] = useState<InspectedEdge | null>(null)
|
||||
const [rebind, setRebind] = useState<Rebind | null>(null)
|
||||
const [flowPanelOpen, setFlowPanelOpen] = useState(false)
|
||||
// The editor at full size covers the canvas, so its chrome steps aside.
|
||||
const [editorExpanded, setEditorExpanded] = useState(false)
|
||||
|
||||
const issues = detail.issues ?? []
|
||||
|
||||
@@ -234,7 +236,10 @@ function FlowEditorInner({ flowName }: { flowName: string }) {
|
||||
|
||||
const renameMutation = useMutation({
|
||||
mutationFn: (newName: string) =>
|
||||
FlowsService.renameFlow({ name: flowName, requestBody: { new_name: newName } }),
|
||||
FlowsService.renameFlow({
|
||||
name: flowName,
|
||||
requestBody: { new_name: newName },
|
||||
}),
|
||||
onSuccess: (detail) => {
|
||||
queryClient.invalidateQueries({ queryKey: flowKeys.all })
|
||||
setFlowPanelOpen(false)
|
||||
@@ -442,6 +447,7 @@ function FlowEditorInner({ flowName }: { flowName: string }) {
|
||||
}}
|
||||
onPaneClick={() => {
|
||||
setSelectedId(null)
|
||||
setEditorExpanded(false)
|
||||
setInspected(null)
|
||||
}}
|
||||
onEdgeClick={(event, edge) => {
|
||||
@@ -476,26 +482,30 @@ function FlowEditorInner({ flowName }: { flowName: string }) {
|
||||
<Background variant={BackgroundVariant.Dots} gap={24} size={1.5} />
|
||||
</ReactFlow>
|
||||
|
||||
<FlowTabs
|
||||
flows={flows.data}
|
||||
active={flowName}
|
||||
saving={saving.isPending}
|
||||
/>
|
||||
{editorExpanded ? null : (
|
||||
<FlowTabs
|
||||
flows={flows.data}
|
||||
active={flowName}
|
||||
saving={saving.isPending}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FlowDock
|
||||
issues={issues}
|
||||
running={runMutation.isPending}
|
||||
onAddNode={() => setPaletteOpen(true)}
|
||||
onEditFlow={() => {
|
||||
setSelectedId(null)
|
||||
setFlowPanelOpen(true)
|
||||
}}
|
||||
onRun={() => {
|
||||
flush()
|
||||
runMutation.mutate()
|
||||
}}
|
||||
onFocusNode={focusNode}
|
||||
/>
|
||||
{editorExpanded ? null : (
|
||||
<FlowDock
|
||||
issues={issues}
|
||||
running={runMutation.isPending}
|
||||
onAddNode={() => setPaletteOpen(true)}
|
||||
onEditFlow={() => {
|
||||
setSelectedId(null)
|
||||
setFlowPanelOpen(true)
|
||||
}}
|
||||
onRun={() => {
|
||||
flush()
|
||||
runMutation.mutate()
|
||||
}}
|
||||
onFocusNode={focusNode}
|
||||
/>
|
||||
)}
|
||||
|
||||
{definitions.length === 0 ? (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
@@ -534,12 +544,15 @@ function FlowEditorInner({ flowName }: { flowName: string }) {
|
||||
flow={flowName}
|
||||
nodeTypes={nodeTypeInfo ?? []}
|
||||
suggestions={suggestions}
|
||||
expanded={editorExpanded}
|
||||
onToggleExpand={() => setEditorExpanded((wide) => !wide)}
|
||||
onChange={updateNode}
|
||||
onSaveSource={(code) => {
|
||||
if (selected) sourceMutation.mutate({ nodeId: selected.id, code })
|
||||
}}
|
||||
onClose={() => {
|
||||
flush()
|
||||
setEditorExpanded(false)
|
||||
setSelectedId(null)
|
||||
}}
|
||||
onDelete={() => selected && deleteNodes([selected.id])}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "@xyflow/react"
|
||||
import { memo, useEffect, useRef, useState } from "react"
|
||||
|
||||
import { duration } from "@/lib/motion"
|
||||
import { cn } from "@/lib/utils"
|
||||
import type { FlowEdgeData } from "./deriveEdges"
|
||||
import { useLiveValue } from "./liveStore"
|
||||
@@ -53,7 +54,7 @@ function LiveEdgeComponent({
|
||||
if (!live?.ts || live.ts === lastTs.current) return
|
||||
lastTs.current = live.ts
|
||||
setPulsing(true)
|
||||
const timer = setTimeout(() => setPulsing(false), 300)
|
||||
const timer = setTimeout(() => setPulsing(false), duration.pulse * 1000)
|
||||
return () => clearTimeout(timer)
|
||||
}, [live?.ts])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { X } from "lucide-react"
|
||||
import { Maximize2, Minimize2, X } from "lucide-react"
|
||||
import { lazy, Suspense, useEffect, useRef, useState } from "react"
|
||||
|
||||
import type { DType, MessageSpec, NodeDef_Input, NodeTypeInfo } from "@/client"
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { nodeSourceQueryOptions } from "./queries"
|
||||
import { PANEL_SECTION, SidePanel } from "./SidePanel"
|
||||
|
||||
@@ -276,15 +277,19 @@ function PanelBody({
|
||||
flow,
|
||||
nodeType,
|
||||
suggestions,
|
||||
expanded,
|
||||
onChange,
|
||||
onSaveSource,
|
||||
onToggleExpand,
|
||||
}: {
|
||||
node: NodeDef_Input
|
||||
flow: string
|
||||
nodeType: NodeTypeInfo | undefined
|
||||
suggestions: PortSuggestions
|
||||
expanded: boolean
|
||||
onChange: (next: NodeDef_Input) => void
|
||||
onSaveSource: (code: string) => void
|
||||
onToggleExpand: () => void
|
||||
}) {
|
||||
const hasSource = nodeType?.has_source ?? node.type === "python"
|
||||
const { data: source } = useQuery({
|
||||
@@ -320,7 +325,7 @@ function PanelBody({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-5 p-4">
|
||||
<div className={cn("grid gap-5 p-4", expanded && "max-w-2xl")}>
|
||||
<PortList
|
||||
title="Consumes"
|
||||
specs={node.requires ?? []}
|
||||
@@ -346,7 +351,21 @@ function PanelBody({
|
||||
|
||||
{hasSource ? (
|
||||
<div className="flex min-h-[280px] flex-1 flex-col gap-2 px-4 pb-4">
|
||||
<span className={SECTION}>Code</span>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={SECTION}>Code</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="hidden text-muted-foreground md:inline-flex"
|
||||
onClick={onToggleExpand}
|
||||
aria-label={
|
||||
expanded ? "Collapse the editor" : "Expand the editor"
|
||||
}
|
||||
data-testid="toggle-editor-size"
|
||||
>
|
||||
{expanded ? <Minimize2 /> : <Maximize2 />}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-hidden rounded-md border border-border">
|
||||
<Suspense
|
||||
fallback={
|
||||
@@ -377,8 +396,10 @@ export function NodePanel({
|
||||
flow,
|
||||
nodeTypes,
|
||||
suggestions,
|
||||
expanded,
|
||||
onChange,
|
||||
onSaveSource,
|
||||
onToggleExpand,
|
||||
onClose,
|
||||
onDelete,
|
||||
}: {
|
||||
@@ -386,8 +407,10 @@ export function NodePanel({
|
||||
flow: string
|
||||
nodeTypes: NodeTypeInfo[]
|
||||
suggestions: PortSuggestions
|
||||
expanded: boolean
|
||||
onChange: (next: NodeDef_Input) => void
|
||||
onSaveSource: (code: string) => void
|
||||
onToggleExpand: () => void
|
||||
onClose: () => void
|
||||
onDelete: () => void
|
||||
}) {
|
||||
@@ -399,6 +422,7 @@ export function NodePanel({
|
||||
label="Node settings"
|
||||
testId="node-panel"
|
||||
bodyKey={node?.id ?? "none"}
|
||||
expanded={expanded}
|
||||
onClose={onClose}
|
||||
header={
|
||||
node ? (
|
||||
@@ -429,8 +453,10 @@ export function NodePanel({
|
||||
flow={flow}
|
||||
nodeType={nodeType}
|
||||
suggestions={suggestions}
|
||||
expanded={expanded}
|
||||
onChange={onChange}
|
||||
onSaveSource={onSaveSource}
|
||||
onToggleExpand={onToggleExpand}
|
||||
/>
|
||||
) : null}
|
||||
</SidePanel>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button"
|
||||
import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet"
|
||||
import { useIsMobile } from "@/hooks/useMobile"
|
||||
import { duration, easeEmphasized, easeStandard } from "@/lib/motion"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
/** Same grammar as the shared `slideUp`, on the axis this panel travels. */
|
||||
const panelSlide = {
|
||||
@@ -38,6 +39,7 @@ export function SidePanel({
|
||||
label,
|
||||
testId,
|
||||
bodyKey,
|
||||
expanded = false,
|
||||
header,
|
||||
footer,
|
||||
children,
|
||||
@@ -49,6 +51,8 @@ export function SidePanel({
|
||||
testId: string
|
||||
/** Remounts the contents when the thing being edited changes. */
|
||||
bodyKey: string
|
||||
/** Fill the content area instead of floating beside the canvas. */
|
||||
expanded?: boolean
|
||||
header: ReactNode
|
||||
footer?: ReactNode
|
||||
children: ReactNode
|
||||
@@ -125,7 +129,14 @@ export function SidePanel({
|
||||
role="complementary"
|
||||
aria-label={label}
|
||||
data-testid={testId}
|
||||
className="pointer-events-auto absolute inset-y-4 right-4 z-10 flex w-[400px] flex-col overflow-hidden rounded-lg border border-border bg-card/80 shadow-e2 backdrop-blur-md"
|
||||
data-expanded={expanded || undefined}
|
||||
className={cn(
|
||||
"pointer-events-auto absolute z-20 flex flex-col overflow-hidden border border-border bg-card/80 shadow-e2 backdrop-blur-md",
|
||||
expanded
|
||||
? // Fills the content region, which already starts after the sidebar.
|
||||
"inset-0 rounded-none"
|
||||
: "inset-y-4 right-4 w-[400px] rounded-lg",
|
||||
)}
|
||||
>
|
||||
{contents}
|
||||
</motion.aside>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
/* A message arriving lights its edge, then decays back to rest. */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.react-flow__edge-path.edge-live {
|
||||
animation: edge-pulse var(--duration-slow) var(--ease-emphasized);
|
||||
animation: edge-pulse var(--duration-pulse) var(--ease-emphasized);
|
||||
}
|
||||
|
||||
@keyframes edge-pulse {
|
||||
@@ -58,7 +58,7 @@
|
||||
border: 2px solid var(--primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent);
|
||||
pointer-events: none;
|
||||
animation: node-pulse var(--duration-slow) var(--ease-emphasized) forwards;
|
||||
animation: node-pulse var(--duration-pulse) var(--ease-emphasized) forwards;
|
||||
}
|
||||
|
||||
@keyframes node-pulse {
|
||||
|
||||
Reference in New Issue
Block a user