@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.
package/dist/index.cjs ADDED
@@ -0,0 +1,732 @@
1
+ 'use client';
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ COLS: () => COLS,
25
+ DEFAULT_ROW_HEIGHT: () => DEFAULT_ROW_HEIGHT,
26
+ DashboardGrid: () => DashboardGrid,
27
+ MAX_ITEMS_PER_ROW: () => MAX_ITEMS_PER_ROW,
28
+ MAX_ROW_HEIGHT: () => MAX_ROW_HEIGHT,
29
+ MIN_COL_SPAN: () => MIN_COL_SPAN,
30
+ MIN_ROW_HEIGHT: () => MIN_ROW_HEIGHT,
31
+ ZONE_WIDTH_PX: () => ZONE_WIDTH_PX,
32
+ canPlaceInRow: () => canPlaceInRow,
33
+ moveRow: () => moveRow,
34
+ placeItemAt: () => placeItemAt,
35
+ placeItemInNewRow: () => placeItemInNewRow,
36
+ rebalanceRow: () => rebalanceRow,
37
+ removeItemFromRow: () => removeItemFromRow,
38
+ resizeItemPair: () => resizeItemPair,
39
+ resizeRow: () => resizeRow
40
+ });
41
+ module.exports = __toCommonJS(index_exports);
42
+
43
+ // src/dashboard-grid.tsx
44
+ var import_react = require("react");
45
+ var import_react_dnd = require("react-dnd");
46
+ var import_react_dnd_html5_backend = require("react-dnd-html5-backend");
47
+ var import_lucide_react = require("lucide-react");
48
+
49
+ // src/layout-engine.ts
50
+ var COLS = 12;
51
+ var ZONE_WIDTH_PX = 12;
52
+ var MIN_COL_SPAN = 1;
53
+ var MAX_ITEMS_PER_ROW = 4;
54
+ var DEFAULT_ROW_HEIGHT = 220;
55
+ var MIN_ROW_HEIGHT = 80;
56
+ var MAX_ROW_HEIGHT = 480;
57
+ function rebalanceRow(items, row) {
58
+ if (row.widgetIds.length === 0) return items;
59
+ const span = COLS / row.widgetIds.length;
60
+ const next = { ...items };
61
+ for (const id of row.widgetIds) {
62
+ const item = next[id];
63
+ if (item && item.colSpan !== span) next[id] = { ...item, colSpan: span };
64
+ }
65
+ return next;
66
+ }
67
+ function canPlaceInRow(row, isSameRowMove) {
68
+ if (isSameRowMove) return true;
69
+ return (row?.widgetIds.length ?? 0) < MAX_ITEMS_PER_ROW;
70
+ }
71
+ function placeItemAt(rows, items, itemId, fromRowId, targetRowId, index) {
72
+ const targetRowBefore = rows.find((row) => row.id === targetRowId);
73
+ const targetCountBefore = targetRowBefore?.widgetIds.length ?? 0;
74
+ const isSameRowMove = fromRowId === targetRowId;
75
+ if (!canPlaceInRow(targetRowBefore, isSameRowMove)) {
76
+ return { rows, items };
77
+ }
78
+ let nextRows = rows;
79
+ let nextItems = items;
80
+ let insertIndex = index;
81
+ let sourceCountBefore = 0;
82
+ if (fromRowId !== void 0) {
83
+ const fromRow = rows.find((row) => row.id === fromRowId);
84
+ sourceCountBefore = fromRow?.widgetIds.length ?? 0;
85
+ const fromIndex = fromRow?.widgetIds.indexOf(itemId) ?? -1;
86
+ if (fromRowId === targetRowId && fromIndex !== -1 && fromIndex < insertIndex) {
87
+ insertIndex -= 1;
88
+ }
89
+ nextRows = nextRows.map(
90
+ (row) => row.id === fromRowId ? { ...row, widgetIds: row.widgetIds.filter((id) => id !== itemId) } : row
91
+ );
92
+ nextRows = nextRows.filter((row) => row.widgetIds.length > 0 || row.id === targetRowId);
93
+ }
94
+ nextRows = nextRows.map((row) => {
95
+ if (row.id !== targetRowId) return row;
96
+ const next = [...row.widgetIds];
97
+ next.splice(Math.min(Math.max(insertIndex, 0), next.length), 0, itemId);
98
+ return { ...row, widgetIds: next };
99
+ });
100
+ const targetRowAfter = nextRows.find((row) => row.id === targetRowId);
101
+ if (targetRowAfter && targetRowAfter.widgetIds.length !== targetCountBefore) {
102
+ nextItems = rebalanceRow(nextItems, targetRowAfter);
103
+ }
104
+ if (fromRowId !== void 0 && fromRowId !== targetRowId) {
105
+ const sourceRowAfter = nextRows.find((row) => row.id === fromRowId);
106
+ if (sourceRowAfter && sourceRowAfter.widgetIds.length !== sourceCountBefore) {
107
+ nextItems = rebalanceRow(nextItems, sourceRowAfter);
108
+ }
109
+ }
110
+ return { rows: nextRows, items: nextItems };
111
+ }
112
+ function placeItemInNewRow(rows, items, itemId, fromRowId, newRowId, heightPx = DEFAULT_ROW_HEIGHT) {
113
+ let nextRows = rows;
114
+ let nextItems = items;
115
+ if (fromRowId !== void 0) {
116
+ const fromRow = rows.find((row) => row.id === fromRowId);
117
+ nextRows = nextRows.map(
118
+ (row) => row.id === fromRowId ? { ...row, widgetIds: row.widgetIds.filter((id) => id !== itemId) } : row
119
+ ).filter((row) => row.widgetIds.length > 0);
120
+ const sourceRowAfter = nextRows.find((row) => row.id === fromRowId);
121
+ if (sourceRowAfter && sourceRowAfter.widgetIds.length !== fromRow?.widgetIds.length) {
122
+ nextItems = rebalanceRow(nextItems, sourceRowAfter);
123
+ }
124
+ }
125
+ const newRow = { id: newRowId, widgetIds: [itemId], heightPx };
126
+ nextItems = rebalanceRow(nextItems, newRow);
127
+ return { rows: [...nextRows, newRow], items: nextItems };
128
+ }
129
+ function removeItemFromRow(rows, items, itemId, rowId) {
130
+ const nextRows = rows.map(
131
+ (row) => row.id === rowId ? { ...row, widgetIds: row.widgetIds.filter((id) => id !== itemId) } : row
132
+ ).filter((row) => row.widgetIds.length > 0);
133
+ const withoutItem = { ...items };
134
+ delete withoutItem[itemId];
135
+ const rowAfter = nextRows.find((row) => row.id === rowId);
136
+ const nextItems = rowAfter ? rebalanceRow(withoutItem, rowAfter) : withoutItem;
137
+ return { rows: nextRows, items: nextItems };
138
+ }
139
+ function resizeItemPair(items, leftId, rightId, nextLeftColSpan) {
140
+ const left = items[leftId];
141
+ const right = items[rightId];
142
+ if (!left || !right) return items;
143
+ const combined = left.colSpan + right.colSpan;
144
+ const clampedLeft = Math.min(combined - MIN_COL_SPAN, Math.max(MIN_COL_SPAN, nextLeftColSpan));
145
+ const nextRight = combined - clampedLeft;
146
+ return {
147
+ ...items,
148
+ [leftId]: { ...left, colSpan: clampedLeft },
149
+ [rightId]: { ...right, colSpan: nextRight }
150
+ };
151
+ }
152
+ function resizeRow(rows, rowId, heightPx) {
153
+ return rows.map((row) => row.id === rowId ? { ...row, heightPx } : row);
154
+ }
155
+ function moveRow(rows, from, to) {
156
+ const next = [...rows];
157
+ const [moved] = next.splice(from, 1);
158
+ next.splice(to, 0, moved);
159
+ return next;
160
+ }
161
+
162
+ // src/cn.ts
163
+ var import_clsx = require("clsx");
164
+ var import_tailwind_merge = require("tailwind-merge");
165
+ function cn(...inputs) {
166
+ return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs));
167
+ }
168
+
169
+ // src/dashboard-grid.tsx
170
+ var import_jsx_runtime = require("react/jsx-runtime");
171
+ var DEFAULT_SAVE_DEBOUNCE_MS = 600;
172
+ var ItemTypes = { WIDGET: "widget", ROW: "row" };
173
+ function DashboardGrid({
174
+ rows: initialRows,
175
+ widgets,
176
+ readOnly = false,
177
+ saveDebounceMs = DEFAULT_SAVE_DEBOUNCE_MS,
178
+ onUpdateLayout,
179
+ onDeleteWidget,
180
+ renderRowEdgeAdd,
181
+ renderWidgetMenu,
182
+ renderWidgetConfigDialog
183
+ }) {
184
+ const [{ rows, items }, setState] = (0, import_react.useState)(() => ({
185
+ rows: initialRows.map((row) => ({
186
+ id: row.id,
187
+ heightPx: row.heightPx,
188
+ widgetIds: row.widgetIds
189
+ })),
190
+ items: Object.fromEntries(
191
+ Object.values(widgets).map((w) => [w.id, { id: w.id, colSpan: w.colSpan }])
192
+ )
193
+ }));
194
+ const saveTimer = (0, import_react.useRef)(null);
195
+ const persist = (0, import_react.useCallback)(
196
+ (nextRows, nextItems) => {
197
+ if (readOnly) return;
198
+ if (saveTimer.current) clearTimeout(saveTimer.current);
199
+ saveTimer.current = setTimeout(() => {
200
+ onUpdateLayout?.(
201
+ nextRows.map((row) => ({ id: row.id, heightPx: row.heightPx, widgetIds: row.widgetIds })),
202
+ Object.values(nextItems).map((item) => ({ id: item.id, colSpan: item.colSpan }))
203
+ );
204
+ }, saveDebounceMs);
205
+ },
206
+ [readOnly, saveDebounceMs, onUpdateLayout]
207
+ );
208
+ const placeWidgetAt = (0, import_react.useCallback)(
209
+ (opts, targetRowId, index) => {
210
+ setState((prev) => {
211
+ const next = placeItemAt(
212
+ prev.rows,
213
+ prev.items,
214
+ opts.widgetId,
215
+ opts.fromRowId,
216
+ targetRowId,
217
+ index
218
+ );
219
+ persist(next.rows, next.items);
220
+ return next;
221
+ });
222
+ },
223
+ [persist]
224
+ );
225
+ const createRowWith = (0, import_react.useCallback)(
226
+ (opts) => {
227
+ setState((prev) => {
228
+ const next = placeItemInNewRow(
229
+ prev.rows,
230
+ prev.items,
231
+ opts.widgetId,
232
+ opts.fromRowId,
233
+ `row-${crypto.randomUUID()}`
234
+ );
235
+ persist(next.rows, next.items);
236
+ return next;
237
+ });
238
+ },
239
+ [persist]
240
+ );
241
+ const removeWidget = (0, import_react.useCallback)(
242
+ (widgetId, rowId) => {
243
+ setState((prev) => {
244
+ const next = removeItemFromRow(prev.rows, prev.items, widgetId, rowId);
245
+ persist(next.rows, next.items);
246
+ return next;
247
+ });
248
+ onDeleteWidget?.(widgetId);
249
+ },
250
+ [persist, onDeleteWidget]
251
+ );
252
+ const resizeWidgetPair = (0, import_react.useCallback)(
253
+ (leftId, rightId, nextLeftColSpan) => {
254
+ setState((prev) => {
255
+ const nextItems = resizeItemPair(prev.items, leftId, rightId, nextLeftColSpan);
256
+ persist(prev.rows, nextItems);
257
+ return { rows: prev.rows, items: nextItems };
258
+ });
259
+ },
260
+ [persist]
261
+ );
262
+ const resizeRowHeight = (0, import_react.useCallback)(
263
+ (rowId, heightPx) => {
264
+ setState((prev) => {
265
+ const nextRows = resizeRow(prev.rows, rowId, heightPx);
266
+ persist(nextRows, prev.items);
267
+ return { rows: nextRows, items: prev.items };
268
+ });
269
+ },
270
+ [persist]
271
+ );
272
+ const moveRowTo = (0, import_react.useCallback)(
273
+ (from, to) => {
274
+ setState((prev) => {
275
+ const nextRows = moveRow(prev.rows, from, to);
276
+ persist(nextRows, prev.items);
277
+ return { rows: nextRows, items: prev.items };
278
+ });
279
+ },
280
+ [persist]
281
+ );
282
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_dnd.DndProvider, { backend: import_react_dnd_html5_backend.HTML5Backend, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
283
+ Board,
284
+ {
285
+ rows,
286
+ items,
287
+ widgets,
288
+ readOnly,
289
+ onDropExisting: placeWidgetAt,
290
+ onCreateRowExisting: createRowWith,
291
+ onRemoveWidget: removeWidget,
292
+ onResizeWidget: resizeWidgetPair,
293
+ onResizeRow: resizeRowHeight,
294
+ onMoveRow: moveRowTo,
295
+ renderRowEdgeAdd,
296
+ renderWidgetMenu,
297
+ renderWidgetConfigDialog
298
+ }
299
+ ) });
300
+ }
301
+ function Board({
302
+ rows,
303
+ items,
304
+ widgets,
305
+ readOnly,
306
+ onDropExisting,
307
+ onCreateRowExisting,
308
+ onRemoveWidget,
309
+ onResizeWidget,
310
+ onResizeRow,
311
+ onMoveRow,
312
+ renderRowEdgeAdd,
313
+ renderWidgetMenu,
314
+ renderWidgetConfigDialog
315
+ }) {
316
+ const dragActive = (0, import_react_dnd.useDragLayer)((monitor) => monitor.isDragging());
317
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex w-full flex-col gap-4", children: [
318
+ rows.map((row, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
319
+ SortableRow,
320
+ {
321
+ row,
322
+ index,
323
+ items,
324
+ widgets,
325
+ readOnly,
326
+ dragActive,
327
+ onDropExisting,
328
+ onRemoveWidget,
329
+ onResizeWidget,
330
+ onResizeRow,
331
+ onMoveRow,
332
+ renderRowEdgeAdd,
333
+ renderWidgetMenu,
334
+ renderWidgetConfigDialog
335
+ },
336
+ row.id
337
+ )),
338
+ !readOnly ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NewRowDropZone, { onDropExisting: onCreateRowExisting }) : null
339
+ ] });
340
+ }
341
+ function NewRowDropZone({
342
+ onDropExisting
343
+ }) {
344
+ const [{ isOver }, dropRef] = (0, import_react_dnd.useDrop)(() => ({
345
+ accept: ItemTypes.WIDGET,
346
+ drop: (item) => onDropExisting(item),
347
+ collect: (monitor) => ({ isOver: monitor.isOver() })
348
+ }));
349
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
350
+ "div",
351
+ {
352
+ ref: (node) => {
353
+ dropRef(node);
354
+ },
355
+ className: cn(
356
+ "flex h-12 items-center justify-center rounded-lg border-2 border-dashed border-border text-xs text-muted-foreground transition-colors",
357
+ isOver && "border-primary bg-primary/5 text-primary"
358
+ ),
359
+ children: "Drop here to start a new row"
360
+ }
361
+ );
362
+ }
363
+ function SortableRow({
364
+ row,
365
+ index,
366
+ items,
367
+ widgets,
368
+ readOnly,
369
+ dragActive,
370
+ onDropExisting,
371
+ onRemoveWidget,
372
+ onResizeWidget,
373
+ onResizeRow,
374
+ onMoveRow,
375
+ renderRowEdgeAdd,
376
+ renderWidgetMenu,
377
+ renderWidgetConfigDialog
378
+ }) {
379
+ const [{ isDragging }, dragRef, previewRef] = (0, import_react_dnd.useDrag)(
380
+ () => ({
381
+ type: ItemTypes.ROW,
382
+ item: { rowId: row.id, index },
383
+ canDrag: !readOnly,
384
+ collect: (monitor) => ({ isDragging: monitor.isDragging() })
385
+ }),
386
+ [row.id, index, readOnly]
387
+ );
388
+ const [, dropRef] = (0, import_react_dnd.useDrop)(
389
+ () => ({
390
+ accept: ItemTypes.ROW,
391
+ canDrop: () => !readOnly,
392
+ hover: (item) => {
393
+ if (readOnly || item.index === index) return;
394
+ onMoveRow(item.index, index);
395
+ item.index = index;
396
+ }
397
+ }),
398
+ [index, readOnly, onMoveRow]
399
+ );
400
+ const setRowRef = (0, import_react.useCallback)(
401
+ (node) => {
402
+ previewRef(node);
403
+ dropRef(node);
404
+ },
405
+ [previewRef, dropRef]
406
+ );
407
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
408
+ "div",
409
+ {
410
+ ref: setRowRef,
411
+ style: { height: row.heightPx },
412
+ className: cn(
413
+ "group/row relative flex items-stretch gap-2 rounded-lg bg-muted/30 p-2 transition-opacity",
414
+ isDragging && "opacity-40"
415
+ ),
416
+ children: [
417
+ !readOnly ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
418
+ "div",
419
+ {
420
+ ref: (node) => {
421
+ dragRef(node);
422
+ },
423
+ className: "flex w-6 shrink-0 cursor-grab items-center justify-center text-muted-foreground active:cursor-grabbing",
424
+ title: "Drag to reorder row",
425
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.GripVerticalIcon, { className: "size-4" })
426
+ }
427
+ ) : null,
428
+ !readOnly && row.widgetIds.length < MAX_ITEMS_PER_ROW ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RowEdgeAddWidget, { rowId: row.id, edge: "start", render: renderRowEdgeAdd }) : null,
429
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
430
+ RowContent,
431
+ {
432
+ row,
433
+ items,
434
+ widgets,
435
+ readOnly,
436
+ dragActive,
437
+ onDropExisting,
438
+ onRemoveWidget,
439
+ onResizeWidget,
440
+ renderWidgetMenu,
441
+ renderWidgetConfigDialog
442
+ }
443
+ ),
444
+ !readOnly && row.widgetIds.length < MAX_ITEMS_PER_ROW ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RowEdgeAddWidget, { rowId: row.id, edge: "end", render: renderRowEdgeAdd }) : null,
445
+ !readOnly ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RowResizeHandle, { rowId: row.id, heightPx: row.heightPx, onResize: onResizeRow }) : null
446
+ ]
447
+ }
448
+ );
449
+ }
450
+ function RowEdgeAddWidget({
451
+ rowId,
452
+ edge,
453
+ render
454
+ }) {
455
+ if (!render) return null;
456
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: render({ rowId, edge }) });
457
+ }
458
+ function RowContent({
459
+ row,
460
+ items,
461
+ widgets,
462
+ readOnly,
463
+ dragActive,
464
+ onDropExisting,
465
+ onRemoveWidget,
466
+ onResizeWidget,
467
+ renderWidgetMenu,
468
+ renderWidgetConfigDialog
469
+ }) {
470
+ const containerRef = (0, import_react.useRef)(null);
471
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { ref: containerRef, className: "flex h-full min-w-0 flex-1 items-stretch", children: [
472
+ !readOnly ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
473
+ InsertionZone,
474
+ {
475
+ rowId: row.id,
476
+ index: 0,
477
+ widgetCount: row.widgetIds.length,
478
+ dragActive,
479
+ onDropExisting
480
+ }
481
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ZoneSpacer, {}),
482
+ row.widgetIds.map((widgetId, i) => {
483
+ const item = items[widgetId];
484
+ const widget = widgets[widgetId];
485
+ if (!item || !widget) return null;
486
+ return (
487
+ // A Fragment, not a wrapping div — PlacedWidget must be a direct
488
+ // flex child of the row container above, since its percentage
489
+ // width resolves against its immediate parent. A wrapper div here
490
+ // (with no definite width of its own) would make that percentage
491
+ // resolve against an indefinite size and collapse to content
492
+ // width instead.
493
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react.Fragment, { children: [
494
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
495
+ PlacedWidget,
496
+ {
497
+ item,
498
+ widget,
499
+ rowId: row.id,
500
+ widgetCount: row.widgetIds.length,
501
+ readOnly,
502
+ onRemoveWidget,
503
+ renderWidgetMenu,
504
+ renderWidgetConfigDialog
505
+ }
506
+ ),
507
+ !readOnly ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
508
+ InsertionZone,
509
+ {
510
+ rowId: row.id,
511
+ index: i + 1,
512
+ widgetCount: row.widgetIds.length,
513
+ dragActive,
514
+ leftWidgetId: widgetId,
515
+ rightWidgetId: row.widgetIds[i + 1],
516
+ leftColSpan: item.colSpan,
517
+ containerRef,
518
+ onDropExisting,
519
+ onResizeWidget
520
+ }
521
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ZoneSpacer, {})
522
+ ] }, widgetId)
523
+ );
524
+ })
525
+ ] });
526
+ }
527
+ function ZoneSpacer() {
528
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { width: ZONE_WIDTH_PX }, className: "shrink-0" });
529
+ }
530
+ function InsertionZone({
531
+ rowId,
532
+ index,
533
+ widgetCount,
534
+ dragActive,
535
+ leftWidgetId,
536
+ rightWidgetId,
537
+ leftColSpan,
538
+ containerRef,
539
+ onDropExisting,
540
+ onResizeWidget
541
+ }) {
542
+ const [{ isOver, canDrop }, dropRef] = (0, import_react_dnd.useDrop)(
543
+ () => ({
544
+ accept: ItemTypes.WIDGET,
545
+ // A same-row reorder never grows the row, so it's always allowed —
546
+ // only a drop that would add a net-new member to an already-full row
547
+ // is rejected (mirrors the authoritative check in placeItemAt).
548
+ canDrop: (item) => {
549
+ if (widgetCount < MAX_ITEMS_PER_ROW) return true;
550
+ return item.fromRowId === rowId;
551
+ },
552
+ drop: (item) => onDropExisting(item, rowId, index),
553
+ collect: (monitor) => ({ isOver: monitor.isOver(), canDrop: monitor.canDrop() })
554
+ }),
555
+ [rowId, index, widgetCount, onDropExisting]
556
+ );
557
+ const isResizable = leftWidgetId !== void 0 && rightWidgetId !== void 0;
558
+ const handleMouseDown = (0, import_react.useCallback)(
559
+ (event) => {
560
+ if (leftWidgetId === void 0 || rightWidgetId === void 0 || leftColSpan === void 0 || !containerRef?.current || !onResizeWidget) {
561
+ return;
562
+ }
563
+ event.preventDefault();
564
+ event.stopPropagation();
565
+ const containerWidth = containerRef.current.getBoundingClientRect().width;
566
+ if (!containerWidth) return;
567
+ const zonesWidthPx = (widgetCount + 1) * ZONE_WIDTH_PX;
568
+ const colWidthPx = (containerWidth - zonesWidthPx) / COLS;
569
+ const startX = event.clientX;
570
+ const startLeftColSpan = leftColSpan;
571
+ const resolvedOnResize = onResizeWidget;
572
+ const resolvedLeftId = leftWidgetId;
573
+ const resolvedRightId = rightWidgetId;
574
+ function handleMove(moveEvent) {
575
+ const deltaCols = Math.round((moveEvent.clientX - startX) / colWidthPx);
576
+ resolvedOnResize(resolvedLeftId, resolvedRightId, startLeftColSpan + deltaCols);
577
+ }
578
+ function handleUp() {
579
+ window.removeEventListener("mousemove", handleMove);
580
+ window.removeEventListener("mouseup", handleUp);
581
+ }
582
+ window.addEventListener("mousemove", handleMove);
583
+ window.addEventListener("mouseup", handleUp);
584
+ },
585
+ [leftWidgetId, rightWidgetId, leftColSpan, containerRef, widgetCount, onResizeWidget]
586
+ );
587
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
588
+ "div",
589
+ {
590
+ ref: (node) => {
591
+ dropRef(node);
592
+ },
593
+ onMouseDown: handleMouseDown,
594
+ style: { width: ZONE_WIDTH_PX },
595
+ className: cn(
596
+ // Fixed footprint at all times — the "grows on hover" affordance
597
+ // comes from a scale transform on the inner pill, not a width
598
+ // change, so it can't perturb every other widget's calc()'d width
599
+ // mid-drag.
600
+ "flex h-full shrink-0 items-center justify-center",
601
+ isResizable && "cursor-ew-resize"
602
+ ),
603
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
604
+ "div",
605
+ {
606
+ className: cn(
607
+ "h-full w-full rounded-full transition-all",
608
+ isOver && canDrop ? "scale-x-150 bg-primary/25 ring-2 ring-primary" : dragActive && canDrop ? "ring-dashed bg-muted-foreground/20 ring-1 ring-border" : isResizable ? "bg-transparent hover:bg-muted-foreground/20 hover:ring-1 hover:ring-border" : "bg-transparent"
609
+ )
610
+ }
611
+ )
612
+ }
613
+ );
614
+ }
615
+ function PlacedWidget({
616
+ item,
617
+ widget,
618
+ rowId,
619
+ widgetCount,
620
+ readOnly,
621
+ onRemoveWidget,
622
+ renderWidgetMenu,
623
+ renderWidgetConfigDialog
624
+ }) {
625
+ const zonesWidthPx = (widgetCount + 1) * ZONE_WIDTH_PX;
626
+ const widthCss = `calc((100% - ${zonesWidthPx}px) * ${item.colSpan} / ${COLS})`;
627
+ const [{ isDragging }, dragRef, previewRef] = (0, import_react_dnd.useDrag)(
628
+ () => ({
629
+ type: ItemTypes.WIDGET,
630
+ item: { widgetId: item.id, fromRowId: rowId },
631
+ canDrag: !readOnly,
632
+ collect: (monitor) => ({ isDragging: monitor.isDragging() })
633
+ }),
634
+ [item.id, rowId, readOnly]
635
+ );
636
+ const [configureOpen, setConfigureOpen] = (0, import_react.useState)(false);
637
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
638
+ "div",
639
+ {
640
+ ref: (node) => {
641
+ previewRef(node);
642
+ },
643
+ style: { width: widthCss },
644
+ className: cn(
645
+ "group relative h-full min-w-0 shrink-0 overflow-hidden transition-opacity",
646
+ isDragging && "opacity-40"
647
+ ),
648
+ children: [
649
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-full", children: widget.content }),
650
+ !readOnly ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
651
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
652
+ "span",
653
+ {
654
+ ref: (node) => {
655
+ dragRef(node);
656
+ },
657
+ 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",
658
+ title: "Drag to move",
659
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.GripVerticalIcon, { className: "size-3.5" })
660
+ }
661
+ ),
662
+ renderWidgetMenu?.(widget, {
663
+ onConfigure: () => setConfigureOpen(true),
664
+ onDelete: () => onRemoveWidget(widget.id, rowId)
665
+ }),
666
+ renderWidgetConfigDialog?.({
667
+ widget,
668
+ open: configureOpen,
669
+ onOpenChange: setConfigureOpen
670
+ })
671
+ ] }) : null
672
+ ]
673
+ }
674
+ );
675
+ }
676
+ function RowResizeHandle({
677
+ rowId,
678
+ heightPx,
679
+ onResize
680
+ }) {
681
+ const handleMouseDown = (0, import_react.useCallback)(
682
+ (event) => {
683
+ event.preventDefault();
684
+ event.stopPropagation();
685
+ const startY = event.clientY;
686
+ const startHeight = heightPx;
687
+ function handleMove(moveEvent) {
688
+ const nextHeight = Math.min(
689
+ MAX_ROW_HEIGHT,
690
+ Math.max(MIN_ROW_HEIGHT, startHeight + (moveEvent.clientY - startY))
691
+ );
692
+ onResize(rowId, nextHeight);
693
+ }
694
+ function handleUp() {
695
+ window.removeEventListener("mousemove", handleMove);
696
+ window.removeEventListener("mouseup", handleUp);
697
+ }
698
+ window.addEventListener("mousemove", handleMove);
699
+ window.addEventListener("mouseup", handleUp);
700
+ },
701
+ [rowId, heightPx, onResize]
702
+ );
703
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
704
+ "div",
705
+ {
706
+ onMouseDown: handleMouseDown,
707
+ className: "absolute inset-x-6 -bottom-2 flex h-3 cursor-ns-resize items-center justify-center",
708
+ title: "Drag to resize row height",
709
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-1 w-10 rounded-full bg-border" })
710
+ }
711
+ );
712
+ }
713
+ // Annotate the CommonJS export names for ESM import in node:
714
+ 0 && (module.exports = {
715
+ COLS,
716
+ DEFAULT_ROW_HEIGHT,
717
+ DashboardGrid,
718
+ MAX_ITEMS_PER_ROW,
719
+ MAX_ROW_HEIGHT,
720
+ MIN_COL_SPAN,
721
+ MIN_ROW_HEIGHT,
722
+ ZONE_WIDTH_PX,
723
+ canPlaceInRow,
724
+ moveRow,
725
+ placeItemAt,
726
+ placeItemInNewRow,
727
+ rebalanceRow,
728
+ removeItemFromRow,
729
+ resizeItemPair,
730
+ resizeRow
731
+ });
732
+ //# sourceMappingURL=index.cjs.map