fix(fleet): equalize action card heights, swap order, replace native autocomplete (#1138)

Three visual adjustments to the Fleet Actions tab:

1. Drop the xl:grid-cols-3 breakpoint. The cards carry live previews and
   per-node estimate wells that compress badly at 3-up on a 1440 monitor.
   2-up is the new max. Pair with auto-rows-fr so every row equalizes to
   the tallest content height, and let <FleetActionCard> stretch into the
   cell via flex flex-col h-full plus a flex-1 body. Footer pins to the
   bottom; the shorter card's body grows whitespace below its content.

2. Reorder the JSX to Prune, Bulk, Stop. Prune sits top-left with its
   live "~ N GB reclaimable" estimate populated on first render. Stop,
   which reads "awaiting target" until the operator types, drops to
   row 2 below the fold.

3. Replace the native <datalist> on the Stop card with a custom
   LabelAutocomplete: free-form text input + a Sencho-styled popover
   (border-glass-border, bg-popover, backdrop-blur) that filters the
   known-label set as the operator types. Click a suggestion via
   mousedown + preventDefault so the input stays focused and the
   selection registers before any blur-driven close. Outside-click and
   Escape close the popover. Operator can still run against a label
   that was not in the autocomplete set; the server-side match-preview
   resolves it.

Also: whitespace-nowrap on the primary button so "Stop fleet" /
"Prune fleet" do not wrap in tight viewports.
This commit is contained in:
Anso
2026-05-21 12:51:54 -04:00
committed by GitHub
parent 0a3e76c0bb
commit 2563e30424
3 changed files with 111 additions and 17 deletions
@@ -12,6 +12,14 @@ export type FleetActionClass = 'destructive' | 'transformative' | 'maintenance';
export type BlastRadiusTone = 'warning' | 'success' | 'muted';
export type PrimaryActionVariant = 'primary' | 'destructive';
/**
* System-Sheet recipe card for the Fleet Actions tab (audit §18, DESIGN §10).
*
* Layout note: the outer <Card> is `flex flex-col h-full` and the body slot
* is `flex-1`, so when a parent gives the card explicit height (e.g. the
* equalized-row wrapper in FleetActionsTab.tsx), the body grows and the
* footer pins to the bottom. In a content-sized parent this is a no-op.
*/
export interface FleetActionCardProps {
/** Crumb segments. Last segment renders in --stat-title; the rest in --stat-subtitle with separators. */
crumb: string[];
@@ -107,7 +115,7 @@ export function FleetActionCard(props: FleetActionCardProps) {
const headCrumbs = crumb.slice(0, -1);
return (
<Card className="relative overflow-hidden p-0">
<Card className="relative overflow-hidden p-0 flex flex-col h-full">
<span
aria-hidden="true"
className="absolute inset-y-0 left-0 w-[3px] bg-brand"
@@ -166,7 +174,7 @@ export function FleetActionCard(props: FleetActionCardProps) {
/>
</div>
<div className="px-6">
<div className="px-6 flex-1">
{children}
</div>