import { useCallback, useState } from 'react' import { AnimatePresence, LayoutGroup, motion } from 'motion/react' import { IconPlus } from '@tabler/icons-react' import { useAppletThumbnails } from '@/client/features/applets/applet-thumbnail' import { useWorkspaceLayoutCtx } from '@/client/features/workspace/WorkspaceLayoutContext' import { findFreePosition } from '@/client/features/widgets/grid' import type { GridItem } from '@/client/features/widgets/grid' import { WidgetShell } from '@/client/features/applets/WidgetShell' import { Button } from '@/client/components/ui/button' import { Skeleton } from '@/client/components/ui/skeleton' import { cn } from '@/client/lib/cn' import type { WidgetInfo } from '@/lib/types' import { HiddenPanel } from './HiddenPanel' import { WidgetGrid, WidgetGridLayout } from './WidgetGrid' function renderItem(id: string) { return } const EMPTY_WIDGET_ITEMS: GridItem[] = Array.from({ length: 10 }, (_, index) => ({ id: `empty-widget-${index}`, w: 2, h: 1 })) function renderEmptyWidget() { return ( ) } type NoWidgetsCreatedProps = { onCreateWidget: () => void } function NoWidgetsCreated({ onCreateWidget }: NoWidgetsCreatedProps) { return (

A little empty here

Widgets are small apps that can read data, perform tasks, and surface important information

) } type WidgetActionsProps = { editing: boolean onCreateWidget: () => void onEditingChange: (editing: boolean) => void } function WidgetActions({ editing, onCreateWidget, onEditingChange }: WidgetActionsProps) { return (
{editing && ( )}
) } function NoWidgetsAdded() { return (

No widgets added yet

) } // Tracks the rendered (border-box) height of whichever bottom panel is open, so // the grid can reserve matching space below it and every card stays reachable. function usePanelHeight() { const [height, setHeight] = useState(0) const ref = useCallback((node: HTMLDivElement | null) => { if (!node) return const ro = new ResizeObserver(([entry]) => { const box = entry.borderBoxSize?.[0] setHeight(box ? box.blockSize : entry.contentRect.height) }) ro.observe(node) return () => ro.disconnect() }, []) return [ref, height] as const } type WidgetsProps = { onCreateWidget: () => void editing: boolean onEditingChange: (editing: boolean) => void // Authoritative widget set from the widgets query; positions come from layout. widgets: WidgetInfo[] } export function Widgets({ onCreateWidget, editing, onEditingChange, widgets }: WidgetsProps) { const { layout, setLayout } = useWorkspaceLayoutCtx() const hasWidgets = widgets.length > 0 const gridIds = new Set(layout.widgetGrid.map(g => g.i)) const visibleItems: GridItem[] = layout.widgetGrid .filter(g => widgets.some(w => w.id === g.i)) .map(g => { const widget = widgets.find(w => w.id === g.i)! return { id: g.i, w: widget.config.colSpan, h: widget.config.rowSpan, x: g.x, y: g.y } }) const hiddenItems: GridItem[] = widgets .filter(w => !gridIds.has(w.id)) .map(w => ({ id: w.id, w: w.config.colSpan, h: w.config.rowSpan })) const [panelRef, panelHeight] = usePanelHeight() const panelOpen = editing && hiddenItems.length > 0 useAppletThumbnails({ kind: 'widget', enabled: !editing, targets: visibleItems.map(item => ({ id: item.id, revision: widgets.find(widget => widget.id === item.id)?.revision })) }) function hide(id: string) { setLayout({ widgetGrid: layout.widgetGrid.filter(g => g.i !== id) }) } function restore(id: string) { const widget = widgets.find(w => w.id === id) if (!widget) return const gridWithSizes = layout.widgetGrid.map(g => { const w = widgets.find(w => w.id === g.i) return { ...g, w: w?.config.colSpan ?? 1, h: w?.config.rowSpan ?? 1 } }) const pos = findFreePosition(gridWithSizes, widget.config.colSpan, widget.config.rowSpan, 4) setLayout({ widgetGrid: [...layout.widgetGrid, { i: id, x: pos.x, y: pos.y }] }) } return ( // Shared working area below the header and positioning context for the // bottom panel. Created-widget states scroll; the initial empty state clips // its fixed-height skeleton grid.
{hasWidgets ? ( <> {visibleItems.length === 0 ? ( ) : ( // The open panel's height is reserved below the grid so every card // can scroll clear of the panel. setLayout({ widgetGrid: items.map(i => ({ i: i.id, x: i.x ?? 0, y: i.y ?? 0 })) }) } /> )} ) : ( )}
{hasWidgets && editing && hiddenItems.length > 0 && ( onEditingChange(false)} onRestore={restore} /> )}
) }