diff --git a/frontend/src/components/fleet/FleetActions/FleetActionsTab.tsx b/frontend/src/components/fleet/FleetActions/FleetActionsTab.tsx index debaa1d4..1cc16d34 100644 --- a/frontend/src/components/fleet/FleetActions/FleetActionsTab.tsx +++ b/frontend/src/components/fleet/FleetActions/FleetActionsTab.tsx @@ -22,14 +22,17 @@ export function FleetActionsTab({ nodes }: Props) { return ; } - // Grid breakpoints per audit §18.7: 1 / 2 / 3 columns at 760 / 1280. The - // 4-column breakpoint is reserved for when a 4th card lands; v1 has three - // so it is intentionally omitted to avoid a hanging gap on wide displays. + // Grid: 1 col under lg, 2 cols at lg and above. auto-rows-fr distributes + // available height evenly across the auto-created rows, and each card uses + // `flex flex-col h-full` (in ) with a `flex-1` body so it + // fills its grid cell. Result: all three cards render at the same height, + // with shorter cards growing their body whitespace before the footer pins. + // Order: Prune top-left, Bulk top-right, Stop on row 2. return ( -
- - +
+ +
); } diff --git a/frontend/src/components/fleet/FleetActions/cards/LabelFleetStopCard.tsx b/frontend/src/components/fleet/FleetActions/cards/LabelFleetStopCard.tsx index 36a04327..13847170 100644 --- a/frontend/src/components/fleet/FleetActions/cards/LabelFleetStopCard.tsx +++ b/frontend/src/components/fleet/FleetActions/cards/LabelFleetStopCard.tsx @@ -201,18 +201,13 @@ export function LabelFleetStopCard({ nodes }: Props) { title="Label · target" meta={`auto-suggested · ${knownLabelNames.length} known`} > - setLabelName(e.target.value)} - placeholder="e.g. production" - className="h-9 text-sm" + onChange={setLabelName} + suggestions={knownLabelNames} disabled={running} + placeholder="e.g. production" /> - - {knownLabelNames.map(n => {previewSection} @@ -308,3 +303,91 @@ function PreviewWell({ perNode }: PreviewWellProps) {
); } + +interface LabelAutocompleteProps { + value: string; + onChange: (next: string) => void; + suggestions: string[]; + disabled?: boolean; + placeholder?: string; +} + +// Free-form text input with a Sencho-styled suggestion popover. Replaces the +// browser-native so the dropdown matches the rest of the kit (same +// surface tokens as ). The operator can still type a label name +// that was not in the suggestions list; the server-side match-preview will +// resolve it or report 0 nodes match. +function LabelAutocomplete({ value, onChange, suggestions, disabled, placeholder }: LabelAutocompleteProps) { + const [open, setOpen] = useState(false); + const wrapperRef = useRef(null); + + const filtered = useMemo(() => { + const q = value.trim().toLowerCase(); + if (q.length === 0) return suggestions; + return suggestions.filter(s => s.toLowerCase().includes(q)); + }, [value, suggestions]); + + useEffect(() => { + if (!open) return; + const onClick = (e: MouseEvent) => { + if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) { + setOpen(false); + } + }; + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.stopPropagation(); + setOpen(false); + } + }; + document.addEventListener('mousedown', onClick); + document.addEventListener('keydown', onKey, true); + return () => { + document.removeEventListener('mousedown', onClick); + document.removeEventListener('keydown', onKey, true); + }; + }, [open]); + + const handleSelect = (next: string) => { + onChange(next); + setOpen(false); + }; + + return ( +
+ { + onChange(e.target.value); + if (!open) setOpen(true); + }} + onFocus={() => { if (!disabled) setOpen(true); }} + placeholder={placeholder} + className="h-9 text-sm" + disabled={disabled} + autoComplete="off" + spellCheck={false} + /> + {open && filtered.length > 0 && ( +
+
    + {filtered.map((s) => ( +
  • + +
  • + ))} +
+
+ )} +
+ ); +} diff --git a/frontend/src/components/ui/fleet-action-card.tsx b/frontend/src/components/ui/fleet-action-card.tsx index c6ca5133..9c1efa1d 100644 --- a/frontend/src/components/ui/fleet-action-card.tsx +++ b/frontend/src/components/ui/fleet-action-card.tsx @@ -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 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 ( - +