Move panel explanations into tooltips
The node, flow, widget and dashboard panels explained themselves in paragraphs under every control. They now carry a caption and an info tooltip, so a panel reads as a list of settings. InfoTip, PanelSection and FieldLabel live beside PANEL_SECTION in SidePanel.tsx, which the panels already share. InfoTip holds its own open state because a Radix tooltip ignores a touch pointer, and below md the whole panel is a full-screen sheet — hover-only help would leave a phone with the caption and nothing else. Dashboard/panels.tsx drops from 30 helper paragraphs to 5; the ones left are empty states and status, not explanation. Em-dashes are out of the user-facing strings, except where one separates the halves of a select item.
This commit is contained in:
@@ -22,7 +22,7 @@ import { cn } from "@/lib/utils"
|
||||
import { qualify } from "./deriveEdges"
|
||||
import { useLiveValue } from "./liveStore"
|
||||
import { asText, DTYPES } from "./NodePanel"
|
||||
import { PANEL_SECTION } from "./SidePanel"
|
||||
import { InfoTip, PANEL_SECTION } from "./SidePanel"
|
||||
import { ValuePreview } from "./ValuePreview"
|
||||
|
||||
/**
|
||||
@@ -274,12 +274,17 @@ export function BoundarySections({
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-3">
|
||||
<span className={PANEL_SECTION}>Mode</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className={PANEL_SECTION}>Mode</span>
|
||||
<InfoTip label="Mode">
|
||||
A live flow runs continuously, with its subscriptions, schedules and
|
||||
webhooks active. A batch flow runs only when a run asks it to, from
|
||||
its inputs to its outputs.
|
||||
</InfoTip>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{batch
|
||||
? "Runs when a run asks it to, from its inputs to its outputs. Nothing here is subscribed or scheduled."
|
||||
: "Runs continuously: subscriptions, schedules and webhooks are live."}
|
||||
{batch ? "Batch" : "Live"}
|
||||
</p>
|
||||
<Select
|
||||
value={definition.mode ?? "live"}
|
||||
@@ -301,6 +306,10 @@ export function BoundarySections({
|
||||
<div className="grid gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={PANEL_SECTION}>Inputs</span>
|
||||
<InfoTip label="Inputs">
|
||||
Messages that arrive from outside, such as a dashboard control, a
|
||||
run or the API, and the value the flow starts from.
|
||||
</InfoTip>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -315,13 +324,6 @@ export function BoundarySections({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{inputs.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Messages that arrive from outside — a dashboard control, a run, the
|
||||
API — and the value the flow starts from.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{inputs.map((declared, index) => (
|
||||
// Keyed by position: renaming an input must not remount its row.
|
||||
<InputRow
|
||||
@@ -341,12 +343,18 @@ export function BoundarySections({
|
||||
|
||||
{batch ? (
|
||||
<div className="grid gap-3">
|
||||
<span className={PANEL_SECTION}>Result</span>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{provided.length === 0
|
||||
? "Nothing is produced here yet, so a run has nothing to report."
|
||||
: "What a run reports when it finishes. Everything else it computed goes with it."}
|
||||
</p>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className={PANEL_SECTION}>Result</span>
|
||||
<InfoTip label="Result">
|
||||
What a run reports when it finishes. Everything else it computed
|
||||
goes with it.
|
||||
</InfoTip>
|
||||
</div>
|
||||
{provided.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Nothing is produced here yet, so a run has nothing to report.
|
||||
</p>
|
||||
) : null}
|
||||
{provided.map((name) => (
|
||||
<label
|
||||
key={name}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from "@/components/ui/dialog"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { BoundarySections } from "./FlowBoundary"
|
||||
import { PANEL_SECTION, PanelTitle, SidePanel } from "./SidePanel"
|
||||
import { PanelSection, PanelTitle, SidePanel } from "./SidePanel"
|
||||
|
||||
/**
|
||||
* The flow's own settings, in the same panel its nodes use — down to where the
|
||||
@@ -72,13 +72,14 @@ export function FlowPanel({
|
||||
}
|
||||
>
|
||||
<div className="grid gap-6 p-4">
|
||||
<div className="grid gap-3">
|
||||
<span className={PANEL_SECTION}>Running</span>
|
||||
<PanelSection
|
||||
title="Running"
|
||||
className="gap-3"
|
||||
help="While this is on, the engine keeps the flow's subscriptions, schedules and webhooks live. Off, none of it is subscribed, scheduled or reachable."
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{enabled
|
||||
? "The engine runs this flow: subscriptions, schedules and webhooks are live."
|
||||
: "Stopped. Nothing of this flow is subscribed, scheduled or reachable."}
|
||||
{enabled ? "Live" : "Stopped"}
|
||||
</p>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
@@ -88,7 +89,7 @@ export function FlowPanel({
|
||||
data-testid="flow-enabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
||||
<BoundarySections
|
||||
flow={definition.name}
|
||||
@@ -97,25 +98,34 @@ export function FlowPanel({
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<span className={PANEL_SECTION}>Contents</span>
|
||||
<PanelSection
|
||||
title="Contents"
|
||||
className="gap-3"
|
||||
help={
|
||||
<>
|
||||
Every message in this flow is named under{" "}
|
||||
<span className="font-mono">{definition.name}</span>, so two
|
||||
flows can use the same short name without colliding.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<span className="font-mono">{definition.name}</span> namespaces
|
||||
every message in here.{" "}
|
||||
{nodeCount === 0
|
||||
? "No nodes yet."
|
||||
: `${nodeCount} node${nodeCount === 1 ? "" : "s"}.`}
|
||||
: `${nodeCount} node${nodeCount === 1 ? "" : "s"}`}
|
||||
</p>
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
||||
{hasDraft ? (
|
||||
<div className="grid gap-3">
|
||||
<span className={PANEL_SECTION}>Unpublished changes</span>
|
||||
<PanelSection
|
||||
title="Unpublished changes"
|
||||
className="gap-3"
|
||||
help="Edits are kept as a draft until you publish them. The bar below publishes this one, or discards it."
|
||||
>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
The engine is still running the last published version of this
|
||||
flow. The bar below publishes it, or throws the edit away.
|
||||
The engine is still running the last published version.
|
||||
</p>
|
||||
</div>
|
||||
</PanelSection>
|
||||
) : null}
|
||||
</div>
|
||||
</SidePanel>
|
||||
|
||||
@@ -49,7 +49,7 @@ import {
|
||||
secretsQueryOptions,
|
||||
useParamSuggestions,
|
||||
} from "./queries"
|
||||
import { PANEL_SECTION, PanelTitle, SidePanel } from "./SidePanel"
|
||||
import { InfoTip, PANEL_SECTION, PanelTitle, SidePanel } from "./SidePanel"
|
||||
|
||||
const NodeEditor = lazy(() => import("./NodeEditor"))
|
||||
|
||||
@@ -368,8 +368,8 @@ function PortList({
|
||||
aria-label="Streaming output"
|
||||
aria-pressed={spec.stream ?? false}
|
||||
title={
|
||||
"Published repeatedly while the node runs — a curve rather " +
|
||||
"than a result. A run keeps every value it takes."
|
||||
"Published repeatedly while the node runs: a curve rather than " +
|
||||
"a result. A run keeps every value it takes."
|
||||
}
|
||||
className={cn(
|
||||
"shrink-0",
|
||||
@@ -613,15 +613,15 @@ function CronHelp({
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Five fields —{" "}
|
||||
<InfoTip label="Cron">
|
||||
Five fields:{" "}
|
||||
<span className="font-mono">
|
||||
minute hour day-of-month month weekday
|
||||
</span>
|
||||
. <span className="font-mono">*</span> is every,{" "}
|
||||
<span className="font-mono">*/5</span> every fifth,{" "}
|
||||
<span className="font-mono">1-5</span> a range.
|
||||
</p>
|
||||
</InfoTip>
|
||||
{derivable && derived && params.cron !== derived ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -886,9 +886,17 @@ function ResourcesSection({
|
||||
|
||||
return (
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="node-resources" className={SECTION}>
|
||||
Resources
|
||||
</Label>
|
||||
<div className="flex items-center gap-1">
|
||||
<Label htmlFor="node-resources" className={SECTION}>
|
||||
Resources
|
||||
</Label>
|
||||
<InfoTip label="Resources">
|
||||
What is held for the whole execution, and what the node's own
|
||||
libraries are told they may use. The duration is a planning figure,
|
||||
not a limit. Held nothing, the node shares the pool with every other
|
||||
node that asks for nothing.
|
||||
</InfoTip>
|
||||
</div>
|
||||
<Select value={choice} onValueChange={pick}>
|
||||
<SelectTrigger id="node-resources" className="h-8 text-sm">
|
||||
<SelectValue />
|
||||
@@ -946,7 +954,7 @@ function ResourcesSection({
|
||||
{resources ? (
|
||||
<Input
|
||||
className="h-8 text-sm"
|
||||
placeholder="expected duration — 30m, 2h"
|
||||
placeholder="expected duration, e.g. 30m or 2h"
|
||||
defaultValue={formatDuration(resources.duration_s)}
|
||||
key={`${choice}-${resources.duration_s ?? ""}`}
|
||||
onBlur={(event) => {
|
||||
@@ -962,12 +970,6 @@ function ResourcesSection({
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{resources
|
||||
? "Held for the whole execution, and what the node's own libraries are told they may use. The duration is a planning fact, not a limit."
|
||||
: "Nothing held: the node shares the pool with every other node that says nothing."}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1047,10 +1049,13 @@ function SharingSection({
|
||||
|
||||
return (
|
||||
<div className="grid gap-3">
|
||||
<span className={SECTION}>Reuse</span>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Move this node's code to the library so other flows can run it too.
|
||||
</p>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className={SECTION}>Reuse</span>
|
||||
<InfoTip label="Reuse">
|
||||
Moves this node's code to the library so other flows can run it too.
|
||||
Editing a shared node changes it everywhere at once.
|
||||
</InfoTip>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
value={name}
|
||||
@@ -1325,9 +1330,16 @@ function PanelBody({
|
||||
) : null}
|
||||
{hasSource ? (
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="node-timeout" className={SECTION}>
|
||||
Timeout
|
||||
</Label>
|
||||
<div className="flex items-center gap-1">
|
||||
<Label htmlFor="node-timeout" className={SECTION}>
|
||||
Timeout
|
||||
</Label>
|
||||
<InfoTip label="Timeout">
|
||||
Seconds this code may stay silent before it is stopped. A yield
|
||||
or an emit resets the clock. 0 disables the limit; empty uses
|
||||
the engine default.
|
||||
</InfoTip>
|
||||
</div>
|
||||
<Input
|
||||
id="node-timeout"
|
||||
type="number"
|
||||
@@ -1346,11 +1358,6 @@ function PanelBody({
|
||||
})
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Seconds this code may be silent before it is stopped — a yield or
|
||||
an emit resets the clock. 0 disables the limit; empty uses the
|
||||
engine default.
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
{hasSource ? (
|
||||
@@ -1394,8 +1401,8 @@ function PanelBody({
|
||||
data-testid="node-source-generated"
|
||||
>
|
||||
Generated by <span className="font-mono">fluksio sync</span> from{" "}
|
||||
<span className="font-mono">{origin.repo || "a repository"}</span>{" "}
|
||||
— edit it there and sync again.
|
||||
<span className="font-mono">{origin.repo || "a repository"}</span>
|
||||
. Edit it there and sync again.
|
||||
</p>
|
||||
) : null}
|
||||
<div className="min-h-0 flex-1 overflow-hidden rounded-md border border-border">
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { X } from "lucide-react"
|
||||
import { Info, X } from "lucide-react"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
import type { ReactNode } from "react"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { useIsMobile } from "@/hooks/useMobile"
|
||||
import { duration, easeEmphasized, easeStandard } from "@/lib/motion"
|
||||
import { cn } from "@/lib/utils"
|
||||
@@ -28,6 +34,94 @@ const panelSlide = {
|
||||
export const PANEL_SECTION =
|
||||
"text-xs font-medium uppercase tracking-[0.5px] text-muted-foreground"
|
||||
|
||||
/**
|
||||
* The explanation behind a caption, one tap or one hover away.
|
||||
*
|
||||
* Panels label their controls and put the rest here, so a panel reads as a
|
||||
* list of settings rather than as a page of prose. Keep it to a sentence or
|
||||
* two: a tooltip nobody can scroll is the wrong home for more.
|
||||
*
|
||||
* `open` is held here because a Radix tooltip never opens on touch — it
|
||||
* ignores a touch pointer entirely — and below `md` the whole panel is a
|
||||
* full-screen sheet, so a phone would get the caption and nothing else.
|
||||
* Clicking toggles it; hover still drives it on a pointer device.
|
||||
*/
|
||||
export function InfoTip({
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
label: string
|
||||
children: ReactNode
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<Tooltip open={open} onOpenChange={setOpen}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`About ${label}`}
|
||||
className="inline-flex size-11 shrink-0 items-center justify-center text-muted-foreground md:size-5"
|
||||
onClick={() => setOpen((was) => !was)}
|
||||
>
|
||||
<Info className="size-3.5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs text-balance">
|
||||
{children}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A field's label, with its explanation behind an `InfoTip` rather than in a
|
||||
* paragraph under the control. Sits inside the usual `grid gap-1.5` block.
|
||||
*/
|
||||
export function FieldLabel({
|
||||
children,
|
||||
help,
|
||||
htmlFor,
|
||||
}: {
|
||||
children: string
|
||||
help?: ReactNode
|
||||
htmlFor?: string
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Label htmlFor={htmlFor} className="text-sm font-normal">
|
||||
{children}
|
||||
</Label>
|
||||
{help ? <InfoTip label={children}>{help}</InfoTip> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A captioned block in a panel: the caption, its explanation behind an
|
||||
* `InfoTip`, and the controls under both.
|
||||
*/
|
||||
export function PanelSection({
|
||||
title,
|
||||
help,
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
title: string
|
||||
help?: ReactNode
|
||||
className?: string
|
||||
children: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("grid gap-2", className)}>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className={PANEL_SECTION}>{title}</span>
|
||||
{help ? <InfoTip label={title}>{help}</InfoTip> : null}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* What the thing in this panel is called, and the one place to rename it.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user