import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from "react"; import { GridLayout, useContainerWidth, verticalCompactor, type Layout, type LayoutItem } from "react-grid-layout"; import { containerBounds, gridBounds, minMaxSize } from "react-grid-layout/core"; import { movePanel, panelNavItem, resizePanel, setPanelVisible, type DashboardLayout, type DashboardPanel, type PanelWidth, } from "./dashboard-layout"; import { applyGridPlacement, gridColumnsForContainer, placementsForLayout, rowsForContent, snapGridWidth, type GridColumns, type GridPlacement, } from "./dashboard-grid"; const ROW_HEIGHT = 8; const GRID_MARGIN = 12; const WIDTHS: PanelWidth[] = [4, 6, 8, 12]; export function DashboardEditor({ layout, onCommit, onHide, renderPanel, panelDomId, }: { layout: DashboardLayout; onCommit: (next: DashboardLayout, message: string) => void; onHide: (panel: DashboardPanel) => void; renderPanel: (panel: DashboardPanel) => ReactNode; panelDomId: (panel: DashboardPanel) => string; }) { const { width, containerRef } = useContainerWidth({ initialWidth: 1200 }); const gridWidth = width > 0 ? width : 1200; const columns = gridColumnsForContainer(gridWidth); const [heights, setHeights] = useState>({}); const [live, setLive] = useState(() => placementsForLayout(layout, columns)); const [restoreOpen, setRestoreOpen] = useState(false); const interacting = useRef(false); const gridRef = useRef(null); const visible = layout.panels.filter((panel) => panel.visible); const hidden = layout.panels.filter((panel) => !panel.visible); const placementKey = placementsForLayout(layout, columns, heights) .map((item) => `${item.i}:${item.x}:${item.y}:${item.w}:${item.h}`) .join("|"); useEffect(() => { if (interacting.current) return; setLive(placementsForLayout(layout, columns, heights)); }, [placementKey, columns, heights, layout]); useLayoutEffect(() => { const root = gridRef.current; if (!root) return; const next: Record = {}; const items = root.querySelectorAll("[data-panel-id]"); for (let index = 0; index < items.length; index += 1) { const item = items[index]; if (!(item instanceof HTMLElement)) continue; const id = item.dataset.panelId; if (!id) continue; const tools = item.querySelector(".cw-panel-tools"); const body = item.querySelector(".cw-panel-body"); const toolsHeight = tools instanceof HTMLElement ? tools.offsetHeight : 0; const bodyHeight = body instanceof HTMLElement ? body.scrollHeight : 0; next[id] = rowsForContent(toolsHeight + bodyHeight, ROW_HEIGHT, GRID_MARGIN); } setHeights((current) => (sameHeights(current, next) ? current : next)); }, [placementKey]); function commit(next: Layout, message: string) { interacting.current = false; const packed = applyGridPlacement(layout, next, columns); setLive(placementsForLayout(packed, columns, heights)); if (sameLayout(layout, packed)) return; onCommit(packed, message); } return (

Drag a handle to move a panel. Drag either edge to resize it.

{hidden.length > 0 ? (
{restoreOpen ? (
    {hidden.map((panel) => { const label = panelNavItem(panel.type).label; return (
  • ); })}
) : null}
) : null}
1, handles: ["e", "w"] }} constraints={[ gridBounds, minMaxSize, containerBounds, { name: "causewise-widths", constrainSize(item, proposed, _height, _handle, context) { return { w: snapGridWidth(proposed, columns, Math.max(1, context.cols - item.x)), h: item.h }; }, }, ]} onDragStart={() => { interacting.current = true; }} onResizeStart={() => { interacting.current = true; }} onLayoutChange={(next) => { if (interacting.current) setLive(toPlacements(next)); }} onDragStop={(next) => commit(next, "Layout updated.")} onResizeStop={(next) => commit(next, "Layout updated.")} > {visible.map((panel) => { const label = panelNavItem(panel.type).label; return (
{renderPanel(panel)}
); })}
); } function onGripKey( key: string, panel: DashboardPanel, layout: DashboardLayout, columns: GridColumns, onCommit: (next: DashboardLayout, message: string) => void, ) { const label = panelNavItem(panel.type).label; if (key === "ArrowUp" || key === "ArrowDown") { const next = movePanel(layout, panel.id, key === "ArrowUp" ? "earlier" : "later"); if (next !== layout) onCommit(next, `${label} moved ${key === "ArrowUp" ? "earlier" : "later"}.`); return; } if ((key === "ArrowLeft" || key === "ArrowRight") && columns > 1) { const current = WIDTHS.indexOf(panel.w as PanelWidth); const index = current < 0 ? WIDTHS.length - 1 : current; const next = WIDTHS[Math.min(WIDTHS.length - 1, Math.max(0, index + (key === "ArrowRight" ? 1 : -1)))]; if (next !== panel.w) onCommit(resizePanel(layout, panel.id, next), `${label} width updated.`); } } function toPlacements(layout: Layout): GridPlacement[] { return layout.map((item: LayoutItem) => ({ i: item.i, x: item.x, y: item.y, w: item.w, h: item.h })); } function sameHeights(current: Record, next: Record) { const keys = Object.keys(next); if (keys.length !== Object.keys(current).length) return false; return keys.every((key) => current[key] === next[key]); } function sameLayout(current: DashboardLayout, next: DashboardLayout) { return current.panels.every((panel, index) => { const other = next.panels[index]; return other && panel.id === other.id && panel.visible === other.visible && panel.w === other.w && panel.x === other.x && panel.y === other.y; }); } function GripIcon() { return ( ); }