@vertly/dashboard-grid 0.1.0 → 0.2.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.
- package/README.md +56 -2
- package/dist/cn.cjs +33 -0
- package/dist/cn.cjs.map +1 -0
- package/dist/cn.d.cts +5 -0
- package/dist/cn.d.ts +5 -0
- package/dist/cn.js +9 -0
- package/dist/cn.js.map +1 -0
- package/dist/dashboard-grid.cjs +621 -0
- package/dist/dashboard-grid.cjs.map +1 -0
- package/dist/dashboard-grid.d.cts +53 -0
- package/dist/dashboard-grid.d.ts +53 -0
- package/dist/dashboard-grid.js +608 -0
- package/dist/dashboard-grid.js.map +1 -0
- package/dist/index.cjs +20 -690
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -76
- package/dist/index.d.ts +3 -76
- package/dist/index.js +19 -672
- package/dist/index.js.map +1 -1
- package/dist/layout-engine.cjs +167 -0
- package/dist/layout-engine.cjs.map +1 -0
- package/dist/layout-engine.d.cts +38 -0
- package/dist/layout-engine.d.ts +38 -0
- package/dist/layout-engine.js +129 -0
- package/dist/layout-engine.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/dashboard-grid.tsx"],"sourcesContent":["'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\n// react-dnd item type for a consumer's own draggable \"spawn a new widget\"\n// source (e.g. a palette of widget kinds) — give your palette items'\n// `useDrag` this exact `type` string, and the row/insertion drop zones will\n// accept them alongside internal widget drags, forwarding the raw dragged\n// item to onDropExternalItem/onCreateRowExternalItem. Only accepted at all\n// when one of those two props is provided, so this is a no-op for\n// consumers who don't use it.\nexport const EXTERNAL_ITEM_TYPE = 'vertly-dashboard-grid:external-item';\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 /**\n * Called when a drag source using EXTERNAL_ITEM_TYPE (e.g. a widget-kind\n * palette) is dropped into an existing row at `index`. `item` is whatever\n * raw payload that drag source's `useDrag` supplied — this component\n * never inspects it. Only accepted by the row's drop zones once this is\n * provided (see also onCreateRowExternalItem, and the note on\n * add/remove needing a `key` change under \"Adding or removing a widget\"\n * in the README).\n */\n onDropExternalItem?: (item: unknown, targetRowId: string, index: number) => void;\n /** Same as onDropExternalItem, but for a drop on the \"start a new row\" zone. */\n onCreateRowExternalItem?: (item: unknown) => 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 onDropExternalItem,\n onCreateRowExternalItem,\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 onDropExternalItem={onDropExternalItem}\n onCreateRowExternalItem={onCreateRowExternalItem}\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 onDropExternalItem?: DashboardGridProps<TWidget>['onDropExternalItem'];\n onCreateRowExternalItem?: DashboardGridProps<TWidget>['onCreateRowExternalItem'];\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 onDropExternalItem,\n onCreateRowExternalItem,\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 onDropExternalItem={onDropExternalItem}\n renderRowEdgeAdd={renderRowEdgeAdd}\n renderWidgetMenu={renderWidgetMenu}\n renderWidgetConfigDialog={renderWidgetConfigDialog}\n />\n ))}\n {!readOnly ? (\n <NewRowDropZone\n onDropExisting={onCreateRowExisting}\n onCreateRowExternalItem={onCreateRowExternalItem}\n />\n ) : null}\n </div>\n );\n}\n\nfunction NewRowDropZone({\n onDropExisting,\n onCreateRowExternalItem,\n}: {\n onDropExisting: (opts: { widgetId: string; fromRowId: string }) => void;\n onCreateRowExternalItem?: (item: unknown) => void;\n}) {\n const [{ isOver }, dropRef] = useDrop<unknown, void, { isOver: boolean }>(\n () => ({\n accept: onCreateRowExternalItem ? [ItemTypes.WIDGET, EXTERNAL_ITEM_TYPE] : ItemTypes.WIDGET,\n drop: (item, monitor) => {\n if (monitor.getItemType() === EXTERNAL_ITEM_TYPE) {\n onCreateRowExternalItem?.(item);\n } else {\n onDropExisting(item as WidgetDragItem);\n }\n },\n collect: (monitor) => ({ isOver: monitor.isOver() }),\n }),\n [onDropExisting, onCreateRowExternalItem],\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 onDropExternalItem?: DashboardGridProps<TWidget>['onDropExternalItem'];\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 onDropExternalItem,\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 onDropExternalItem={onDropExternalItem}\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 onDropExternalItem?: DashboardGridProps<TWidget>['onDropExternalItem'];\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 onDropExternalItem,\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 onDropExternalItem={onDropExternalItem}\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 onDropExternalItem={onDropExternalItem}\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 onDropExternalItem,\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 onDropExternalItem?: BoardProps<TWidget>['onDropExternalItem'];\n onResizeWidget?: BoardProps<TWidget>['onResizeWidget'];\n}) {\n const [{ isOver, canDrop }, dropRef] = useDrop<\n unknown,\n void,\n { isOver: boolean; canDrop: boolean }\n >(\n () => ({\n accept: onDropExternalItem ? [ItemTypes.WIDGET, EXTERNAL_ITEM_TYPE] : 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). A\n // dropped external item is always a net-new member (there's nothing\n // to reorder), so it only ever gets the row-not-full check.\n canDrop: (item, monitor) => {\n if (monitor.getItemType() === EXTERNAL_ITEM_TYPE) {\n return widgetCount < MAX_ITEMS_PER_ROW;\n }\n if (widgetCount < MAX_ITEMS_PER_ROW) return true;\n return (item as WidgetDragItem).fromRowId === rowId;\n },\n drop: (item, monitor) => {\n if (monitor.getItemType() === EXTERNAL_ITEM_TYPE) {\n onDropExternalItem?.(item, rowId, index);\n } else {\n onDropExisting(item as WidgetDragItem, rowId, index);\n }\n },\n collect: (monitor) => ({ isOver: monitor.isOver(), canDrop: monitor.canDrop() }),\n }),\n [rowId, index, widgetCount, onDropExisting, onDropExternalItem],\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"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { RowLike } from './layout-engine.cjs';
|
|
4
|
+
|
|
5
|
+
interface DashboardGridWidgetData {
|
|
6
|
+
id: string;
|
|
7
|
+
colSpan: number;
|
|
8
|
+
content: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
declare const EXTERNAL_ITEM_TYPE = "vertly-dashboard-grid:external-item";
|
|
11
|
+
interface DashboardGridProps<TWidget extends DashboardGridWidgetData = DashboardGridWidgetData> {
|
|
12
|
+
rows: RowLike[];
|
|
13
|
+
widgets: Record<string, TWidget>;
|
|
14
|
+
readOnly?: boolean;
|
|
15
|
+
/** Debounce (ms) before onUpdateLayout fires after a drag/resize. Default 600. */
|
|
16
|
+
saveDebounceMs?: number;
|
|
17
|
+
onUpdateLayout?: (rows: RowLike[], widgetColSpans: {
|
|
18
|
+
id: string;
|
|
19
|
+
colSpan: number;
|
|
20
|
+
}[]) => void;
|
|
21
|
+
onDeleteWidget?: (widgetId: string) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Called when a drag source using EXTERNAL_ITEM_TYPE (e.g. a widget-kind
|
|
24
|
+
* palette) is dropped into an existing row at `index`. `item` is whatever
|
|
25
|
+
* raw payload that drag source's `useDrag` supplied — this component
|
|
26
|
+
* never inspects it. Only accepted by the row's drop zones once this is
|
|
27
|
+
* provided (see also onCreateRowExternalItem, and the note on
|
|
28
|
+
* add/remove needing a `key` change under "Adding or removing a widget"
|
|
29
|
+
* in the README).
|
|
30
|
+
*/
|
|
31
|
+
onDropExternalItem?: (item: unknown, targetRowId: string, index: number) => void;
|
|
32
|
+
/** Same as onDropExternalItem, but for a drop on the "start a new row" zone. */
|
|
33
|
+
onCreateRowExternalItem?: (item: unknown) => void;
|
|
34
|
+
/** Renders the "add widget" affordance at the start/end of a row. Omit to disable. */
|
|
35
|
+
renderRowEdgeAdd?: (opts: {
|
|
36
|
+
rowId: string;
|
|
37
|
+
edge: 'start' | 'end';
|
|
38
|
+
}) => ReactNode;
|
|
39
|
+
/** Renders a widget's action menu (e.g. configure/delete). Omit to disable. */
|
|
40
|
+
renderWidgetMenu?: (widget: TWidget, actions: {
|
|
41
|
+
onConfigure: () => void;
|
|
42
|
+
onDelete: () => void;
|
|
43
|
+
}) => ReactNode;
|
|
44
|
+
/** Renders a widget's configuration dialog, controlled via `open`/`onOpenChange`. */
|
|
45
|
+
renderWidgetConfigDialog?: (opts: {
|
|
46
|
+
widget: TWidget;
|
|
47
|
+
open: boolean;
|
|
48
|
+
onOpenChange: (open: boolean) => void;
|
|
49
|
+
}) => ReactNode;
|
|
50
|
+
}
|
|
51
|
+
declare function DashboardGrid<TWidget extends DashboardGridWidgetData = DashboardGridWidgetData>({ rows: initialRows, widgets, readOnly, saveDebounceMs, onUpdateLayout, onDeleteWidget, onDropExternalItem, onCreateRowExternalItem, renderRowEdgeAdd, renderWidgetMenu, renderWidgetConfigDialog, }: DashboardGridProps<TWidget>): react.JSX.Element;
|
|
52
|
+
|
|
53
|
+
export { DashboardGrid, type DashboardGridProps, type DashboardGridWidgetData, EXTERNAL_ITEM_TYPE };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { RowLike } from './layout-engine.js';
|
|
4
|
+
|
|
5
|
+
interface DashboardGridWidgetData {
|
|
6
|
+
id: string;
|
|
7
|
+
colSpan: number;
|
|
8
|
+
content: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
declare const EXTERNAL_ITEM_TYPE = "vertly-dashboard-grid:external-item";
|
|
11
|
+
interface DashboardGridProps<TWidget extends DashboardGridWidgetData = DashboardGridWidgetData> {
|
|
12
|
+
rows: RowLike[];
|
|
13
|
+
widgets: Record<string, TWidget>;
|
|
14
|
+
readOnly?: boolean;
|
|
15
|
+
/** Debounce (ms) before onUpdateLayout fires after a drag/resize. Default 600. */
|
|
16
|
+
saveDebounceMs?: number;
|
|
17
|
+
onUpdateLayout?: (rows: RowLike[], widgetColSpans: {
|
|
18
|
+
id: string;
|
|
19
|
+
colSpan: number;
|
|
20
|
+
}[]) => void;
|
|
21
|
+
onDeleteWidget?: (widgetId: string) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Called when a drag source using EXTERNAL_ITEM_TYPE (e.g. a widget-kind
|
|
24
|
+
* palette) is dropped into an existing row at `index`. `item` is whatever
|
|
25
|
+
* raw payload that drag source's `useDrag` supplied — this component
|
|
26
|
+
* never inspects it. Only accepted by the row's drop zones once this is
|
|
27
|
+
* provided (see also onCreateRowExternalItem, and the note on
|
|
28
|
+
* add/remove needing a `key` change under "Adding or removing a widget"
|
|
29
|
+
* in the README).
|
|
30
|
+
*/
|
|
31
|
+
onDropExternalItem?: (item: unknown, targetRowId: string, index: number) => void;
|
|
32
|
+
/** Same as onDropExternalItem, but for a drop on the "start a new row" zone. */
|
|
33
|
+
onCreateRowExternalItem?: (item: unknown) => void;
|
|
34
|
+
/** Renders the "add widget" affordance at the start/end of a row. Omit to disable. */
|
|
35
|
+
renderRowEdgeAdd?: (opts: {
|
|
36
|
+
rowId: string;
|
|
37
|
+
edge: 'start' | 'end';
|
|
38
|
+
}) => ReactNode;
|
|
39
|
+
/** Renders a widget's action menu (e.g. configure/delete). Omit to disable. */
|
|
40
|
+
renderWidgetMenu?: (widget: TWidget, actions: {
|
|
41
|
+
onConfigure: () => void;
|
|
42
|
+
onDelete: () => void;
|
|
43
|
+
}) => ReactNode;
|
|
44
|
+
/** Renders a widget's configuration dialog, controlled via `open`/`onOpenChange`. */
|
|
45
|
+
renderWidgetConfigDialog?: (opts: {
|
|
46
|
+
widget: TWidget;
|
|
47
|
+
open: boolean;
|
|
48
|
+
onOpenChange: (open: boolean) => void;
|
|
49
|
+
}) => ReactNode;
|
|
50
|
+
}
|
|
51
|
+
declare function DashboardGrid<TWidget extends DashboardGridWidgetData = DashboardGridWidgetData>({ rows: initialRows, widgets, readOnly, saveDebounceMs, onUpdateLayout, onDeleteWidget, onDropExternalItem, onCreateRowExternalItem, renderRowEdgeAdd, renderWidgetMenu, renderWidgetConfigDialog, }: DashboardGridProps<TWidget>): react.JSX.Element;
|
|
52
|
+
|
|
53
|
+
export { DashboardGrid, type DashboardGridProps, type DashboardGridWidgetData, EXTERNAL_ITEM_TYPE };
|