@vertly/dashboard-grid 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/dashboard-grid.tsx","../src/layout-engine.ts","../src/cn.ts"],"sourcesContent":["export { DashboardGrid } from './dashboard-grid';\nexport type { DashboardGridWidgetData, DashboardGridProps } from './dashboard-grid';\nexport {\n COLS,\n ZONE_WIDTH_PX,\n MIN_COL_SPAN,\n MAX_ITEMS_PER_ROW,\n DEFAULT_ROW_HEIGHT,\n MIN_ROW_HEIGHT,\n MAX_ROW_HEIGHT,\n rebalanceRow,\n canPlaceInRow,\n placeItemAt,\n placeItemInNewRow,\n removeItemFromRow,\n resizeItemPair,\n resizeRow,\n moveRow,\n} from './layout-engine';\nexport type { RowLike, SizedItem } from './layout-engine';\n","'use client';\n\nimport { Fragment, useCallback, useRef, useState, type ReactNode } from 'react';\nimport { DndProvider, useDrag, useDragLayer, useDrop } from 'react-dnd';\nimport { HTML5Backend } from 'react-dnd-html5-backend';\nimport { GripVerticalIcon, PlusIcon } from 'lucide-react';\n\nimport {\n COLS,\n MAX_ITEMS_PER_ROW,\n MAX_ROW_HEIGHT,\n MIN_ROW_HEIGHT,\n ZONE_WIDTH_PX,\n moveRow,\n placeItemAt,\n placeItemInNewRow,\n removeItemFromRow,\n resizeItemPair,\n resizeRow,\n type RowLike,\n} from './layout-engine';\nimport { cn } from './cn';\n\nconst DEFAULT_SAVE_DEBOUNCE_MS = 600;\n\n// Local, layout-only shadow of a widget — colSpan is optimistically mutated\n// client-side by drag/resize; everything else a widget needs to render is\n// read straight from the `widgets` prop each render (caller-fresh), not\n// duplicated into this state, so a content-only change in the caller's data\n// is never shadowed by stale local state.\ninterface WidgetItem {\n id: string;\n colSpan: number;\n}\n\nexport interface DashboardGridWidgetData {\n id: string;\n colSpan: number;\n // Rendered by the caller, not this component — keeps this package free of\n // any opinion on what a widget's content looks like or where its data\n // comes from.\n content: ReactNode;\n}\n\nconst ItemTypes = { WIDGET: 'widget', ROW: 'row' } as const;\n\ninterface WidgetDragItem {\n widgetId: string;\n fromRowId: string;\n}\ninterface RowDragItem {\n rowId: string;\n index: number;\n}\n\nexport interface DashboardGridProps<TWidget extends DashboardGridWidgetData = DashboardGridWidgetData> {\n rows: RowLike[];\n widgets: Record<string, TWidget>;\n readOnly?: boolean;\n /** Debounce (ms) before onUpdateLayout fires after a drag/resize. Default 600. */\n saveDebounceMs?: number;\n onUpdateLayout?: (rows: RowLike[], widgetColSpans: { id: string; colSpan: number }[]) => void;\n onDeleteWidget?: (widgetId: string) => void;\n /** Renders the \"add widget\" affordance at the start/end of a row. Omit to disable. */\n renderRowEdgeAdd?: (opts: { rowId: string; edge: 'start' | 'end' }) => ReactNode;\n /** Renders a widget's action menu (e.g. configure/delete). Omit to disable. */\n renderWidgetMenu?: (\n widget: TWidget,\n actions: { onConfigure: () => void; onDelete: () => void },\n ) => ReactNode;\n /** Renders a widget's configuration dialog, controlled via `open`/`onOpenChange`. */\n renderWidgetConfigDialog?: (opts: {\n widget: TWidget;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n }) => ReactNode;\n}\n\nexport function DashboardGrid<TWidget extends DashboardGridWidgetData = DashboardGridWidgetData>({\n rows: initialRows,\n widgets,\n readOnly = false,\n saveDebounceMs = DEFAULT_SAVE_DEBOUNCE_MS,\n onUpdateLayout,\n onDeleteWidget,\n renderRowEdgeAdd,\n renderWidgetMenu,\n renderWidgetConfigDialog,\n}: DashboardGridProps<TWidget>) {\n const [{ rows, items }, setState] = useState<{\n rows: RowLike[];\n items: Record<string, WidgetItem>;\n }>(() => ({\n rows: initialRows.map((row) => ({\n id: row.id,\n heightPx: row.heightPx,\n widgetIds: row.widgetIds,\n })),\n items: Object.fromEntries(\n Object.values(widgets).map((w) => [w.id, { id: w.id, colSpan: w.colSpan }]),\n ),\n }));\n\n const saveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const persist = useCallback(\n (nextRows: RowLike[], nextItems: Record<string, WidgetItem>) => {\n if (readOnly) return;\n if (saveTimer.current) clearTimeout(saveTimer.current);\n saveTimer.current = setTimeout(() => {\n onUpdateLayout?.(\n nextRows.map((row) => ({ id: row.id, heightPx: row.heightPx, widgetIds: row.widgetIds })),\n Object.values(nextItems).map((item) => ({ id: item.id, colSpan: item.colSpan })),\n );\n }, saveDebounceMs);\n },\n [readOnly, saveDebounceMs, onUpdateLayout],\n );\n\n const placeWidgetAt = useCallback(\n (opts: { widgetId: string; fromRowId: string }, targetRowId: string, index: number) => {\n setState((prev) => {\n const next = placeItemAt(\n prev.rows,\n prev.items,\n opts.widgetId,\n opts.fromRowId,\n targetRowId,\n index,\n );\n persist(next.rows, next.items);\n return next;\n });\n },\n [persist],\n );\n\n const createRowWith = useCallback(\n (opts: { widgetId: string; fromRowId: string }) => {\n setState((prev) => {\n const next = placeItemInNewRow(\n prev.rows,\n prev.items,\n opts.widgetId,\n opts.fromRowId,\n `row-${crypto.randomUUID()}`,\n );\n persist(next.rows, next.items);\n return next;\n });\n },\n [persist],\n );\n\n const removeWidget = useCallback(\n (widgetId: string, rowId: string) => {\n setState((prev) => {\n const next = removeItemFromRow(prev.rows, prev.items, widgetId, rowId);\n persist(next.rows, next.items);\n return next;\n });\n onDeleteWidget?.(widgetId);\n },\n [persist, onDeleteWidget],\n );\n\n const resizeWidgetPair = useCallback(\n (leftId: string, rightId: string, nextLeftColSpan: number) => {\n setState((prev) => {\n const nextItems = resizeItemPair(prev.items, leftId, rightId, nextLeftColSpan);\n persist(prev.rows, nextItems);\n return { rows: prev.rows, items: nextItems };\n });\n },\n [persist],\n );\n\n const resizeRowHeight = useCallback(\n (rowId: string, heightPx: number) => {\n setState((prev) => {\n const nextRows = resizeRow(prev.rows, rowId, heightPx);\n persist(nextRows, prev.items);\n return { rows: nextRows, items: prev.items };\n });\n },\n [persist],\n );\n\n const moveRowTo = useCallback(\n (from: number, to: number) => {\n setState((prev) => {\n const nextRows = moveRow(prev.rows, from, to);\n persist(nextRows, prev.items);\n return { rows: nextRows, items: prev.items };\n });\n },\n [persist],\n );\n\n return (\n <DndProvider backend={HTML5Backend}>\n <Board\n rows={rows}\n items={items}\n widgets={widgets}\n readOnly={readOnly}\n onDropExisting={placeWidgetAt}\n onCreateRowExisting={createRowWith}\n onRemoveWidget={removeWidget}\n onResizeWidget={resizeWidgetPair}\n onResizeRow={resizeRowHeight}\n onMoveRow={moveRowTo}\n renderRowEdgeAdd={renderRowEdgeAdd}\n renderWidgetMenu={renderWidgetMenu}\n renderWidgetConfigDialog={renderWidgetConfigDialog}\n />\n </DndProvider>\n );\n}\n\ninterface BoardProps<TWidget extends DashboardGridWidgetData> {\n rows: RowLike[];\n items: Record<string, WidgetItem>;\n widgets: Record<string, TWidget>;\n readOnly: boolean;\n onDropExisting: (\n opts: { widgetId: string; fromRowId: string },\n targetRowId: string,\n index: number,\n ) => void;\n onCreateRowExisting: (opts: { widgetId: string; fromRowId: string }) => void;\n onRemoveWidget: (widgetId: string, rowId: string) => void;\n onResizeWidget: (leftId: string, rightId: string, nextLeftColSpan: number) => void;\n onResizeRow: (rowId: string, heightPx: number) => void;\n onMoveRow: (from: number, to: number) => void;\n renderRowEdgeAdd?: DashboardGridProps<TWidget>['renderRowEdgeAdd'];\n renderWidgetMenu?: DashboardGridProps<TWidget>['renderWidgetMenu'];\n renderWidgetConfigDialog?: DashboardGridProps<TWidget>['renderWidgetConfigDialog'];\n}\n\nfunction Board<TWidget extends DashboardGridWidgetData>({\n rows,\n items,\n widgets,\n readOnly,\n onDropExisting,\n onCreateRowExisting,\n onRemoveWidget,\n onResizeWidget,\n onResizeRow,\n onMoveRow,\n renderRowEdgeAdd,\n renderWidgetMenu,\n renderWidgetConfigDialog,\n}: BoardProps<TWidget>) {\n // Global \"is anything being dragged\" state — drives every InsertionZone\n // becoming visible at once while a drag is in progress, not just the one\n // under the cursor.\n const dragActive = useDragLayer((monitor) => monitor.isDragging());\n\n return (\n <div className=\"flex w-full flex-col gap-4\">\n {rows.map((row, index) => (\n <SortableRow\n key={row.id}\n row={row}\n index={index}\n items={items}\n widgets={widgets}\n readOnly={readOnly}\n dragActive={dragActive}\n onDropExisting={onDropExisting}\n onRemoveWidget={onRemoveWidget}\n onResizeWidget={onResizeWidget}\n onResizeRow={onResizeRow}\n onMoveRow={onMoveRow}\n renderRowEdgeAdd={renderRowEdgeAdd}\n renderWidgetMenu={renderWidgetMenu}\n renderWidgetConfigDialog={renderWidgetConfigDialog}\n />\n ))}\n {!readOnly ? <NewRowDropZone onDropExisting={onCreateRowExisting} /> : null}\n </div>\n );\n}\n\nfunction NewRowDropZone({\n onDropExisting,\n}: {\n onDropExisting: (opts: { widgetId: string; fromRowId: string }) => void;\n}) {\n const [{ isOver }, dropRef] = useDrop<WidgetDragItem, void, { isOver: boolean }>(() => ({\n accept: ItemTypes.WIDGET,\n drop: (item) => onDropExisting(item),\n collect: (monitor) => ({ isOver: monitor.isOver() }),\n }));\n\n return (\n <div\n ref={(node) => {\n dropRef(node);\n }}\n className={cn(\n 'flex h-12 items-center justify-center rounded-lg border-2 border-dashed border-border text-xs text-muted-foreground transition-colors',\n isOver && 'border-primary bg-primary/5 text-primary',\n )}\n >\n Drop here to start a new row\n </div>\n );\n}\n\ninterface SortableRowProps<TWidget extends DashboardGridWidgetData> {\n row: RowLike;\n index: number;\n items: Record<string, WidgetItem>;\n widgets: Record<string, TWidget>;\n readOnly: boolean;\n dragActive: boolean;\n onDropExisting: BoardProps<TWidget>['onDropExisting'];\n onRemoveWidget: BoardProps<TWidget>['onRemoveWidget'];\n onResizeWidget: BoardProps<TWidget>['onResizeWidget'];\n onResizeRow: BoardProps<TWidget>['onResizeRow'];\n onMoveRow: BoardProps<TWidget>['onMoveRow'];\n renderRowEdgeAdd?: DashboardGridProps<TWidget>['renderRowEdgeAdd'];\n renderWidgetMenu?: DashboardGridProps<TWidget>['renderWidgetMenu'];\n renderWidgetConfigDialog?: DashboardGridProps<TWidget>['renderWidgetConfigDialog'];\n}\n\n// Classic react-dnd sortable-list pattern: each row is simultaneously a drag\n// source (carrying its own index) and a drop target for other rows, swapping\n// positions live as the dragged row hovers over another.\nfunction SortableRow<TWidget extends DashboardGridWidgetData>({\n row,\n index,\n items,\n widgets,\n readOnly,\n dragActive,\n onDropExisting,\n onRemoveWidget,\n onResizeWidget,\n onResizeRow,\n onMoveRow,\n renderRowEdgeAdd,\n renderWidgetMenu,\n renderWidgetConfigDialog,\n}: SortableRowProps<TWidget>) {\n const [{ isDragging }, dragRef, previewRef] = useDrag<RowDragItem, void, { isDragging: boolean }>(\n () => ({\n type: ItemTypes.ROW,\n item: { rowId: row.id, index },\n canDrag: !readOnly,\n collect: (monitor) => ({ isDragging: monitor.isDragging() }),\n }),\n [row.id, index, readOnly],\n );\n\n const [, dropRef] = useDrop<RowDragItem, void, unknown>(\n () => ({\n accept: ItemTypes.ROW,\n canDrop: () => !readOnly,\n hover: (item) => {\n if (readOnly || item.index === index) return;\n onMoveRow(item.index, index);\n item.index = index;\n },\n }),\n [index, readOnly, onMoveRow],\n );\n\n // Merged as a callback ref (not composed via a shared RefObject read\n // during render) — react-dnd's connectors accept either, but reading\n // ref.current mid-render trips react-hooks/refs.\n const setRowRef = useCallback(\n (node: HTMLDivElement | null) => {\n previewRef(node);\n dropRef(node);\n },\n [previewRef, dropRef],\n );\n\n return (\n <div\n ref={setRowRef}\n style={{ height: row.heightPx }}\n className={cn(\n 'group/row relative flex items-stretch gap-2 rounded-lg bg-muted/30 p-2 transition-opacity',\n isDragging && 'opacity-40',\n )}\n >\n {!readOnly ? (\n <div\n ref={(node) => {\n dragRef(node);\n }}\n className=\"flex w-6 shrink-0 cursor-grab items-center justify-center text-muted-foreground active:cursor-grabbing\"\n title=\"Drag to reorder row\"\n >\n <GripVerticalIcon className=\"size-4\" />\n </div>\n ) : null}\n {/* Row-edge \"add widget\" entry points — deliberately siblings of\n RowContent, not rendered inside it, so they sit in their own fixed-\n width gutter rather than eating into RowContent's percentage width\n budget (which only accounts for widgets + InsertionZones — see\n PlacedWidget's widthCss comment). Hidden once the row is already at\n MAX_ITEMS_PER_ROW, mirroring InsertionZone's own canDrop cap. */}\n {!readOnly && row.widgetIds.length < MAX_ITEMS_PER_ROW ? (\n <RowEdgeAddWidget rowId={row.id} edge=\"start\" render={renderRowEdgeAdd} />\n ) : null}\n <RowContent\n row={row}\n items={items}\n widgets={widgets}\n readOnly={readOnly}\n dragActive={dragActive}\n onDropExisting={onDropExisting}\n onRemoveWidget={onRemoveWidget}\n onResizeWidget={onResizeWidget}\n renderWidgetMenu={renderWidgetMenu}\n renderWidgetConfigDialog={renderWidgetConfigDialog}\n />\n {!readOnly && row.widgetIds.length < MAX_ITEMS_PER_ROW ? (\n <RowEdgeAddWidget rowId={row.id} edge=\"end\" render={renderRowEdgeAdd} />\n ) : null}\n {!readOnly ? (\n <RowResizeHandle rowId={row.id} heightPx={row.heightPx} onResize={onResizeRow} />\n ) : null}\n </div>\n );\n}\n\n// Small \"+\" trigger, revealed on hover of the row (group/row, set on\n// SortableRow's own wrapper) — the actual menu/flow it opens is supplied by\n// the caller via `render`.\nfunction RowEdgeAddWidget({\n rowId,\n edge,\n render,\n}: {\n rowId: string;\n edge: 'start' | 'end';\n render?: (opts: { rowId: string; edge: 'start' | 'end' }) => ReactNode;\n}) {\n if (!render) return null;\n return <>{render({ rowId, edge })}</>;\n}\n\ninterface RowContentProps<TWidget extends DashboardGridWidgetData> {\n row: RowLike;\n items: Record<string, WidgetItem>;\n widgets: Record<string, TWidget>;\n readOnly: boolean;\n dragActive: boolean;\n onDropExisting: BoardProps<TWidget>['onDropExisting'];\n onRemoveWidget: BoardProps<TWidget>['onRemoveWidget'];\n onResizeWidget: BoardProps<TWidget>['onResizeWidget'];\n renderWidgetMenu?: DashboardGridProps<TWidget>['renderWidgetMenu'];\n renderWidgetConfigDialog?: DashboardGridProps<TWidget>['renderWidgetConfigDialog'];\n}\n\n// Drop zones live in the margins between widgets (and before the first /\n// after the last), not as a widget-sized block of their own — an\n// InsertionZone per gap, alternating with each PlacedWidget. The zone\n// between two widgets doubles as their resize handle (drag it to trade\n// width between them) — the one before the first widget and after the\n// last has no adjacent pair, so it's a plain drop zone only.\nfunction RowContent<TWidget extends DashboardGridWidgetData>({\n row,\n items,\n widgets,\n readOnly,\n dragActive,\n onDropExisting,\n onRemoveWidget,\n onResizeWidget,\n renderWidgetMenu,\n renderWidgetConfigDialog,\n}: RowContentProps<TWidget>) {\n // Widgets size themselves as a % of this container's actual rendered\n // width (fluid, no fixed board width) — an internal InsertionZone\n // measures this same element to convert a resize drag's pixel delta into\n // a column delta.\n const containerRef = useRef<HTMLDivElement>(null);\n\n // min-w-0 is load-bearing, not decorative: a flex item's default\n // min-width is `auto` (its content's natural size), and a row's content\n // may include elements (e.g. <canvas>) whose width/height are real HTML\n // attributes (not CSS) rather than fluid — without min-w-0, growing a\n // widget can grow such an element, which then becomes part of *this*\n // container's min-content floor on the next layout pass — flex-shrink\n // can't push it back down, so widening a widget ratchets the whole row\n // wider and it never shrinks back (compounding further on each\n // subsequent resize). min-w-0 removes that floor so every widget's\n // calc()'d % width (computed against this element, see above) is the\n // real, sole source of truth for its size.\n return (\n <div ref={containerRef} className=\"flex h-full min-w-0 flex-1 items-stretch\">\n {!readOnly ? (\n <InsertionZone\n rowId={row.id}\n index={0}\n widgetCount={row.widgetIds.length}\n dragActive={dragActive}\n onDropExisting={onDropExisting}\n />\n ) : (\n <ZoneSpacer />\n )}\n {row.widgetIds.map((widgetId, i) => {\n const item = items[widgetId];\n const widget = widgets[widgetId];\n if (!item || !widget) return null;\n return (\n // A Fragment, not a wrapping div — PlacedWidget must be a direct\n // flex child of the row container above, since its percentage\n // width resolves against its immediate parent. A wrapper div here\n // (with no definite width of its own) would make that percentage\n // resolve against an indefinite size and collapse to content\n // width instead.\n <Fragment key={widgetId}>\n <PlacedWidget\n item={item}\n widget={widget}\n rowId={row.id}\n widgetCount={row.widgetIds.length}\n readOnly={readOnly}\n onRemoveWidget={onRemoveWidget}\n renderWidgetMenu={renderWidgetMenu}\n renderWidgetConfigDialog={renderWidgetConfigDialog}\n />\n {!readOnly ? (\n <InsertionZone\n rowId={row.id}\n index={i + 1}\n widgetCount={row.widgetIds.length}\n dragActive={dragActive}\n leftWidgetId={widgetId}\n rightWidgetId={row.widgetIds[i + 1]}\n leftColSpan={item.colSpan}\n containerRef={containerRef}\n onDropExisting={onDropExisting}\n onResizeWidget={onResizeWidget}\n />\n ) : (\n <ZoneSpacer />\n )}\n </Fragment>\n );\n })}\n </div>\n );\n}\n\n// Read-only stand-in for InsertionZone — same fixed footprint, no drop/\n// resize behavior. Every widget's widthCss (see PlacedWidget) subtracts\n// `(widgetCount + 1) * ZONE_WIDTH_PX` from the row on the assumption that\n// many zones actually occupy that space; without a same-sized element still\n// rendered here in read-only mode, that reserved width goes unclaimed by\n// anything (no visible gap between widgets, and dead space at the row's\n// end) rather than becoming the visual gap it's meant to be.\nfunction ZoneSpacer() {\n return <div style={{ width: ZONE_WIDTH_PX }} className=\"shrink-0\" />;\n}\n\nfunction InsertionZone<TWidget extends DashboardGridWidgetData>({\n rowId,\n index,\n widgetCount,\n dragActive,\n leftWidgetId,\n rightWidgetId,\n leftColSpan,\n containerRef,\n onDropExisting,\n onResizeWidget,\n}: {\n rowId: string;\n index: number;\n widgetCount: number;\n dragActive: boolean;\n // Only set for a zone sitting between two widgets — undefined for the\n // leading zone (nothing to its left) and the trailing zone (nothing to\n // its right), which are drop targets only, not resize handles.\n leftWidgetId?: string;\n rightWidgetId?: string;\n leftColSpan?: number;\n containerRef?: React.RefObject<HTMLDivElement | null>;\n onDropExisting: BoardProps<TWidget>['onDropExisting'];\n onResizeWidget?: BoardProps<TWidget>['onResizeWidget'];\n}) {\n const [{ isOver, canDrop }, dropRef] = useDrop<\n WidgetDragItem,\n void,\n { isOver: boolean; canDrop: boolean }\n >(\n () => ({\n accept: ItemTypes.WIDGET,\n // A same-row reorder never grows the row, so it's always allowed —\n // only a drop that would add a net-new member to an already-full row\n // is rejected (mirrors the authoritative check in placeItemAt).\n canDrop: (item) => {\n if (widgetCount < MAX_ITEMS_PER_ROW) return true;\n return item.fromRowId === rowId;\n },\n drop: (item) => onDropExisting(item, rowId, index),\n collect: (monitor) => ({ isOver: monitor.isOver(), canDrop: monitor.canDrop() }),\n }),\n [rowId, index, widgetCount, onDropExisting],\n );\n\n const isResizable = leftWidgetId !== undefined && rightWidgetId !== undefined;\n\n // Plain mouse events (not Pointer Events) — broadest-compatible for a\n // desktop-first drag/resize interaction. Unrelated to the useDrop above —\n // HTML5 DnD's dragenter/dragover/drop events and plain mousedown/\n // mousemove never fire for the same gesture, so the two behaviors can't\n // conflict on one element.\n const handleMouseDown = useCallback(\n (event: React.MouseEvent) => {\n if (\n leftWidgetId === undefined ||\n rightWidgetId === undefined ||\n leftColSpan === undefined ||\n !containerRef?.current ||\n !onResizeWidget\n ) {\n return;\n }\n event.preventDefault();\n event.stopPropagation();\n const containerWidth = containerRef.current.getBoundingClientRect().width;\n if (!containerWidth) return;\n const zonesWidthPx = (widgetCount + 1) * ZONE_WIDTH_PX;\n const colWidthPx = (containerWidth - zonesWidthPx) / COLS;\n const startX = event.clientX;\n const startLeftColSpan = leftColSpan;\n // Rebound to new consts — TS doesn't carry the undefined-narrowing\n // from the guard above into a nested function declaration.\n const resolvedOnResize = onResizeWidget;\n const resolvedLeftId = leftWidgetId;\n const resolvedRightId = rightWidgetId;\n\n function handleMove(moveEvent: MouseEvent) {\n const deltaCols = Math.round((moveEvent.clientX - startX) / colWidthPx);\n resolvedOnResize(resolvedLeftId, resolvedRightId, startLeftColSpan + deltaCols);\n }\n\n function handleUp() {\n window.removeEventListener('mousemove', handleMove);\n window.removeEventListener('mouseup', handleUp);\n }\n\n window.addEventListener('mousemove', handleMove);\n window.addEventListener('mouseup', handleUp);\n },\n [leftWidgetId, rightWidgetId, leftColSpan, containerRef, widgetCount, onResizeWidget],\n );\n\n return (\n <div\n ref={(node) => {\n dropRef(node);\n }}\n onMouseDown={handleMouseDown}\n style={{ width: ZONE_WIDTH_PX }}\n className={cn(\n // Fixed footprint at all times — the \"grows on hover\" affordance\n // comes from a scale transform on the inner pill, not a width\n // change, so it can't perturb every other widget's calc()'d width\n // mid-drag.\n 'flex h-full shrink-0 items-center justify-center',\n isResizable && 'cursor-ew-resize',\n )}\n >\n <div\n className={cn(\n 'h-full w-full rounded-full transition-all',\n isOver && canDrop\n ? 'scale-x-150 bg-primary/25 ring-2 ring-primary'\n : dragActive && canDrop\n ? 'ring-dashed bg-muted-foreground/20 ring-1 ring-border'\n : isResizable\n ? 'bg-transparent hover:bg-muted-foreground/20 hover:ring-1 hover:ring-border'\n : 'bg-transparent',\n )}\n />\n </div>\n );\n}\n\ninterface PlacedWidgetProps<TWidget extends DashboardGridWidgetData> {\n item: WidgetItem;\n widget: TWidget;\n rowId: string;\n widgetCount: number;\n readOnly: boolean;\n onRemoveWidget: BoardProps<TWidget>['onRemoveWidget'];\n renderWidgetMenu?: DashboardGridProps<TWidget>['renderWidgetMenu'];\n renderWidgetConfigDialog?: DashboardGridProps<TWidget>['renderWidgetConfigDialog'];\n}\n\nfunction PlacedWidget<TWidget extends DashboardGridWidgetData>({\n item,\n widget,\n rowId,\n widgetCount,\n readOnly,\n onRemoveWidget,\n renderWidgetMenu,\n renderWidgetConfigDialog,\n}: PlacedWidgetProps<TWidget>) {\n // colSpan's share of the row *after* reserving space for every\n // InsertionZone in it — not a plain colSpan/12 * 100%, or a row with more\n // zones (more widgets) would end up wider than one with fewer, since the\n // zones' real pixel width wouldn't be accounted for.\n const zonesWidthPx = (widgetCount + 1) * ZONE_WIDTH_PX;\n const widthCss = `calc((100% - ${zonesWidthPx}px) * ${item.colSpan} / ${COLS})`;\n\n const [{ isDragging }, dragRef, previewRef] = useDrag<\n WidgetDragItem,\n void,\n { isDragging: boolean }\n >(\n () => ({\n type: ItemTypes.WIDGET,\n item: { widgetId: item.id, fromRowId: rowId },\n canDrag: !readOnly,\n collect: (monitor) => ({ isDragging: monitor.isDragging() }),\n }),\n [item.id, rowId, readOnly],\n );\n\n // Controls the \"Configure\" dialog independently of the action menu's own\n // open/closed state — a menu item click typically closes the menu, but\n // the dialog needs to stay open past that point.\n const [configureOpen, setConfigureOpen] = useState(false);\n\n return (\n <div\n ref={(node) => {\n previewRef(node);\n }}\n style={{ width: widthCss }}\n className={cn(\n 'group relative h-full min-w-0 shrink-0 overflow-hidden transition-opacity',\n isDragging && 'opacity-40',\n )}\n >\n <div className=\"h-full\">{widget.content}</div>\n {!readOnly ? (\n <>\n {/* Hover-revealed controls rather than an always-visible action\n bar — a drag handle opposite the action menu, both absolutely\n positioned over the content rather than reserving their own\n layout space, so every widget's content can use its card's\n full height. */}\n <span\n ref={(node) => {\n dragRef(node);\n }}\n className=\"absolute top-2 left-2 z-10 cursor-grab rounded-md bg-background/80 p-1 text-muted-foreground opacity-0 backdrop-blur transition-opacity group-hover:opacity-100 focus-visible:opacity-100 active:cursor-grabbing\"\n title=\"Drag to move\"\n >\n <GripVerticalIcon className=\"size-3.5\" />\n </span>\n {renderWidgetMenu?.(widget, {\n onConfigure: () => setConfigureOpen(true),\n onDelete: () => onRemoveWidget(widget.id, rowId),\n })}\n {renderWidgetConfigDialog?.({\n widget,\n open: configureOpen,\n onOpenChange: setConfigureOpen,\n })}\n </>\n ) : null}\n </div>\n );\n}\n\nfunction RowResizeHandle({\n rowId,\n heightPx,\n onResize,\n}: {\n rowId: string;\n heightPx: number;\n onResize: (rowId: string, heightPx: number) => void;\n}) {\n const handleMouseDown = useCallback(\n (event: React.MouseEvent) => {\n event.preventDefault();\n event.stopPropagation();\n const startY = event.clientY;\n const startHeight = heightPx;\n\n function handleMove(moveEvent: MouseEvent) {\n const nextHeight = Math.min(\n MAX_ROW_HEIGHT,\n Math.max(MIN_ROW_HEIGHT, startHeight + (moveEvent.clientY - startY)),\n );\n onResize(rowId, nextHeight);\n }\n\n function handleUp() {\n window.removeEventListener('mousemove', handleMove);\n window.removeEventListener('mouseup', handleUp);\n }\n\n window.addEventListener('mousemove', handleMove);\n window.addEventListener('mouseup', handleUp);\n },\n [rowId, heightPx, onResize],\n );\n\n return (\n <div\n onMouseDown={handleMouseDown}\n className=\"absolute inset-x-6 -bottom-2 flex h-3 cursor-ns-resize items-center justify-center\"\n title=\"Drag to resize row height\"\n >\n <div className=\"h-1 w-10 rounded-full bg-border\" />\n </div>\n );\n}\n","// Row/resize layout math for the fluid, multi-column drag-and-drop grid in\n// dashboard-grid.tsx — deliberately zero-dependency and framework-free\n// (no React, no DOM) so it's just as safe to import from server code (to\n// validate/default a colSpan before it ever reaches the client) as from\n// the 'use client' grid component itself.\n//\n// The board is fluid — 100% of its parent's width — with each item's\n// colSpan expressed as a CSS percentage of the row rather than a pixel\n// width computed from a fixed board width. Plain flex handles this fine (no\n// need for CSS Grid's integer-only `grid-column: span`, which wouldn't\n// tolerate rebalanceRow's fractional spans like 12/5 = 2.4 for an odd item\n// count).\n//\n// Insertion zones between items are expected to be real flex siblings (not\n// just a CSS `gap`), so their width eats into the row's 100% budget too,\n// and a row's zone *count* varies with its item count (N items -> N+1\n// zones) — a row with more items has more zones, so naively giving every\n// item colSpan/12 * 100% would leave less-populated rows narrower than\n// fuller ones by the zone width difference. ZONE_WIDTH_PX is kept a fixed,\n// non-interactive-state footprint for exactly this reason — each item's\n// width should be computed as `(100% - (count+1)*ZONE_WIDTH_PX) *\n// colSpan/12`, i.e. colSpan's share of the row *after* reserving space for\n// every zone in it, so every row's total width comes out to exactly 100%\n// regardless of item count. See dashboard-grid.tsx for the component that\n// renders this layout.\nexport const COLS = 12;\nexport const ZONE_WIDTH_PX = 12;\n// Resizing trades space between two adjacent items (see resizeItemPair)\n// rather than growing one independently, so there's no separate max — an\n// item's ceiling is just \"however much its neighbor is willing to give up,\"\n// down to MIN_COL_SPAN each.\nexport const MIN_COL_SPAN = 1;\nexport const MAX_ITEMS_PER_ROW = 4;\nexport const DEFAULT_ROW_HEIGHT = 220;\nexport const MIN_ROW_HEIGHT = 80;\nexport const MAX_ROW_HEIGHT = 480;\n\nexport interface RowLike {\n id: string;\n widgetIds: string[];\n heightPx: number;\n}\n\nexport interface SizedItem {\n colSpan: number;\n}\n\n// Every item in a row gets an equal share of the 12 columns — e.g. a 3rd\n// item landing in a row of 2 resizes all three to 4. Only call this where a\n// row's item count actually changed (add/remove), never on a same-row\n// reorder, so manual width tweaks survive plain drag-to-reorder.\nexport function rebalanceRow<T extends SizedItem>(\n items: Record<string, T>,\n row: { widgetIds: string[] },\n): Record<string, T> {\n if (row.widgetIds.length === 0) return items;\n const span = COLS / row.widgetIds.length;\n const next = { ...items };\n for (const id of row.widgetIds) {\n const item = next[id];\n if (item && item.colSpan !== span) next[id] = { ...item, colSpan: span };\n }\n return next;\n}\n\n// A same-row reorder never grows the row, so it's always allowed — anything\n// that would add a net-new member to an already-full row is rejected. The\n// authoritative version of this check (a UI's own canDrop is typically the\n// UI-level version, for the \"can't drop here\" affordance).\nexport function canPlaceInRow(\n row: { widgetIds: string[] } | undefined,\n isSameRowMove: boolean,\n): boolean {\n if (isSameRowMove) return true;\n return (row?.widgetIds.length ?? 0) < MAX_ITEMS_PER_ROW;\n}\n\n// Moves (or, when fromRowId is undefined, inserts a not-yet-placed item —\n// e.g. one just spawned/created) itemId into targetRowId at `index`\n// (0..widgetIds.length), rebalancing whichever rows' item counts actually\n// changed. `items` must already contain itemId (the caller creates/spawns\n// it before calling this). Enforces the row cap itself (see canPlaceInRow)\n// as a no-op return, not just via the caller's UI-level check.\nexport function placeItemAt<T extends SizedItem>(\n rows: RowLike[],\n items: Record<string, T>,\n itemId: string,\n fromRowId: string | undefined,\n targetRowId: string,\n index: number,\n): { rows: RowLike[]; items: Record<string, T> } {\n const targetRowBefore = rows.find((row) => row.id === targetRowId);\n const targetCountBefore = targetRowBefore?.widgetIds.length ?? 0;\n const isSameRowMove = fromRowId === targetRowId;\n if (!canPlaceInRow(targetRowBefore, isSameRowMove)) {\n return { rows, items };\n }\n\n let nextRows = rows;\n let nextItems = items;\n let insertIndex = index;\n let sourceCountBefore = 0;\n\n if (fromRowId !== undefined) {\n const fromRow = rows.find((row) => row.id === fromRowId);\n sourceCountBefore = fromRow?.widgetIds.length ?? 0;\n const fromIndex = fromRow?.widgetIds.indexOf(itemId) ?? -1;\n // Removing an earlier item from the same row shifts every later index\n // left by one, including the drop target itself.\n if (fromRowId === targetRowId && fromIndex !== -1 && fromIndex < insertIndex) {\n insertIndex -= 1;\n }\n nextRows = nextRows.map((row) =>\n row.id === fromRowId\n ? { ...row, widgetIds: row.widgetIds.filter((id) => id !== itemId) }\n : row,\n );\n nextRows = nextRows.filter((row) => row.widgetIds.length > 0 || row.id === targetRowId);\n }\n\n nextRows = nextRows.map((row) => {\n if (row.id !== targetRowId) return row;\n const next = [...row.widgetIds];\n next.splice(Math.min(Math.max(insertIndex, 0), next.length), 0, itemId);\n return { ...row, widgetIds: next };\n });\n\n // Equal-width redistribution only where a row's item count actually\n // changed — the row the item landed in, and (for a cross-row move) the\n // row it left. A same-row reorder changes neither count, so existing\n // widths are left alone.\n const targetRowAfter = nextRows.find((row) => row.id === targetRowId);\n if (targetRowAfter && targetRowAfter.widgetIds.length !== targetCountBefore) {\n nextItems = rebalanceRow(nextItems, targetRowAfter);\n }\n if (fromRowId !== undefined && fromRowId !== targetRowId) {\n const sourceRowAfter = nextRows.find((row) => row.id === fromRowId);\n if (sourceRowAfter && sourceRowAfter.widgetIds.length !== sourceCountBefore) {\n nextItems = rebalanceRow(nextItems, sourceRowAfter);\n }\n }\n\n return { rows: nextRows, items: nextItems };\n}\n\n// Moves (or inserts) itemId into a brand-new row of its own — e.g. dropped\n// on a \"start a new row\" zone. newRowId is caller-generated (client-side\n// temp id or a real one, depending on caller).\nexport function placeItemInNewRow<T extends SizedItem>(\n rows: RowLike[],\n items: Record<string, T>,\n itemId: string,\n fromRowId: string | undefined,\n newRowId: string,\n heightPx: number = DEFAULT_ROW_HEIGHT,\n): { rows: RowLike[]; items: Record<string, T> } {\n let nextRows = rows;\n let nextItems = items;\n\n if (fromRowId !== undefined) {\n const fromRow = rows.find((row) => row.id === fromRowId);\n nextRows = nextRows\n .map((row) =>\n row.id === fromRowId\n ? { ...row, widgetIds: row.widgetIds.filter((id) => id !== itemId) }\n : row,\n )\n .filter((row) => row.widgetIds.length > 0);\n const sourceRowAfter = nextRows.find((row) => row.id === fromRowId);\n if (sourceRowAfter && sourceRowAfter.widgetIds.length !== fromRow?.widgetIds.length) {\n nextItems = rebalanceRow(nextItems, sourceRowAfter);\n }\n }\n\n const newRow: RowLike = { id: newRowId, widgetIds: [itemId], heightPx };\n nextItems = rebalanceRow(nextItems, newRow);\n\n return { rows: [...nextRows, newRow], items: nextItems };\n}\n\n// Removes itemId from rowId entirely (not moved elsewhere — e.g. deleted),\n// rebalancing whatever's left in that row, or dropping the row if it was\n// the last member.\nexport function removeItemFromRow<T extends SizedItem>(\n rows: RowLike[],\n items: Record<string, T>,\n itemId: string,\n rowId: string,\n): { rows: RowLike[]; items: Record<string, T> } {\n const nextRows = rows\n .map((row) =>\n row.id === rowId ? { ...row, widgetIds: row.widgetIds.filter((id) => id !== itemId) } : row,\n )\n .filter((row) => row.widgetIds.length > 0);\n const withoutItem = { ...items };\n delete withoutItem[itemId];\n const rowAfter = nextRows.find((row) => row.id === rowId);\n const nextItems = rowAfter ? rebalanceRow(withoutItem, rowAfter) : withoutItem;\n return { rows: nextRows, items: nextItems };\n}\n\n// Resizing always trades space between two *adjacent* items, never changes\n// either alone — leftColSpan + rightColSpan stays constant, so (combined\n// with rebalanceRow keeping every row's total at exactly COLS whenever its\n// item count changes) a row's items always sum to COLS, full stop,\n// regardless of how much manual resizing happens in between.\n// nextLeftColSpan is an absolute target (not a delta) — callers compute it\n// fresh from their drag-start snapshot each mousemove.\nexport function resizeItemPair<T extends SizedItem>(\n items: Record<string, T>,\n leftId: string,\n rightId: string,\n nextLeftColSpan: number,\n): Record<string, T> {\n const left = items[leftId];\n const right = items[rightId];\n if (!left || !right) return items;\n const combined = left.colSpan + right.colSpan;\n const clampedLeft = Math.min(combined - MIN_COL_SPAN, Math.max(MIN_COL_SPAN, nextLeftColSpan));\n const nextRight = combined - clampedLeft;\n return {\n ...items,\n [leftId]: { ...left, colSpan: clampedLeft },\n [rightId]: { ...right, colSpan: nextRight },\n };\n}\n\nexport function resizeRow(rows: RowLike[], rowId: string, heightPx: number): RowLike[] {\n return rows.map((row) => (row.id === rowId ? { ...row, heightPx } : row));\n}\n\nexport function moveRow(rows: RowLike[], from: number, to: number): RowLike[] {\n const next = [...rows];\n const [moved] = next.splice(from, 1);\n next.splice(to, 0, moved);\n return next;\n}\n","import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,76 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ declare const COLS = 12;
5
+ declare const ZONE_WIDTH_PX = 12;
6
+ declare const MIN_COL_SPAN = 1;
7
+ declare const MAX_ITEMS_PER_ROW = 4;
8
+ declare const DEFAULT_ROW_HEIGHT = 220;
9
+ declare const MIN_ROW_HEIGHT = 80;
10
+ declare const MAX_ROW_HEIGHT = 480;
11
+ interface RowLike {
12
+ id: string;
13
+ widgetIds: string[];
14
+ heightPx: number;
15
+ }
16
+ interface SizedItem {
17
+ colSpan: number;
18
+ }
19
+ declare function rebalanceRow<T extends SizedItem>(items: Record<string, T>, row: {
20
+ widgetIds: string[];
21
+ }): Record<string, T>;
22
+ declare function canPlaceInRow(row: {
23
+ widgetIds: string[];
24
+ } | undefined, isSameRowMove: boolean): boolean;
25
+ declare function placeItemAt<T extends SizedItem>(rows: RowLike[], items: Record<string, T>, itemId: string, fromRowId: string | undefined, targetRowId: string, index: number): {
26
+ rows: RowLike[];
27
+ items: Record<string, T>;
28
+ };
29
+ declare function placeItemInNewRow<T extends SizedItem>(rows: RowLike[], items: Record<string, T>, itemId: string, fromRowId: string | undefined, newRowId: string, heightPx?: number): {
30
+ rows: RowLike[];
31
+ items: Record<string, T>;
32
+ };
33
+ declare function removeItemFromRow<T extends SizedItem>(rows: RowLike[], items: Record<string, T>, itemId: string, rowId: string): {
34
+ rows: RowLike[];
35
+ items: Record<string, T>;
36
+ };
37
+ declare function resizeItemPair<T extends SizedItem>(items: Record<string, T>, leftId: string, rightId: string, nextLeftColSpan: number): Record<string, T>;
38
+ declare function resizeRow(rows: RowLike[], rowId: string, heightPx: number): RowLike[];
39
+ declare function moveRow(rows: RowLike[], from: number, to: number): RowLike[];
40
+
41
+ interface DashboardGridWidgetData {
42
+ id: string;
43
+ colSpan: number;
44
+ content: ReactNode;
45
+ }
46
+ interface DashboardGridProps<TWidget extends DashboardGridWidgetData = DashboardGridWidgetData> {
47
+ rows: RowLike[];
48
+ widgets: Record<string, TWidget>;
49
+ readOnly?: boolean;
50
+ /** Debounce (ms) before onUpdateLayout fires after a drag/resize. Default 600. */
51
+ saveDebounceMs?: number;
52
+ onUpdateLayout?: (rows: RowLike[], widgetColSpans: {
53
+ id: string;
54
+ colSpan: number;
55
+ }[]) => void;
56
+ onDeleteWidget?: (widgetId: string) => void;
57
+ /** Renders the "add widget" affordance at the start/end of a row. Omit to disable. */
58
+ renderRowEdgeAdd?: (opts: {
59
+ rowId: string;
60
+ edge: 'start' | 'end';
61
+ }) => ReactNode;
62
+ /** Renders a widget's action menu (e.g. configure/delete). Omit to disable. */
63
+ renderWidgetMenu?: (widget: TWidget, actions: {
64
+ onConfigure: () => void;
65
+ onDelete: () => void;
66
+ }) => ReactNode;
67
+ /** Renders a widget's configuration dialog, controlled via `open`/`onOpenChange`. */
68
+ renderWidgetConfigDialog?: (opts: {
69
+ widget: TWidget;
70
+ open: boolean;
71
+ onOpenChange: (open: boolean) => void;
72
+ }) => ReactNode;
73
+ }
74
+ declare function DashboardGrid<TWidget extends DashboardGridWidgetData = DashboardGridWidgetData>({ rows: initialRows, widgets, readOnly, saveDebounceMs, onUpdateLayout, onDeleteWidget, renderRowEdgeAdd, renderWidgetMenu, renderWidgetConfigDialog, }: DashboardGridProps<TWidget>): react.JSX.Element;
75
+
76
+ export { COLS, DEFAULT_ROW_HEIGHT, DashboardGrid, type DashboardGridProps, type DashboardGridWidgetData, MAX_ITEMS_PER_ROW, MAX_ROW_HEIGHT, MIN_COL_SPAN, MIN_ROW_HEIGHT, type RowLike, type SizedItem, ZONE_WIDTH_PX, canPlaceInRow, moveRow, placeItemAt, placeItemInNewRow, rebalanceRow, removeItemFromRow, resizeItemPair, resizeRow };
@@ -0,0 +1,76 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ declare const COLS = 12;
5
+ declare const ZONE_WIDTH_PX = 12;
6
+ declare const MIN_COL_SPAN = 1;
7
+ declare const MAX_ITEMS_PER_ROW = 4;
8
+ declare const DEFAULT_ROW_HEIGHT = 220;
9
+ declare const MIN_ROW_HEIGHT = 80;
10
+ declare const MAX_ROW_HEIGHT = 480;
11
+ interface RowLike {
12
+ id: string;
13
+ widgetIds: string[];
14
+ heightPx: number;
15
+ }
16
+ interface SizedItem {
17
+ colSpan: number;
18
+ }
19
+ declare function rebalanceRow<T extends SizedItem>(items: Record<string, T>, row: {
20
+ widgetIds: string[];
21
+ }): Record<string, T>;
22
+ declare function canPlaceInRow(row: {
23
+ widgetIds: string[];
24
+ } | undefined, isSameRowMove: boolean): boolean;
25
+ declare function placeItemAt<T extends SizedItem>(rows: RowLike[], items: Record<string, T>, itemId: string, fromRowId: string | undefined, targetRowId: string, index: number): {
26
+ rows: RowLike[];
27
+ items: Record<string, T>;
28
+ };
29
+ declare function placeItemInNewRow<T extends SizedItem>(rows: RowLike[], items: Record<string, T>, itemId: string, fromRowId: string | undefined, newRowId: string, heightPx?: number): {
30
+ rows: RowLike[];
31
+ items: Record<string, T>;
32
+ };
33
+ declare function removeItemFromRow<T extends SizedItem>(rows: RowLike[], items: Record<string, T>, itemId: string, rowId: string): {
34
+ rows: RowLike[];
35
+ items: Record<string, T>;
36
+ };
37
+ declare function resizeItemPair<T extends SizedItem>(items: Record<string, T>, leftId: string, rightId: string, nextLeftColSpan: number): Record<string, T>;
38
+ declare function resizeRow(rows: RowLike[], rowId: string, heightPx: number): RowLike[];
39
+ declare function moveRow(rows: RowLike[], from: number, to: number): RowLike[];
40
+
41
+ interface DashboardGridWidgetData {
42
+ id: string;
43
+ colSpan: number;
44
+ content: ReactNode;
45
+ }
46
+ interface DashboardGridProps<TWidget extends DashboardGridWidgetData = DashboardGridWidgetData> {
47
+ rows: RowLike[];
48
+ widgets: Record<string, TWidget>;
49
+ readOnly?: boolean;
50
+ /** Debounce (ms) before onUpdateLayout fires after a drag/resize. Default 600. */
51
+ saveDebounceMs?: number;
52
+ onUpdateLayout?: (rows: RowLike[], widgetColSpans: {
53
+ id: string;
54
+ colSpan: number;
55
+ }[]) => void;
56
+ onDeleteWidget?: (widgetId: string) => void;
57
+ /** Renders the "add widget" affordance at the start/end of a row. Omit to disable. */
58
+ renderRowEdgeAdd?: (opts: {
59
+ rowId: string;
60
+ edge: 'start' | 'end';
61
+ }) => ReactNode;
62
+ /** Renders a widget's action menu (e.g. configure/delete). Omit to disable. */
63
+ renderWidgetMenu?: (widget: TWidget, actions: {
64
+ onConfigure: () => void;
65
+ onDelete: () => void;
66
+ }) => ReactNode;
67
+ /** Renders a widget's configuration dialog, controlled via `open`/`onOpenChange`. */
68
+ renderWidgetConfigDialog?: (opts: {
69
+ widget: TWidget;
70
+ open: boolean;
71
+ onOpenChange: (open: boolean) => void;
72
+ }) => ReactNode;
73
+ }
74
+ declare function DashboardGrid<TWidget extends DashboardGridWidgetData = DashboardGridWidgetData>({ rows: initialRows, widgets, readOnly, saveDebounceMs, onUpdateLayout, onDeleteWidget, renderRowEdgeAdd, renderWidgetMenu, renderWidgetConfigDialog, }: DashboardGridProps<TWidget>): react.JSX.Element;
75
+
76
+ export { COLS, DEFAULT_ROW_HEIGHT, DashboardGrid, type DashboardGridProps, type DashboardGridWidgetData, MAX_ITEMS_PER_ROW, MAX_ROW_HEIGHT, MIN_COL_SPAN, MIN_ROW_HEIGHT, type RowLike, type SizedItem, ZONE_WIDTH_PX, canPlaceInRow, moveRow, placeItemAt, placeItemInNewRow, rebalanceRow, removeItemFromRow, resizeItemPair, resizeRow };