Long names stay in their box: overview cards and Home's dashboard tiles truncate with the full name on hover, the node panel's flow path and a device id leave half the row to the name field, dialog titles and select items wrap an unbroken name, and a select menu is capped at the room it has; the flow canvas shows a loader until the layout engine has placed the nodes, so a slow link no longer looks like an empty flow

This commit is contained in:
2026-11-10 19:01:23 +01:00
parent fba4bd811a
commit 33ed6f669f
7 changed files with 38 additions and 15 deletions
@@ -165,8 +165,11 @@ function Tile({
)}
>
<Footprint dashboard={dashboard} />
<span className="flex items-center gap-2">
<span className="truncate text-sm font-medium">
<span className="flex min-w-0 items-center gap-2">
<span
className="truncate text-sm font-medium"
title={dashboard.title || dashboard.name}
>
{dashboard.title || dashboard.name}
</span>
{dashboard.has_draft ? (
@@ -68,7 +68,7 @@ export function OverviewCard({
}: {
link: LinkProps
icon: LucideIcon
title: ReactNode
title: string
detail: ReactNode
/** Has unpublished changes. */
draft?: boolean
@@ -132,9 +132,11 @@ export function OverviewCard({
if (held.current) event.preventDefault()
}}
>
<span className="flex items-center gap-2">
<span className="flex min-w-0 items-center gap-2">
<Icon className="size-4 shrink-0 text-muted-foreground" />
<span className="truncate">{title}</span>
<span className="truncate" title={title}>
{title}
</span>
{draft ? (
<span className="size-1.5 shrink-0 rounded-full bg-primary">
<span className="sr-only">Unpublished changes</span>
@@ -175,7 +175,7 @@ function Basics({
}) {
return (
<PanelSection title="Device">
<div className="flex items-center gap-2">
<div className="flex min-w-0 items-center gap-2">
<Input
// Saved on blur or Enter, not per keystroke. Remounted with whatever
// was saved, so a refused title snaps back.
@@ -194,7 +194,10 @@ function Basics({
if (title !== device.title) onChange({ title })
}}
/>
<span className="shrink-0 font-mono text-sm text-muted-foreground">
<span
className="max-w-1/2 shrink-0 truncate font-mono text-sm text-muted-foreground"
title={device.id}
>
{device.id}
</span>
</div>
+16 -4
View File
@@ -50,6 +50,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { FluksioLoader } from "@/components/ui/fluksio-loader"
import useAuth from "@/hooks/useAuth"
import useCustomToast from "@/hooks/useCustomToast"
import { useIsMobile } from "@/hooks/useMobile"
@@ -110,10 +111,10 @@ const nodeTypes = { flow: FlowNode, [ENDPOINT_TYPE]: EndpointNode }
* Where every node sits and how every edge runs, once the engine has said so.
*
* The layout is a chunk of its own and answers asynchronously, so a canvas has
* a moment before it knows where anything goes. It draws nothing at all until
* then — a route already shows a skeleton for that — and an *edit* keeps the
* arrangement it had, so only the node that changed lands a frame late rather
* than the whole graph flickering through the corner.
* a moment before it knows where anything goes. It draws no node until then,
* only a loader, and an *edit* keeps the arrangement it had, so only the node
* that changed lands a frame late rather than the whole graph flickering
* through the corner.
*/
function useLayout(
ids: string[],
@@ -1565,6 +1566,17 @@ function FlowEditorInner({
/>
</div>
{/* On a slow link the layout engine arrives well after the document,
and a bare canvas until then reads as a flow with no nodes. */}
{definitions.length > 0 && !positions ? (
<div
className="pointer-events-none absolute inset-0 flex items-center justify-center"
data-testid="flow-loading"
>
<FluksioLoader className="size-6 text-muted-foreground" />
</div>
) : null}
{definitions.length === 0 ? (
<div
className="pointer-events-none absolute inset-0 flex items-center justify-center"
+4 -1
View File
@@ -2520,7 +2520,10 @@ export function NodePanel({
<div className="flex min-w-0 flex-1 items-center gap-1.5">
{/* Where the node lives: the flow is the namespace of its
messages, so it reads like the folder holding them. */}
<span className="min-w-0 truncate font-mono text-sm text-muted-foreground">
<span
className="min-w-0 max-w-1/2 truncate font-mono text-sm text-muted-foreground"
title={`/${flow}/`}
>
/{flow}/
</span>
<PanelTitle
+1 -1
View File
@@ -110,7 +110,7 @@ function DialogTitle({
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
className={cn("text-lg leading-none font-semibold wrap-anywhere", className)}
{...props}
/>
)
+2 -2
View File
@@ -60,7 +60,7 @@ function SelectContent({
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-e2",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) max-w-(--radix-select-content-available-width) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-e2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
@@ -107,7 +107,7 @@ function SelectItem({
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm wrap-anywhere outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}