From 721b89f5ebc36dc8de758c7a1db33303e14b2f3e Mon Sep 17 00:00:00 2001 From: stroblme Date: Thu, 27 Aug 2026 16:15:26 +0200 Subject: [PATCH] Un-nest the dock panels: crisp in Firefox, and side by side MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The console panel lived inside the bar, so its centring translate and its backdrop filter were both nested inside the bar's own — a half-pixel offset under a blurred layer, which is what Gecko was rasterising the text through. Panels and bar are siblings in one column now. That column is also what the issues list needed: it was a popover anchored to its trigger, opaque where the console is frosted and landing on top of it when both were open. It is the same panel now, the same width, in the same row. Co-Authored-By: Claude Opus 5 (1M context) --- frontend/src/components/Flow/FlowDock.tsx | 531 ++++++++++++--------- frontend/src/components/Flow/LogsPanel.tsx | 229 +++++---- 2 files changed, 420 insertions(+), 340 deletions(-) diff --git a/frontend/src/components/Flow/FlowDock.tsx b/frontend/src/components/Flow/FlowDock.tsx index d0d06dd..df6003f 100644 --- a/frontend/src/components/Flow/FlowDock.tsx +++ b/frontend/src/components/Flow/FlowDock.tsx @@ -13,15 +13,11 @@ import { WifiOff, X, } from "lucide-react" -import { motion } from "motion/react" +import { AnimatePresence, motion } from "motion/react" +import { useEffect, useState } from "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, @@ -30,7 +26,7 @@ import { } from "@/components/ui/tooltip" import { slideUp, transitions } from "@/lib/motion" import { cn } from "@/lib/utils" -import { type LogsFilter, LogsPanel } from "./LogsPanel" +import { type LogsFilter, LogsPanel, LogsTrigger, PANEL } from "./LogsPanel" import { useLiveConnection } from "./liveStore" /** @@ -110,271 +106,336 @@ export function FlowDock({ }) { const { fitView } = useReactFlow() const connected = useLiveConnection() + const [issuesOpen, setIssuesOpen] = useState(false) // The engine marks the issues it does not count against a flow. They are // still worth saying, so they stay in the list — but in the muted tone, and // without turning the summary red, since nothing here is actually broken. const faults = issues.filter((issue) => !issue.advisory) return ( - - - - - - Add a node (⌘K) - +
+ setIssuesOpen(false)} + onFocusNode={onFocusNode} + /> + +
- + + + + + + Add a node (⌘K) + - {/* A phone pinches to zoom and the graph fits itself, so this would only + + + {/* A phone pinches to zoom and the graph fits itself, so this would only be taking room the rest of the bar needs. */} - - - - - Fit to screen - + + + + + Fit to screen + - {issues.length > 0 ? ( - <> - - - + {issues.length > 0 ? ( + <> + + + + ) : null} + + + + + + + + + + + {!enabled + ? "This flow is stopped" + : paused + ? "Let the flow carry on" + : "Hold the nodes; values still arrive"} + + + + {paused && enabled ? ( + + - - -

- Needs attention -

-
    - {issues.map((issue) => ( -
  • - -
  • - ))} -
-
-
- - ) : null} + + Let one held-back item through + + ) : null} - + + + + + + + + {enabled ? "Run every node once" : "Start the flow to run it"} + + - + - - - - - - {!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 + Flow settings - ) : 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 + {/* Throwing the edit away sits next to putting it live, and only exists while there is something to throw away. */} - {hasDraft ? ( + {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. + */} - + + + - Discard the unpublished changes + + {!connected + ? "Reconnecting to the engine" + : publishing + ? "Publishing" + : saving + ? "Saving" + : hasDraft + ? "Saved — publish to put it live" + : "All changes saved"} + +
+ + ) +} + +/** + * What stops this flow running, or is merely worth knowing — the same chrome as + * the logs, so a canvas with both open shows one pair rather than a panel and a + * popover disagreeing about what a panel looks like. + * + * Not a popover for the same reason the logs are not: clicking a node is what + * you do while reading this, since each line points at one. + */ +function IssuesPanel({ + open, + issues, + onClose, + onFocusNode, +}: { + open: boolean + issues: ValidationIssue[] + onClose: () => void + onFocusNode: (nodeId: string) => void +}) { + useEffect(() => { + if (!open) return + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") onClose() + } + window.addEventListener("keydown", onKeyDown) + return () => window.removeEventListener("keydown", onKeyDown) + }, [open, onClose]) + + return ( + + {open && issues.length > 0 ? ( + +
+

+ Needs attention +

+
+
    + {issues.map((issue) => ( +
  • + +
  • + ))} +
+
) : 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"} - - -
+ ) } diff --git a/frontend/src/components/Flow/LogsPanel.tsx b/frontend/src/components/Flow/LogsPanel.tsx index ff51558..849e80d 100644 --- a/frontend/src/components/Flow/LogsPanel.tsx +++ b/frontend/src/components/Flow/LogsPanel.tsx @@ -13,6 +13,13 @@ import { slideUp, transitions } from "@/lib/motion" import { cn } from "@/lib/utils" import { liveStore, useLiveLogs } from "./liveStore" +/** + * The chrome a panel over this canvas wears: frosted, bordered, and the same + * width whichever of them is open, so two side by side read as one pair. + */ +export const PANEL = + "pointer-events-auto w-[30rem] max-w-[calc(100vw-2rem)] rounded-lg border border-border bg-card/80 shadow-e2 backdrop-blur-md" + function shortTime(ts: number): string { return new Date(ts * 1000).toLocaleTimeString(undefined, { hour12: false, @@ -36,6 +43,38 @@ export type LogsFilter = { onClearNode: () => void } +/** + * The dock button that opens the logs. Separate from the panel because the two + * sit in different rows: the trigger belongs in the bar, the panel above it, + * beside whatever else is open. + */ +export function LogsTrigger({ + open, + onOpenChange, +}: Pick) { + return ( + + + + + What this flow printed + + ) +} + /** * What the nodes of this flow printed, and the tracebacks of the ones that * failed — the detail the one-line error bubble on a node has no room for. @@ -75,112 +114,92 @@ export function LogsPanel({ }, [open, onOpenChange]) return ( - <> - - - - - What this flow printed - - - - {open ? ( - // A sibling of the button but positioned against the dock, so it - // sits centred above the whole bar and clears it by `mb-3` however - // many rows the bar wrapped into. Deliberately not a popover: a - // click on the canvas is what you do *while* reading the logs, so - // only the button or Escape puts them away. - -
-
-

- Logs -

- {node ? ( - - ) : null} -
- -
- - {lines.length === 0 ? ( -

- {node - ? `Nothing from ${node} yet.` - : "Nothing yet. Anything a node prints shows up here."} + + {open ? ( + // Laid out by the dock's panel row rather than positioned against the + // bar, so it can sit beside the issues list instead of over it — and + // so neither this nor the bar nests a half-pixel translate or a + // backdrop filter inside the other, which is what Firefox was + // rasterising the text through. Deliberately not a popover: a click on + // the canvas is what you do *while* reading the logs, so only the + // button or Escape puts them away. + +

+
+

+ Logs

- ) : ( - -
    - {lines.map((line, index) => ( -
  • + {node} + + + ) : null} +
+ +
+ + {lines.length === 0 ? ( +

+ {node + ? `Nothing from ${node} yet.` + : "Nothing yet. Anything a node prints shows up here."} +

+ ) : ( + +
    + {lines.map((line, index) => ( +
  • + + {shortTime(line.ts)}{" "} + + {nodeLabel(line.node, flow)} + + + - - {shortTime(line.ts)}{" "} - - {nodeLabel(line.node, flow)} - - - - {line.text.replace(/\n+$/, "")} - {line.truncated ? "\n… truncated" : ""} - -
  • - ))} -
  • -
-
- )} -
- ) : null} -
- + {line.text.replace(/\n+$/, "")} + {line.truncated ? "\n… truncated" : ""} + + + ))} +
  • + + + )} + + ) : null} + ) }