WidgetFrame.tsx3.1 KBView on GitHub 'use client';
import type { ReactNode } from 'react';
import { Pencil } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
/**
* The chrome every rail widget wears: a titled card with an icon and an optional header action.
*
* One frame for every tile rather than each widget drawing its own card, because the rail's
* whole job is to read as a single column of comparable things. A widget that brings its own
* border, padding and title weight makes the rail look like a pile of unrelated panels — which
* is exactly what the home screen looked like before it had a rail.
*
* NO PER-TILE REMOVE CONTROL. Removing is editing, and editing happens in one place: the "Edit
* widgets" button at the foot of the rail. A hover-revealed ✕ on every card put a destructive
* action under the cursor on the way to every other one, and split "what is on my rail" across
* six places instead of one.
*/
export function WidgetFrame({
title,
icon: Icon,
action,
onEdit,
editLabel,
children,
className,
bodyClassName,
}: {
title: string;
icon: LucideIcon;
/** A control shown at the right of the header (e.g. the day arrows, "Add agent"). */
action?: ReactNode;
/**
* Opens this widget's own settings. When given, a pencil appears in the header ON HOVER —
* so a per-widget control costs no header space until you go looking for it, instead of
* sitting there stating its own configuration at you all day.
*/
onEdit?: () => void;
/** Accessible name for the pencil, e.g. "Pipeline filters". */
editLabel?: string;
children: ReactNode;
className?: string;
bodyClassName?: string;
}) {
return (
<section
data-testid="widget-frame"
data-widget-title={title}
className={cn(
'group/widget flex flex-col overflow-hidden rounded-xl border border-border bg-surface/75 shadow-sm',
className,
)}
>
<header className="flex shrink-0 items-center gap-2 px-3 pb-1.5 pt-2.5">
<Icon aria-hidden className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<h3 className="truncate text-xs font-medium uppercase tracking-wide text-muted-foreground">
{title}
</h3>
<div className="ml-auto flex shrink-0 items-center gap-1">
{action}
{onEdit && (
<button
type="button"
onClick={onEdit}
aria-label={editLabel ?? `Edit ${title}`}
title={editLabel ?? `Edit ${title}`}
// Revealed on hover of the WIDGET, not of the header row: reaching for it moves
// the pointer up out of the body, and a control that vanishes on the way there
// cannot be clicked.
className="cursor-pointer rounded-md p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-sunken hover:text-foreground focus-visible:opacity-100 group-hover/widget:opacity-100"
>
<Pencil aria-hidden className="h-3.5 w-3.5" />
</button>
)}
</div>
</header>
<div className={cn('min-h-0 px-3 pb-3', bodyClassName)}>{children}</div>
</section>
);
}