The flow toolbar beside a panel sheds by the lane it sits in: below 440px the fit control, the separators and Run's caption, below 336px the console. Below 988px wide the sidebar folds while a panel is open, without touching the saved preference, and comes back when it closes; the view is aimed again once the sidebar has moved
Docs / docs (push) Successful in 1m33s
Playwright Tests / test-playwright (1, 2) (push) Successful in 4m28s
Playwright Tests / test-playwright (2, 2) (push) Successful in 3m9s
pre-commit / pre-commit (push) Successful in 3m36s
Test Backend / test-backend (3.12) (push) Failing after 6m45s
Test Backend / test-backend (3.14) (push) Failing after 5m51s
Compose Smoke Test / test-compose (push) Successful in 30s
Playwright Tests / merge-reports (push) Successful in 1m23s
Docs / docs (push) Successful in 1m33s
Playwright Tests / test-playwright (1, 2) (push) Successful in 4m28s
Playwright Tests / test-playwright (2, 2) (push) Successful in 3m9s
pre-commit / pre-commit (push) Successful in 3m36s
Test Backend / test-backend (3.12) (push) Failing after 6m45s
Test Backend / test-backend (3.14) (push) Failing after 5m51s
Compose Smoke Test / test-compose (push) Successful in 30s
Playwright Tests / merge-reports (push) Successful in 1m23s
This commit is contained in:
@@ -56,9 +56,12 @@ export function Dock({
|
||||
}
|
||||
|
||||
/** Groups the dock's buttons; a phone wraps instead. */
|
||||
export function DockSeparator() {
|
||||
export function DockSeparator({ className }: { className?: string }) {
|
||||
return (
|
||||
<Separator orientation="vertical" className="mx-0.5 !h-5 hidden md:block" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className={cn("mx-0.5 !h-5 hidden md:block", className)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,11 @@ export const FIT_VIEW_PANEL = {
|
||||
padding: { top: "10%", bottom: "10%", left: "10%", right: "432px" } as const,
|
||||
}
|
||||
|
||||
/** Hidden in a lane narrower than the whole bar: the phone's cuts. */
|
||||
const SHED = "md:@max-[27.5rem]:hidden"
|
||||
/** Hidden in a lane narrower than that shorter bar. */
|
||||
const SHED_MORE = "md:@max-[21rem]:hidden"
|
||||
|
||||
/**
|
||||
* The action bar, floating bottom-centre. Run is the one brand-secondary
|
||||
* affordance on this view; everything else stays quiet.
|
||||
@@ -64,6 +69,11 @@ export const FIT_VIEW_PANEL = {
|
||||
* On a phone it is a bar across the bottom edge, which drops the fit control
|
||||
* and the caption on Run to leave the row room; otherwise it floats and wraps
|
||||
* rather than overflows. See DESIGN-GUIDELINES.md → Responsive.
|
||||
*
|
||||
* Beside a panel it is measured against the lane it sits in, not the screen.
|
||||
* A lane too narrow for the editing bar (the issues badge and discard showing)
|
||||
* sheds what a phone sheds, then the console. Below that the editor folds the
|
||||
* sidebar away while the panel is open (`CRAMPED` in FlowEditor).
|
||||
*/
|
||||
export function FlowDock({
|
||||
flow,
|
||||
@@ -120,13 +130,14 @@ export function FlowDock({
|
||||
// half-pixel centring translate or its backdrop filter inside the other —
|
||||
// which is what Firefox was drawing the console's text through. It also
|
||||
// gives the two panels a row to share, instead of one landing on the other.
|
||||
// The column spans the canvas lane, which is what `SHED` measures.
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-x-0 bottom-0 z-10 flex flex-col items-center md:inset-x-auto md:bottom-4 md:left-1/2 md:w-max md:max-w-[calc(100vw-2rem)] md:-translate-x-1/2",
|
||||
"@container pointer-events-none absolute inset-x-0 bottom-0 z-10 flex flex-col items-center md:bottom-4",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="mb-3 flex max-w-[calc(100vw-2rem)] flex-wrap items-end justify-center gap-3">
|
||||
<div className="mb-3 flex max-w-[calc(100vw-2rem)] flex-wrap items-end justify-center gap-3 md:w-max">
|
||||
<IssuesPanel
|
||||
open={issuesOpen}
|
||||
issues={issues}
|
||||
@@ -153,7 +164,7 @@ export function FlowDock({
|
||||
<TooltipContent>Add a node (⌘P)</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<DockSeparator />
|
||||
<DockSeparator className={SHED} />
|
||||
|
||||
{/* A phone pinches to zoom and the graph fits itself, so this would only
|
||||
be taking room the rest of the bar needs. */}
|
||||
@@ -162,7 +173,10 @@ export function FlowDock({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-touch"
|
||||
className="hidden text-muted-foreground md:inline-flex"
|
||||
className={cn(
|
||||
"hidden text-muted-foreground md:inline-flex",
|
||||
SHED,
|
||||
)}
|
||||
onClick={() => fitView({ ...FIT_VIEW, duration: 300 })}
|
||||
aria-label="Fit the flow to the screen"
|
||||
>
|
||||
@@ -174,7 +188,7 @@ export function FlowDock({
|
||||
|
||||
{issues.length > 0 ? (
|
||||
<>
|
||||
<DockSeparator />
|
||||
<DockSeparator className={SHED} />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm-touch"
|
||||
@@ -197,9 +211,13 @@ export function FlowDock({
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<DockSeparator />
|
||||
<DockSeparator className={SHED} />
|
||||
|
||||
<LogsTrigger open={logs.open} onOpenChange={logs.onOpenChange} />
|
||||
<LogsTrigger
|
||||
open={logs.open}
|
||||
onOpenChange={logs.onOpenChange}
|
||||
className={SHED_MORE}
|
||||
/>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -261,7 +279,7 @@ export function FlowDock({
|
||||
) : (
|
||||
<Play className="size-4" />
|
||||
)}
|
||||
<span className="hidden md:inline">Run</span>
|
||||
<span className={cn("hidden md:inline", SHED)}>Run</span>
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
@@ -270,7 +288,7 @@ export function FlowDock({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<DockSeparator />
|
||||
<DockSeparator className={SHED} />
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { FluksioLoader } from "@/components/ui/fluksio-loader"
|
||||
import { useSidebar } from "@/components/ui/sidebar"
|
||||
import useAuth from "@/hooks/useAuth"
|
||||
import useCustomToast from "@/hooks/useCustomToast"
|
||||
import { useIsMobile } from "@/hooks/useMobile"
|
||||
@@ -190,6 +191,13 @@ type History = {
|
||||
const HISTORY_LIMIT = 50
|
||||
const TYPING_WINDOW = 500
|
||||
|
||||
/**
|
||||
* Where the dock beside a panel is short of room even with all it sheds
|
||||
* (`FlowDock`): 285px of bar and a margin, the 16rem sidebar and the 27rem
|
||||
* panel. The sidebar folds away while a panel is open here.
|
||||
*/
|
||||
const CRAMPED = "(min-width: 48rem) and (max-width: 61.75rem)"
|
||||
|
||||
/** Nodes copied here, and the code of the ones carrying their own. */
|
||||
type NodeClipboard = {
|
||||
nodes: NodeDef_Input[]
|
||||
@@ -867,6 +875,22 @@ function FlowEditorInner({
|
||||
// the lanes the bars sit in: publishing is most wanted right after editing.
|
||||
const panelOpen = Boolean(selected) || flowPanelOpen
|
||||
|
||||
const [cramped, setCramped] = useState(
|
||||
() => window.matchMedia(CRAMPED).matches,
|
||||
)
|
||||
useEffect(() => {
|
||||
const query = window.matchMedia(CRAMPED)
|
||||
const onChange = () => setCramped(query.matches)
|
||||
query.addEventListener("change", onChange)
|
||||
return () => query.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
const { holdCollapsed } = useSidebar()
|
||||
useEffect(() => {
|
||||
if (!panelOpen || !cramped) return
|
||||
holdCollapsed(true)
|
||||
return () => holdCollapsed(false)
|
||||
}, [panelOpen, cramped, holdCollapsed])
|
||||
|
||||
// One effect owns the viewport, so nothing fights over it. Selecting a node
|
||||
// brings that node into the lane the panel leaves; every other change to the
|
||||
// graph — new wiring, a new endpoint, a panel opening — re-fits the whole flow
|
||||
@@ -879,6 +903,19 @@ function FlowEditorInner({
|
||||
// somewhere the user was already looking, so those stay animated.
|
||||
const fitted = useRef(false)
|
||||
const centred = useRef<string | null>(null)
|
||||
// The sidebar folding or unfolding resizes the canvas under the view, the
|
||||
// hold above included, so the view is aimed again once it has settled.
|
||||
const [laneMoved, setLaneMoved] = useState(0)
|
||||
useEffect(() => {
|
||||
const onEnd = (event: TransitionEvent) => {
|
||||
const slot = (event.target as HTMLElement).dataset?.slot
|
||||
if (slot !== "sidebar-gap" || event.propertyName !== "width") return
|
||||
centred.current = null
|
||||
setLaneMoved((n) => n + 1)
|
||||
}
|
||||
window.addEventListener("transitionend", onEnd)
|
||||
return () => window.removeEventListener("transitionend", onEnd)
|
||||
}, [])
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: refit when the shape or the panel changes, not on every render.
|
||||
useEffect(() => {
|
||||
// Recorded as centred only once the fit below has run: arriving from
|
||||
@@ -927,6 +964,7 @@ function FlowEditorInner({
|
||||
editorExpanded,
|
||||
isMobile,
|
||||
fitView,
|
||||
laneMoved,
|
||||
])
|
||||
|
||||
const runMutation = useMutation({
|
||||
|
||||
@@ -51,14 +51,19 @@ export type LogsFilter = {
|
||||
export function LogsTrigger({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: Pick<LogsFilter, "open" | "onOpenChange">) {
|
||||
className,
|
||||
}: Pick<LogsFilter, "open" | "onOpenChange"> & { className?: string }) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-touch"
|
||||
className={cn("text-muted-foreground", open && "text-primary")}
|
||||
className={cn(
|
||||
"text-muted-foreground",
|
||||
open && "text-primary",
|
||||
className,
|
||||
)}
|
||||
onClick={() => onOpenChange(!open)}
|
||||
aria-pressed={open}
|
||||
aria-label="Logs"
|
||||
|
||||
@@ -34,6 +34,8 @@ type SidebarContextProps = {
|
||||
state: "expanded" | "collapsed"
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
/** Folds the sidebar away while held, without touching the saved state. */
|
||||
holdCollapsed: (held: boolean) => void
|
||||
openMobile: boolean
|
||||
setOpenMobile: (open: boolean) => void
|
||||
isMobile: boolean
|
||||
@@ -82,10 +84,14 @@ function SidebarProvider({
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(getInitialOpen)
|
||||
const open = openProp ?? _open
|
||||
// A screen short of room holds the sidebar folded. Letting go brings back
|
||||
// whatever was saved, and a toggle meanwhile is the user's and wins.
|
||||
const [held, holdCollapsed] = React.useState(false)
|
||||
const open = (openProp ?? _open) && !held
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === "function" ? value(open) : value
|
||||
holdCollapsed(false)
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState)
|
||||
} else {
|
||||
@@ -128,6 +134,7 @@ function SidebarProvider({
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
holdCollapsed,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
|
||||
Reference in New Issue
Block a user