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 (
)
}
// 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}
/>
)}
)
}