/** * WidgetPicker — Dialog to browse available widgets and add them to the dashboard. */ import { useState, useCallback } from "react"; import type { ReactElement } from "react"; import type { WidgetPlacement } from "@mosaic/shared"; import { getAllWidgets, type WidgetDefinition } from "./WidgetRegistry"; export interface WidgetPickerProps { open: boolean; onClose: () => void; onAddWidget: (placement: WidgetPlacement) => void; currentLayout: WidgetPlacement[]; } /** Generate a unique widget ID: "WidgetType-" */ function generateWidgetId(widgetName: string): string { const suffix = Math.random().toString(36).slice(2, 8); return `${widgetName}-${suffix}`; } /** Find the first open Y position at x=0 that doesn't overlap */ function findNextY(layout: WidgetPlacement[]): number { if (layout.length === 0) return 0; let maxBottom = 0; for (const item of layout) { const bottom = item.y + item.h; if (bottom > maxBottom) maxBottom = bottom; } return maxBottom; } function WidgetPickerItem({ widget, onAdd, }: { widget: WidgetDefinition; onAdd: () => void; }): ReactElement { const [hovered, setHovered] = useState(false); return (
{ setHovered(true); }} onMouseLeave={(): void => { setHovered(false); }} style={{ display: "flex", alignItems: "center", gap: 12, padding: "12px 16px", borderRadius: "var(--r)", background: hovered ? "var(--surface-2)" : "transparent", transition: "background 0.12s ease", }} >
{widget.displayName}
{widget.description}
Default size: {widget.defaultWidth}×{widget.defaultHeight}
); } export function WidgetPicker({ open, onClose, onAddWidget, currentLayout, }: WidgetPickerProps): ReactElement | null { const allWidgets = getAllWidgets(); const [search, setSearch] = useState(""); const filtered = search ? allWidgets.filter( (w) => w.displayName.toLowerCase().includes(search.toLowerCase()) || w.description.toLowerCase().includes(search.toLowerCase()) ) : allWidgets; const handleAdd = useCallback( (widget: WidgetDefinition) => { const placement: WidgetPlacement = { i: generateWidgetId(widget.name), x: 0, y: findNextY(currentLayout), w: widget.defaultWidth, h: widget.defaultHeight, minW: widget.minWidth, minH: widget.minHeight, }; if (widget.maxWidth !== undefined) placement.maxW = widget.maxWidth; if (widget.maxHeight !== undefined) placement.maxH = widget.maxHeight; onAddWidget(placement); }, [currentLayout, onAddWidget] ); if (!open) return null; return ( <> {/* Backdrop */}