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