gridsmith-ui 0.31.1 → 0.33.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/CHANGELOG.md +78 -1
- package/README.md +4 -2
- package/dist/charts.d.ts +111 -31
- package/dist/charts.js +288 -148
- package/dist/charts.js.map +1 -1
- package/dist/chat.js +6 -6
- package/dist/{chunk-EZ3ZGFGA.js → chunk-2724SJO2.js} +12 -4
- package/dist/chunk-2724SJO2.js.map +1 -0
- package/dist/{chunk-I6JJ7KKP.js → chunk-6NRLTG5W.js} +16 -11
- package/dist/chunk-6NRLTG5W.js.map +1 -0
- package/dist/{chunk-BHTAVJIG.js → chunk-6XCWKLJZ.js} +41 -6
- package/dist/chunk-6XCWKLJZ.js.map +1 -0
- package/dist/{chunk-TH272DIC.js → chunk-AB4UQ4G5.js} +125 -235
- package/dist/chunk-AB4UQ4G5.js.map +1 -0
- package/dist/{chunk-IOHGE4EW.js → chunk-AVSP6NDX.js} +8 -8
- package/dist/chunk-AVSP6NDX.js.map +1 -0
- package/dist/chunk-F2XMZOUP.js +81 -0
- package/dist/chunk-F2XMZOUP.js.map +1 -0
- package/dist/{chunk-HQFTP7Y5.js → chunk-FF57N3P7.js} +10 -8
- package/dist/chunk-FF57N3P7.js.map +1 -0
- package/dist/chunk-GIA4BMGI.js +18 -0
- package/dist/chunk-GIA4BMGI.js.map +1 -0
- package/dist/{chunk-DR6WEI3A.js → chunk-HCEHTJ5E.js} +3 -3
- package/dist/{chunk-DR6WEI3A.js.map → chunk-HCEHTJ5E.js.map} +1 -1
- package/dist/{chunk-IZW5XHKA.js → chunk-OWGF362U.js} +10 -8
- package/dist/chunk-OWGF362U.js.map +1 -0
- package/dist/chunk-P3NUUIL6.js +9 -0
- package/dist/chunk-P3NUUIL6.js.map +1 -0
- package/dist/{chunk-W3GY5ROH.js → chunk-R2IAK3J3.js} +3 -3
- package/dist/{chunk-W3GY5ROH.js.map → chunk-R2IAK3J3.js.map} +1 -1
- package/dist/chunk-R7FG4ZQ5.js +16 -0
- package/dist/chunk-R7FG4ZQ5.js.map +1 -0
- package/dist/{chunk-V73LHNRL.js → chunk-RS624TD5.js} +29 -10
- package/dist/chunk-RS624TD5.js.map +1 -0
- package/dist/{chunk-DBHVJ7Z3.js → chunk-SS3C24IJ.js} +3 -2
- package/dist/chunk-SS3C24IJ.js.map +1 -0
- package/dist/{chunk-K3NAFQ4S.js → chunk-TMPPBXX3.js} +3 -3
- package/dist/{chunk-K3NAFQ4S.js.map → chunk-TMPPBXX3.js.map} +1 -1
- package/dist/{chunk-NDL2JGQ7.js → chunk-UBLBVCQN.js} +5 -5
- package/dist/{chunk-NDL2JGQ7.js.map → chunk-UBLBVCQN.js.map} +1 -1
- package/dist/{chunk-2TCCSKL3.js → chunk-WD6T57ZJ.js} +5 -5
- package/dist/{chunk-2TCCSKL3.js.map → chunk-WD6T57ZJ.js.map} +1 -1
- package/dist/{chunk-SLGGZ3MU.js → chunk-WSKPESAX.js} +12 -10
- package/dist/chunk-WSKPESAX.js.map +1 -0
- package/dist/chunk-ZQV26NND.js +213 -0
- package/dist/chunk-ZQV26NND.js.map +1 -0
- package/dist/dashboard.d.ts +207 -0
- package/dist/dashboard.js +1210 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/data-grid.js +11 -10
- package/dist/data-grid.js.map +1 -1
- package/dist/editor-mentions.js +2 -2
- package/dist/editor.js +3 -2
- package/dist/editor.js.map +1 -1
- package/dist/index.d.ts +94 -12
- package/dist/index.js +601 -316
- package/dist/index.js.map +1 -1
- package/dist/kanban.js +9 -9
- package/dist/resizable.d.ts +11 -2
- package/dist/resizable.js +97 -22
- package/dist/resizable.js.map +1 -1
- package/dist/roots.css +2 -2
- package/dist/tokens.css +6 -6
- package/dist/utils.d.ts +51 -1
- package/dist/utils.js +55 -4
- package/dist/utils.js.map +1 -1
- package/dist/virtualized.js +9 -8
- package/dist/virtualized.js.map +1 -1
- package/dist-static/tokens.css +6 -6
- package/package.json +6 -1
- package/dist/chunk-BHTAVJIG.js.map +0 -1
- package/dist/chunk-DBHVJ7Z3.js.map +0 -1
- package/dist/chunk-EZ3ZGFGA.js.map +0 -1
- package/dist/chunk-HQFTP7Y5.js.map +0 -1
- package/dist/chunk-I6JJ7KKP.js.map +0 -1
- package/dist/chunk-IOHGE4EW.js.map +0 -1
- package/dist/chunk-IZW5XHKA.js.map +0 -1
- package/dist/chunk-KSJTMSYA.js +0 -71
- package/dist/chunk-KSJTMSYA.js.map +0 -1
- package/dist/chunk-LJYSVZMI.js +0 -25
- package/dist/chunk-LJYSVZMI.js.map +0 -1
- package/dist/chunk-SLGGZ3MU.js.map +0 -1
- package/dist/chunk-TH272DIC.js.map +0 -1
- package/dist/chunk-V73LHNRL.js.map +0 -1
|
@@ -0,0 +1,1210 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCompact, OverlayCloseCtx } from './chunk-ZQV26NND.js';
|
|
3
|
+
import { DashboardCellContext } from './chunk-P3NUUIL6.js';
|
|
4
|
+
import { useHydrated } from './chunk-GIA4BMGI.js';
|
|
5
|
+
import './chunk-72SDAX54.js';
|
|
6
|
+
import { DropdownMenu } from './chunk-WD6T57ZJ.js';
|
|
7
|
+
import { FillContext } from './chunk-SS3C24IJ.js';
|
|
8
|
+
import { Spinner, elementType } from './chunk-FF57N3P7.js';
|
|
9
|
+
import './chunk-ZHUVLBCC.js';
|
|
10
|
+
import './chunk-7RKC5C7G.js';
|
|
11
|
+
import { keyboardScroll } from './chunk-F2XMZOUP.js';
|
|
12
|
+
import { AutoTooltip, useRevealFocus } from './chunk-HCEHTJ5E.js';
|
|
13
|
+
import { warnOnce, Icon } from './chunk-53F7GVBQ.js';
|
|
14
|
+
import { cn, FOCUS_RING, GLASS_DROPDOWN, useLatest, focusAfterSelfRemoval, hasTabStop } from './chunk-2724SJO2.js';
|
|
15
|
+
import { createContext, useContext, useId, useRef, useState, useLayoutEffect, useCallback, useMemo, useEffect, Children, isValidElement, Fragment as Fragment$1 } from 'react';
|
|
16
|
+
import { createPortal } from 'react-dom';
|
|
17
|
+
import { useDraggable, useSensors, useSensor, MouseSensor, TouchSensor, DndContext, DragOverlay } from '@dnd-kit/core';
|
|
18
|
+
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
19
|
+
|
|
20
|
+
var SURFACE = 'svg[tabindex="0"]';
|
|
21
|
+
var holdsStop = (el) => hasTabStop(el) || !!el.querySelector(SURFACE);
|
|
22
|
+
function useCellFrameScroll(content, label) {
|
|
23
|
+
const part = useRef(null);
|
|
24
|
+
useRevealFocus(part);
|
|
25
|
+
const labelRef = useLatest(label);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const wrapper = content.current;
|
|
28
|
+
if (!wrapper || typeof MutationObserver === "undefined") return;
|
|
29
|
+
let held = null;
|
|
30
|
+
let queued = 0;
|
|
31
|
+
const read = () => {
|
|
32
|
+
queued = 0;
|
|
33
|
+
const frame = wrapper.firstElementChild;
|
|
34
|
+
const button = frame?.getAttribute("role") === "button";
|
|
35
|
+
let stop = null;
|
|
36
|
+
let name = () => "";
|
|
37
|
+
if (frame instanceof HTMLElement && frame.hasAttribute("data-stats-card")) {
|
|
38
|
+
part.current = frame;
|
|
39
|
+
if (!button) {
|
|
40
|
+
stop = frame;
|
|
41
|
+
name = () => frame.querySelector("span")?.textContent?.trim() || labelRef.current;
|
|
42
|
+
}
|
|
43
|
+
} else if (frame?.matches("[data-card]:not([data-gs])")) {
|
|
44
|
+
const body = frame.querySelector(":scope > [data-card-content]");
|
|
45
|
+
part.current = body;
|
|
46
|
+
if (button) warnOnce("DashboardGrid", `the widget "${labelRef.current}" is a clickable Card with no CardTitle, so it stays a button and its content can't scroll by keyboard: give it a CardHeader with a CardTitle (every widget needs a title), which then carries the card's action.`);
|
|
47
|
+
else if (body) {
|
|
48
|
+
stop = body;
|
|
49
|
+
name = () => frame.querySelector(":scope > [data-card-header] [data-card-title]")?.textContent?.trim() || "Card content";
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
const table = frame?.matches('[data-gs="table"]') ? frame : frame?.matches("[data-gs-paged]") ? frame.querySelector(':scope > [data-gs="table"]') : null;
|
|
53
|
+
part.current = table?.querySelector(":scope > [data-table-scroll]") ?? null;
|
|
54
|
+
}
|
|
55
|
+
if (held && held.el !== stop) {
|
|
56
|
+
held.scroll?.end();
|
|
57
|
+
held = null;
|
|
58
|
+
}
|
|
59
|
+
if (held) held.scroll?.update();
|
|
60
|
+
else if (stop) held = { el: stop, scroll: keyboardScroll(stop, name, "y", holdsStop) };
|
|
61
|
+
};
|
|
62
|
+
read();
|
|
63
|
+
const observer = new MutationObserver(() => {
|
|
64
|
+
if (!queued) queued = requestAnimationFrame(read);
|
|
65
|
+
});
|
|
66
|
+
observer.observe(wrapper, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ["role"] });
|
|
67
|
+
return () => {
|
|
68
|
+
observer.disconnect();
|
|
69
|
+
cancelAnimationFrame(queued);
|
|
70
|
+
held?.scroll?.end();
|
|
71
|
+
part.current = null;
|
|
72
|
+
};
|
|
73
|
+
}, [content, labelRef]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ../../showcase/src/lib/dashboard-layout.ts
|
|
77
|
+
var DASHBOARD_COLUMNS = 12;
|
|
78
|
+
var MIN_W = 3;
|
|
79
|
+
var DEFAULT_MIN_H = 2;
|
|
80
|
+
var DASHBOARD_MAX_ROWS = 100;
|
|
81
|
+
var clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
82
|
+
var whole = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? Math.round(value) : fallback;
|
|
83
|
+
function sizeLimits(item) {
|
|
84
|
+
const minW = clamp(whole(item.minW, MIN_W), MIN_W, DASHBOARD_COLUMNS);
|
|
85
|
+
const maxW = clamp(whole(item.maxW, DASHBOARD_COLUMNS), minW, DASHBOARD_COLUMNS);
|
|
86
|
+
const minH = clamp(whole(item.minH, DEFAULT_MIN_H), 1, DASHBOARD_MAX_ROWS);
|
|
87
|
+
const maxH = clamp(whole(item.maxH, DASHBOARD_MAX_ROWS), minH, DASHBOARD_MAX_ROWS);
|
|
88
|
+
return { minW, maxW, minH, maxH };
|
|
89
|
+
}
|
|
90
|
+
function normaliseItem(item) {
|
|
91
|
+
const limits = sizeLimits(item);
|
|
92
|
+
const next = { ...item, w: clamp(whole(item.w, limits.minW), limits.minW, limits.maxW), h: clamp(whole(item.h, limits.minH), limits.minH, limits.maxH) };
|
|
93
|
+
for (const key of ["minW", "maxW", "minH", "maxH"]) if (item[key] !== void 0) next[key] = limits[key];
|
|
94
|
+
return next;
|
|
95
|
+
}
|
|
96
|
+
function normaliseLayout(layout) {
|
|
97
|
+
const seen = /* @__PURE__ */ new Set();
|
|
98
|
+
const out = [];
|
|
99
|
+
for (const item of Array.isArray(layout) ? layout : []) {
|
|
100
|
+
if (!item || typeof item.id !== "string" || item.id === "" || seen.has(item.id)) continue;
|
|
101
|
+
seen.add(item.id);
|
|
102
|
+
out.push(normaliseItem(item));
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
function moveItem(layout, id, toIndex) {
|
|
107
|
+
const items = normaliseLayout(layout);
|
|
108
|
+
const from = items.findIndex((item2) => item2.id === id);
|
|
109
|
+
if (from < 0) return items;
|
|
110
|
+
const [item] = items.splice(from, 1);
|
|
111
|
+
items.splice(clamp(whole(toIndex, from), 0, items.length), 0, item);
|
|
112
|
+
return items;
|
|
113
|
+
}
|
|
114
|
+
function resizeItem(layout, id, size) {
|
|
115
|
+
return normaliseLayout(layout).map((item) => item.id === id ? normaliseItem({ ...item, ...size.w !== void 0 ? { w: size.w } : {}, ...size.h !== void 0 ? { h: size.h } : {} }) : item);
|
|
116
|
+
}
|
|
117
|
+
function addItem(layout, item, index) {
|
|
118
|
+
const items = normaliseLayout(layout);
|
|
119
|
+
if (!item || typeof item.id !== "string" || item.id === "" || items.some((it) => it.id === item.id)) return items;
|
|
120
|
+
items.splice(clamp(whole(index, items.length), 0, items.length), 0, normaliseItem(item));
|
|
121
|
+
return items;
|
|
122
|
+
}
|
|
123
|
+
function placeLayout(items, columns = DASHBOARD_COLUMNS) {
|
|
124
|
+
const taken = [];
|
|
125
|
+
const free = (row2, col2, w, h) => {
|
|
126
|
+
for (let r = row2; r < row2 + h; r++) for (let c = col2; c < col2 + w; c++) if (taken[r]?.[c]) return false;
|
|
127
|
+
return true;
|
|
128
|
+
};
|
|
129
|
+
const out = [];
|
|
130
|
+
let row = 0;
|
|
131
|
+
let col = 0;
|
|
132
|
+
for (const item of items) {
|
|
133
|
+
const w = clamp(item.w, 1, columns);
|
|
134
|
+
const h = Math.max(1, item.h);
|
|
135
|
+
for (; ; ) {
|
|
136
|
+
if (col + w > columns) {
|
|
137
|
+
row++;
|
|
138
|
+
col = 0;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (free(row, col, w, h)) break;
|
|
142
|
+
col++;
|
|
143
|
+
}
|
|
144
|
+
for (let r = row; r < row + h; r++) {
|
|
145
|
+
taken[r] ??= [];
|
|
146
|
+
for (let c = col; c < col + w; c++) taken[r][c] = true;
|
|
147
|
+
}
|
|
148
|
+
out.push({ id: item.id, col, row, w, h });
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
function landingAt(others, item, index, columns) {
|
|
153
|
+
return placeLayout([...others.slice(0, index), item, ...others.slice(index)], columns)[index];
|
|
154
|
+
}
|
|
155
|
+
function stepTarget(layout, id, step, columns = DASHBOARD_COLUMNS) {
|
|
156
|
+
const at = layout.findIndex((item2) => item2.id === id);
|
|
157
|
+
const last = layout.length - 1;
|
|
158
|
+
if (at < 0) return { blocked: "first" };
|
|
159
|
+
switch (step) {
|
|
160
|
+
case "earlier":
|
|
161
|
+
return at > 0 ? { index: at - 1 } : { blocked: "first" };
|
|
162
|
+
case "later":
|
|
163
|
+
return at < last ? { index: at + 1 } : { blocked: "last" };
|
|
164
|
+
case "first":
|
|
165
|
+
return at > 0 ? { index: 0 } : { blocked: "first" };
|
|
166
|
+
case "last":
|
|
167
|
+
return at < last ? { index: last } : { blocked: "last" };
|
|
168
|
+
}
|
|
169
|
+
const item = layout[at];
|
|
170
|
+
const others = layout.filter((_, i) => i !== at);
|
|
171
|
+
const here = placeLayout(layout, columns)[at];
|
|
172
|
+
let best = null;
|
|
173
|
+
for (let index = 0; index <= others.length; index++) {
|
|
174
|
+
if (index === at) continue;
|
|
175
|
+
const p = landingAt(others, item, index, columns);
|
|
176
|
+
if (step === "down" ? p.row <= here.row : p.row >= here.row) continue;
|
|
177
|
+
const rows = Math.abs(p.row - here.row);
|
|
178
|
+
const cols = Math.abs(p.col - here.col);
|
|
179
|
+
const steps = Math.abs(index - at);
|
|
180
|
+
if (!best || rows < best.rows || rows === best.rows && (cols < best.cols || cols === best.cols && steps < best.steps)) best = { index, rows, cols, steps };
|
|
181
|
+
}
|
|
182
|
+
return best ? { index: best.index } : { blocked: step === "up" ? "top" : "bottom" };
|
|
183
|
+
}
|
|
184
|
+
function nearestIndex(others, item, point, pitch, columns = DASHBOARD_COLUMNS) {
|
|
185
|
+
let best = 0;
|
|
186
|
+
let bestDistance = Infinity;
|
|
187
|
+
for (let index = 0; index <= others.length; index++) {
|
|
188
|
+
const p = landingAt(others, item, index, columns);
|
|
189
|
+
const distance = Math.hypot((p.col - point.col) * pitch.col, (p.row - point.row) * pitch.row);
|
|
190
|
+
if (distance < bestDistance - 1e-3) {
|
|
191
|
+
best = index;
|
|
192
|
+
bestDistance = distance;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return best;
|
|
196
|
+
}
|
|
197
|
+
var DASHBOARD_WIDTHS = [
|
|
198
|
+
{ w: 3, label: "A quarter of the width", words: "a quarter of the width" },
|
|
199
|
+
{ w: 4, label: "A third of the width", words: "a third of the width" },
|
|
200
|
+
{ w: 6, label: "Half the width", words: "half the width" },
|
|
201
|
+
{ w: 8, label: "Two thirds of the width", words: "two thirds of the width" },
|
|
202
|
+
{ w: 9, label: "Three quarters of the width", words: "three quarters of the width" },
|
|
203
|
+
{ w: 12, label: "The full width", words: "the full width" }
|
|
204
|
+
];
|
|
205
|
+
var DASHBOARD_HEIGHTS = [
|
|
206
|
+
{ h: 2, label: "Short" },
|
|
207
|
+
{ h: 5, label: "Medium" },
|
|
208
|
+
{ h: 8, label: "Tall" }
|
|
209
|
+
];
|
|
210
|
+
var widthWords = (w) => DASHBOARD_WIDTHS.find((option) => option.w === w)?.words ?? `${w} ${w === 1 ? "column" : "columns"} wide`;
|
|
211
|
+
var sizeWords = (w, h) => `${widthWords(w)}, ${h} ${h === 1 ? "row" : "rows"} tall`;
|
|
212
|
+
var GAP = "var(--ds-space-4)";
|
|
213
|
+
var ROW = "var(--ds-space-12)";
|
|
214
|
+
var FLOOR = `calc(4 * ${ROW} + 3 * ${GAP})`;
|
|
215
|
+
var UNLISTED = { w: 4, h: 4 };
|
|
216
|
+
var WATCH_MS = 1e4;
|
|
217
|
+
var NEW_ID = "\0new";
|
|
218
|
+
var NO_BREAK_SPACE = String.fromCharCode(160);
|
|
219
|
+
var LIFTED_LAYER = "ds-dashboard-lifted-layer";
|
|
220
|
+
var FRAMES = '[data-card], [data-gs="table"], [data-gs-paged], [data-gs="chart"], [data-stats-card]';
|
|
221
|
+
var SETTLE_MS = 1e4;
|
|
222
|
+
var CONTROL = cn(
|
|
223
|
+
"absolute z-[1] inline-flex items-center justify-center h-[var(--ds-control-height-sm)] w-[var(--ds-control-height-sm)]",
|
|
224
|
+
"rounded-[var(--ds-radius-md)] text-[var(--ds-text-muted)] hover:text-[var(--ds-text-primary)] hover:bg-[var(--ds-bg-muted)]",
|
|
225
|
+
"transition-colors duration-[var(--ds-transition-speed)]",
|
|
226
|
+
FOCUS_RING
|
|
227
|
+
);
|
|
228
|
+
var PLACE_TOP = [
|
|
229
|
+
"top-[calc(var(--ds-space-4)+(var(--ds-font-size-lg)*var(--ds-line-height-lg)-var(--ds-control-height-sm))/2)]",
|
|
230
|
+
"[:has(>[data-dashboard-content]>[data-stats-card])>&]:top-[calc(var(--ds-space-3)+(var(--ds-font-size-xs)*4/3-var(--ds-control-height-sm))/2)]",
|
|
231
|
+
"sm:[:has(>[data-dashboard-content]>[data-stats-card])>&]:top-[calc(var(--ds-space-5)+(var(--ds-font-size-xs)*4/3-var(--ds-control-height-sm))/2)]",
|
|
232
|
+
"@max-[13rem]/dashboard-cell:top-ds-4",
|
|
233
|
+
"@max-[13rem]/dashboard-cell:[:has(>[data-dashboard-content]>[data-stats-card])>&]:top-ds-3",
|
|
234
|
+
"sm:@max-[13rem]/dashboard-cell:[:has(>[data-dashboard-content]>[data-stats-card])>&]:top-ds-5"
|
|
235
|
+
].join(" ");
|
|
236
|
+
var PLACE_START = "start-ds-5 [:has(>[data-dashboard-content]>[data-stats-card])>&]:start-ds-3 sm:[:has(>[data-dashboard-content]>[data-stats-card])>&]:start-ds-5";
|
|
237
|
+
var PLACE_END = "end-ds-5 [:has(>[data-dashboard-content]>[data-stats-card])>&]:end-ds-3 sm:[:has(>[data-dashboard-content]>[data-stats-card])>&]:end-ds-5";
|
|
238
|
+
var GRIP = cn(
|
|
239
|
+
"absolute bottom-[4px] end-[4px] z-[1] inline-flex h-6 w-6 items-center justify-center rounded-[var(--ds-radius-sm)]",
|
|
240
|
+
"text-[var(--ds-text-muted)] hover:text-[var(--ds-text-primary)] hover:bg-[var(--ds-bg-muted)]",
|
|
241
|
+
"cursor-nwse-resize rtl:cursor-nesw-resize touch-manipulation select-none [-webkit-touch-callout:none]",
|
|
242
|
+
FOCUS_RING
|
|
243
|
+
);
|
|
244
|
+
var READOUT = cn(
|
|
245
|
+
"pointer-events-none absolute end-[4px] bottom-[calc(4px+1.5rem+var(--ds-space-1))] z-[2] whitespace-nowrap px-ds-2 py-ds-1 text-xs tabular-nums",
|
|
246
|
+
"bg-[var(--ds-bg-surface-elevated)] text-[var(--ds-text-primary)] border-[length:var(--ds-border-width)] border-[var(--ds-border-default)] rounded-[var(--ds-radius-sm)]",
|
|
247
|
+
GLASS_DROPDOWN
|
|
248
|
+
);
|
|
249
|
+
var PLACEHOLDER = "rounded-[var(--ds-radius-md)] border-2 border-dashed border-[var(--ds-accent-primary)] bg-[var(--ds-accent-subtle)]";
|
|
250
|
+
var LIFTED = "rounded-[var(--ds-radius-md)] shadow-[var(--ds-shadow-lg)] outline-2 outline-dashed outline-offset-2 outline-[var(--ds-accent-primary)]";
|
|
251
|
+
var SILENT = { onDragStart: () => void 0, onDragOver: () => void 0, onDragEnd: () => void 0, onDragCancel: () => void 0 };
|
|
252
|
+
var MOUSE = { activationConstraint: { distance: 5 } };
|
|
253
|
+
var TOUCH = { activationConstraint: { delay: 250, tolerance: 5 } };
|
|
254
|
+
function isRtl(el) {
|
|
255
|
+
const dir = el.closest("[dir]")?.getAttribute("dir")?.toLowerCase();
|
|
256
|
+
if (dir === "rtl" || dir === "ltr") return dir === "rtl";
|
|
257
|
+
return typeof getComputedStyle === "function" && getComputedStyle(el).direction === "rtl";
|
|
258
|
+
}
|
|
259
|
+
function speedOf(el) {
|
|
260
|
+
const raw = getComputedStyle(el).getPropertyValue("--ds-transition-speed").trim();
|
|
261
|
+
const n = parseFloat(raw);
|
|
262
|
+
if (!Number.isFinite(n)) return 0;
|
|
263
|
+
return raw.endsWith("ms") ? n : raw.endsWith("s") ? n * 1e3 : n;
|
|
264
|
+
}
|
|
265
|
+
function pitchOf(cells, columns) {
|
|
266
|
+
const style = getComputedStyle(cells);
|
|
267
|
+
const colGap = parseFloat(style.columnGap) || 0;
|
|
268
|
+
const rowGap = parseFloat(style.rowGap) || 0;
|
|
269
|
+
const row = parseFloat(style.gridAutoRows) || 0;
|
|
270
|
+
return { col: (cells.clientWidth + colGap) / columns, row: row + rowGap, rtl: isRtl(cells) };
|
|
271
|
+
}
|
|
272
|
+
function partOf(root, attr, id) {
|
|
273
|
+
return [...root?.querySelectorAll(`[${attr}]`) ?? []].find((el) => el.getAttribute(attr) === id) ?? null;
|
|
274
|
+
}
|
|
275
|
+
var isThenable = (value) => value != null && typeof value.then === "function";
|
|
276
|
+
var clamp2 = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
277
|
+
var DashboardArenaCtx = createContext(null);
|
|
278
|
+
function Lifted({ picture, surface }) {
|
|
279
|
+
const host = useRef(null);
|
|
280
|
+
useLayoutEffect(() => {
|
|
281
|
+
host.current?.replaceChildren(picture);
|
|
282
|
+
}, [picture]);
|
|
283
|
+
return /* @__PURE__ */ jsx(
|
|
284
|
+
"div",
|
|
285
|
+
{
|
|
286
|
+
ref: host,
|
|
287
|
+
"aria-hidden": "true",
|
|
288
|
+
"data-dashboard-lifted": "",
|
|
289
|
+
className: cn(
|
|
290
|
+
"h-full w-full rounded-[var(--ds-radius-md)] shadow-[var(--ds-shadow-lg)]",
|
|
291
|
+
surface && "overflow-hidden bg-[var(--ds-bg-surface)] border-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]"
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
function pictureOf(el) {
|
|
297
|
+
if (!el) return null;
|
|
298
|
+
const copy = el.cloneNode(true);
|
|
299
|
+
copy.removeAttribute("role");
|
|
300
|
+
copy.removeAttribute("aria-label");
|
|
301
|
+
copy.removeAttribute("id");
|
|
302
|
+
for (const named of copy.querySelectorAll("[id]")) named.removeAttribute("id");
|
|
303
|
+
copy.setAttribute("inert", "");
|
|
304
|
+
copy.style.width = "100%";
|
|
305
|
+
copy.style.height = "100%";
|
|
306
|
+
return copy;
|
|
307
|
+
}
|
|
308
|
+
function DragArena({ children }) {
|
|
309
|
+
const drags = useRef(null);
|
|
310
|
+
const [grid, setGrid] = useState(null);
|
|
311
|
+
const [lifted, setLifted] = useState(null);
|
|
312
|
+
const [settle, setSettle] = useState(null);
|
|
313
|
+
const hydrated = useHydrated();
|
|
314
|
+
const sensors = useSensors(useSensor(MouseSensor, MOUSE), useSensor(TouchSensor, TOUCH));
|
|
315
|
+
const api = useMemo(() => ({
|
|
316
|
+
connect: (handlers) => {
|
|
317
|
+
if (drags.current && drags.current !== handlers) warnOnce("DashboardGrid", "one DashboardGrid at a time in a DashboardEditor: show one tab's grid at a time.");
|
|
318
|
+
drags.current = handlers;
|
|
319
|
+
return () => {
|
|
320
|
+
if (drags.current === handlers) drags.current = null;
|
|
321
|
+
};
|
|
322
|
+
},
|
|
323
|
+
publish: setGrid
|
|
324
|
+
}), []);
|
|
325
|
+
const value = useMemo(() => ({ ...api, grid, dragging: lifted?.type ?? null }), [api, grid, lifted]);
|
|
326
|
+
const dragOf = (active) => active.data.current;
|
|
327
|
+
const point = useRef(null);
|
|
328
|
+
const collisionDetection = useCallback(({ pointerCoordinates }) => {
|
|
329
|
+
point.current = pointerCoordinates;
|
|
330
|
+
return [];
|
|
331
|
+
}, []);
|
|
332
|
+
const dragOn = lifted !== null;
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
if (!dragOn) return;
|
|
335
|
+
const claim = (e) => {
|
|
336
|
+
if (e.key === "Escape") e.preventDefault();
|
|
337
|
+
};
|
|
338
|
+
document.addEventListener("keydown", claim, true);
|
|
339
|
+
return () => document.removeEventListener("keydown", claim, true);
|
|
340
|
+
}, [dragOn]);
|
|
341
|
+
return /* @__PURE__ */ jsxs(
|
|
342
|
+
DndContext,
|
|
343
|
+
{
|
|
344
|
+
sensors,
|
|
345
|
+
accessibility: { announcements: SILENT, screenReaderInstructions: { draggable: "" }, restoreFocus: false, container: hydrated ? document.body : void 0 },
|
|
346
|
+
collisionDetection,
|
|
347
|
+
cancelDrop: ({ active }) => {
|
|
348
|
+
const drag = dragOf(active);
|
|
349
|
+
return !!drag && !!drags.current?.current.refuses(drag, point.current);
|
|
350
|
+
},
|
|
351
|
+
onDragStart: ({ active }) => {
|
|
352
|
+
const drag = dragOf(active);
|
|
353
|
+
if (!drag) return;
|
|
354
|
+
const node = drag.picture();
|
|
355
|
+
setSettle({ type: drag.type, speed: speedOf(node ?? document.documentElement), rtl: !!node && isRtl(node) });
|
|
356
|
+
setLifted({ type: drag.type, picture: drag.type === "resize" ? null : pictureOf(node) });
|
|
357
|
+
drags.current?.current.start(drag);
|
|
358
|
+
},
|
|
359
|
+
onDragMove: (event) => {
|
|
360
|
+
const drag = dragOf(event.active);
|
|
361
|
+
if (drag) drags.current?.current.move(drag, event, point.current);
|
|
362
|
+
},
|
|
363
|
+
onDragEnd: ({ active }) => {
|
|
364
|
+
const drag = dragOf(active);
|
|
365
|
+
setLifted(null);
|
|
366
|
+
if (drag) drags.current?.current.end(drag);
|
|
367
|
+
},
|
|
368
|
+
onDragCancel: ({ active }) => {
|
|
369
|
+
const drag = dragOf(active);
|
|
370
|
+
setLifted(null);
|
|
371
|
+
if (drag) drags.current?.current.cancel(drag);
|
|
372
|
+
},
|
|
373
|
+
children: [
|
|
374
|
+
/* @__PURE__ */ jsx(DashboardArenaCtx, { value, children }),
|
|
375
|
+
hydrated && createPortal(
|
|
376
|
+
/* @__PURE__ */ jsx(
|
|
377
|
+
DragOverlay,
|
|
378
|
+
{
|
|
379
|
+
className: LIFTED_LAYER,
|
|
380
|
+
dropAnimation: settle?.type === "move" && settle.speed > 0 ? { duration: settle.speed, easing: "ease" } : null,
|
|
381
|
+
style: { cursor: settle?.type === "resize" ? settle.rtl ? "nesw-resize" : "nwse-resize" : "grabbing" },
|
|
382
|
+
children: lifted?.picture ? /* @__PURE__ */ jsx(Lifted, { picture: lifted.picture, surface: lifted.type === "kind" }) : null
|
|
383
|
+
}
|
|
384
|
+
),
|
|
385
|
+
document.body
|
|
386
|
+
)
|
|
387
|
+
]
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
function DashboardEditor({ children }) {
|
|
392
|
+
return /* @__PURE__ */ jsx(DragArena, { children });
|
|
393
|
+
}
|
|
394
|
+
function DashboardItem({ children }) {
|
|
395
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
396
|
+
}
|
|
397
|
+
function itemsOf(children) {
|
|
398
|
+
const items = [];
|
|
399
|
+
const repeated = [];
|
|
400
|
+
let strays = 0;
|
|
401
|
+
const seen = /* @__PURE__ */ new Set();
|
|
402
|
+
const open = (node) => Children.forEach(node, (child) => {
|
|
403
|
+
if (!isValidElement(child)) return;
|
|
404
|
+
if (child.type === Fragment$1) {
|
|
405
|
+
open(child.props.children);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (elementType(child.type) !== DashboardItem) {
|
|
409
|
+
strays++;
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const { id, label, children: content } = child.props;
|
|
413
|
+
if (typeof id !== "string" || id === "" || seen.has(id)) {
|
|
414
|
+
repeated.push(String(id));
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
seen.add(id);
|
|
418
|
+
items.push({ id, label: typeof label === "string" && label ? label : id, content });
|
|
419
|
+
});
|
|
420
|
+
open(children);
|
|
421
|
+
return { items, strays, repeated };
|
|
422
|
+
}
|
|
423
|
+
function drawnLayout(layout, ids) {
|
|
424
|
+
const present = new Set(ids);
|
|
425
|
+
const known = normaliseLayout(layout).filter((item) => present.has(item.id));
|
|
426
|
+
const listed = new Set(known.map((item) => item.id));
|
|
427
|
+
return [...known, ...ids.filter((id) => !listed.has(id)).map((id) => ({ id, ...UNLISTED }))];
|
|
428
|
+
}
|
|
429
|
+
function applyPreview(committed, preview) {
|
|
430
|
+
if (!preview) return committed;
|
|
431
|
+
if (preview.type === "move") return moveItem(committed, preview.id, preview.index);
|
|
432
|
+
if (preview.type === "resize") return resizeItem(committed, preview.id, { w: preview.w, h: preview.h });
|
|
433
|
+
return addItem(committed, { id: NEW_ID, w: preview.kind.w, h: preview.kind.h }, preview.index);
|
|
434
|
+
}
|
|
435
|
+
var BLOCKED = {
|
|
436
|
+
first: (name) => `${name} is first.`,
|
|
437
|
+
last: (name) => `${name} is last.`,
|
|
438
|
+
top: (name) => `${name} is in the top row.`,
|
|
439
|
+
bottom: (name) => `${name} is in the bottom row.`
|
|
440
|
+
};
|
|
441
|
+
function tidy(entries) {
|
|
442
|
+
const isDivider = (e) => !!e && "type" in e && e.type === "divider";
|
|
443
|
+
return entries.reduce((acc, e) => isDivider(e) && (acc.length === 0 || isDivider(acc[acc.length - 1])) ? acc : [...acc, e], []).filter((e, i, all) => !(isDivider(e) && i === all.length - 1));
|
|
444
|
+
}
|
|
445
|
+
function menuEntries(item, at, total, stacked, actions) {
|
|
446
|
+
const { id } = item;
|
|
447
|
+
const last = total - 1;
|
|
448
|
+
const moves = [
|
|
449
|
+
...at > 1 ? [{ label: "Move to the start", onClick: () => actions.moveTo(id, 0) }] : [],
|
|
450
|
+
...at > 0 ? [{ label: "Move earlier", onClick: () => actions.moveTo(id, at - 1) }] : [],
|
|
451
|
+
...at < last ? [{ label: "Move later", onClick: () => actions.moveTo(id, at + 1) }] : [],
|
|
452
|
+
...at < last - 1 ? [{ label: "Move to the end", onClick: () => actions.moveTo(id, last) }] : []
|
|
453
|
+
];
|
|
454
|
+
const limits = sizeLimits(item);
|
|
455
|
+
const widths = DASHBOARD_WIDTHS.filter((o) => o.w !== item.w && o.w >= limits.minW && o.w <= limits.maxW).map((o) => ({ label: o.label, onClick: () => actions.sizeTo(id, { w: o.w }) }));
|
|
456
|
+
const heights = DASHBOARD_HEIGHTS.filter((o) => o.h !== item.h && o.h >= limits.minH && o.h <= limits.maxH).map((o) => ({ label: o.label, onClick: () => actions.sizeTo(id, { h: o.h }) }));
|
|
457
|
+
const divider = { type: "divider" };
|
|
458
|
+
const size = widths.length && heights.length ? [...widths, divider, ...heights] : [...widths, ...heights];
|
|
459
|
+
const watched = (entry) => ({ ...entry, onClick: () => {
|
|
460
|
+
actions.chose(id);
|
|
461
|
+
entry.onClick();
|
|
462
|
+
} });
|
|
463
|
+
const own = (actions.itemActions?.(id) ?? []).map((entry) => "type" in entry ? entry : "items" in entry ? { ...entry, items: entry.items.map((sub) => "type" in sub ? sub : watched(sub)) } : watched(entry));
|
|
464
|
+
const select = actions.onSelect;
|
|
465
|
+
return tidy([
|
|
466
|
+
...select ? [{ label: "Set up", onClick: () => select(id) }] : [],
|
|
467
|
+
...moves.length ? [{ label: "Move", items: moves }] : [],
|
|
468
|
+
// A12: a stacked grid has no resizing; its widgets keep their rows, and their widths don't show
|
|
469
|
+
...!stacked && size.length ? [{ label: "Size", items: size }] : [],
|
|
470
|
+
divider,
|
|
471
|
+
...own
|
|
472
|
+
]);
|
|
473
|
+
}
|
|
474
|
+
function DashboardGrid(props) {
|
|
475
|
+
const arena = useContext(DashboardArenaCtx);
|
|
476
|
+
return arena ? /* @__PURE__ */ jsx(Grid, { ...props, arena }) : /* @__PURE__ */ jsx(DragArena, { children: /* @__PURE__ */ jsx(OwnArenaGrid, { ...props }) });
|
|
477
|
+
}
|
|
478
|
+
function OwnArenaGrid(props) {
|
|
479
|
+
const arena = useContext(DashboardArenaCtx);
|
|
480
|
+
return arena ? /* @__PURE__ */ jsx(Grid, { ...props, arena }) : null;
|
|
481
|
+
}
|
|
482
|
+
var PART_ATTR = { handle: "data-dashboard-handle", menu: "data-dashboard-menu", grip: "data-dashboard-resize" };
|
|
483
|
+
function Grid({ layout, onLayoutChange, editing = false, onSelect, itemActions, onAdd, children, "aria-label": ariaLabel = "Dashboard widgets", className, arena }) {
|
|
484
|
+
const prefix = useId();
|
|
485
|
+
const { items, strays, repeated } = itemsOf(children);
|
|
486
|
+
const byId = new Map(items.map((item) => [item.id, item]));
|
|
487
|
+
const committed = drawnLayout(layout, items.map((item) => item.id));
|
|
488
|
+
const committedIds = committed.map((item) => item.id).join("\n");
|
|
489
|
+
const nameOf = (id) => byId.get(id)?.label ?? id;
|
|
490
|
+
const rootRef = useRef(null);
|
|
491
|
+
const cellsRef = useRef(null);
|
|
492
|
+
const stacked = useCompact(rootRef, "sm", 16);
|
|
493
|
+
const columns = stacked ? 1 : DASHBOARD_COLUMNS;
|
|
494
|
+
const [preview, setPreviewState] = useState(null);
|
|
495
|
+
const previewRef = useRef(null);
|
|
496
|
+
const setPreview = (next) => {
|
|
497
|
+
previewRef.current = next;
|
|
498
|
+
setPreviewState(next);
|
|
499
|
+
};
|
|
500
|
+
if (preview && (!editing || preview.type !== "add" && !committed.some((item) => item.id === preview.id))) setPreviewState(null);
|
|
501
|
+
useLayoutEffect(() => {
|
|
502
|
+
previewRef.current = preview;
|
|
503
|
+
}, [preview]);
|
|
504
|
+
const shown = applyPreview(committed, preview);
|
|
505
|
+
const busy = preview !== null || arena.dragging !== null;
|
|
506
|
+
const [notice, setNotice] = useState("");
|
|
507
|
+
const say = useCallback((text) => setNotice((was) => was === text ? text + NO_BREAK_SPACE : text), []);
|
|
508
|
+
const refocus = useRef(null);
|
|
509
|
+
const [, setTick] = useState(0);
|
|
510
|
+
const removing = useRef(null);
|
|
511
|
+
const focusLater = (target) => {
|
|
512
|
+
refocus.current = target;
|
|
513
|
+
setTimeout(() => {
|
|
514
|
+
if (refocus.current === target) refocus.current = null;
|
|
515
|
+
}, WATCH_MS);
|
|
516
|
+
};
|
|
517
|
+
const commit = (next) => onLayoutChange?.(next);
|
|
518
|
+
const pickUp = (id, by) => {
|
|
519
|
+
const at = committed.findIndex((item) => item.id === id);
|
|
520
|
+
if (at < 0) return;
|
|
521
|
+
setPreview({ type: "move", id, index: at, from: at, by });
|
|
522
|
+
say(`Picked up ${nameOf(id)}, position ${at + 1} of ${committed.length}.`);
|
|
523
|
+
};
|
|
524
|
+
const stepMove = (step) => {
|
|
525
|
+
const p = previewRef.current;
|
|
526
|
+
if (p?.type !== "move") return;
|
|
527
|
+
const result = stepTarget(shown, p.id, step, columns);
|
|
528
|
+
if ("blocked" in result) {
|
|
529
|
+
say(BLOCKED[result.blocked](nameOf(p.id)));
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
setPreview({ ...p, index: result.index });
|
|
533
|
+
say(`${nameOf(p.id)}, position ${result.index + 1} of ${committed.length}.`);
|
|
534
|
+
};
|
|
535
|
+
const dropMove = () => {
|
|
536
|
+
const p = previewRef.current;
|
|
537
|
+
if (p?.type !== "move") return;
|
|
538
|
+
setPreview(null);
|
|
539
|
+
say(`Dropped ${nameOf(p.id)} at position ${p.index + 1} of ${committed.length}.`);
|
|
540
|
+
if (p.index === p.from) return;
|
|
541
|
+
commit(moveItem(committed, p.id, p.index));
|
|
542
|
+
focusLater({ id: p.id, part: "handle" });
|
|
543
|
+
};
|
|
544
|
+
const cancelMove = () => {
|
|
545
|
+
const p = previewRef.current;
|
|
546
|
+
if (p?.type !== "move") return;
|
|
547
|
+
setPreview(null);
|
|
548
|
+
say(`Move cancelled. ${nameOf(p.id)} is back at position ${p.from + 1} of ${committed.length}.`);
|
|
549
|
+
};
|
|
550
|
+
const startResize = (id, by) => {
|
|
551
|
+
const item = committed.find((it) => it.id === id);
|
|
552
|
+
if (!item) return;
|
|
553
|
+
setPreview({ type: "resize", id, w: item.w, h: item.h, from: { w: item.w, h: item.h }, by });
|
|
554
|
+
say(`Resizing ${nameOf(id)}, ${sizeWords(item.w, item.h)}.`);
|
|
555
|
+
};
|
|
556
|
+
const resizeTo = (w, h) => {
|
|
557
|
+
const p = previewRef.current;
|
|
558
|
+
const item = p?.type === "resize" ? committed.find((it) => it.id === p.id) : void 0;
|
|
559
|
+
if (p?.type !== "resize" || !item) return null;
|
|
560
|
+
const limits = sizeLimits(item);
|
|
561
|
+
const next = { w: clamp2(w, limits.minW, limits.maxW), h: clamp2(h, limits.minH, limits.maxH) };
|
|
562
|
+
if (next.w !== p.w || next.h !== p.h) setPreview({ ...p, ...next });
|
|
563
|
+
return next;
|
|
564
|
+
};
|
|
565
|
+
const stepResize = (dw, dh) => {
|
|
566
|
+
const p = previewRef.current;
|
|
567
|
+
if (p?.type !== "resize") return;
|
|
568
|
+
const next = resizeTo(p.w + dw, p.h + dh);
|
|
569
|
+
if (!next) return;
|
|
570
|
+
const name = nameOf(p.id);
|
|
571
|
+
if (next.w === p.w && next.h === p.h) {
|
|
572
|
+
say(dw > 0 ? `${name} is at its widest.` : dw < 0 ? `${name} is at its narrowest.` : dh > 0 ? `${name} is at its tallest.` : `${name} is at its shortest.`);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
say(`${name}, ${sizeWords(next.w, next.h)}.`);
|
|
576
|
+
};
|
|
577
|
+
const keepResize = () => {
|
|
578
|
+
const p = previewRef.current;
|
|
579
|
+
if (p?.type !== "resize") return;
|
|
580
|
+
setPreview(null);
|
|
581
|
+
const name = nameOf(p.id);
|
|
582
|
+
if (p.w === p.from.w && p.h === p.from.h) {
|
|
583
|
+
say(`${name} is still ${sizeWords(p.w, p.h)}.`);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
commit(resizeItem(committed, p.id, { w: p.w, h: p.h }));
|
|
587
|
+
say(`Resized ${name} to ${sizeWords(p.w, p.h)}.`);
|
|
588
|
+
};
|
|
589
|
+
const cancelResize = () => {
|
|
590
|
+
const p = previewRef.current;
|
|
591
|
+
if (p?.type !== "resize") return;
|
|
592
|
+
setPreview(null);
|
|
593
|
+
say(`Resize cancelled. ${nameOf(p.id)} is back to ${sizeWords(p.from.w, p.from.h)}.`);
|
|
594
|
+
};
|
|
595
|
+
const moveTo = (id, index) => {
|
|
596
|
+
const at = committed.findIndex((item) => item.id === id);
|
|
597
|
+
if (at < 0 || index === at) return;
|
|
598
|
+
commit(moveItem(committed, id, index));
|
|
599
|
+
focusLater({ id, part: "menu" });
|
|
600
|
+
say(`Moved ${nameOf(id)} to position ${index + 1} of ${committed.length}.`);
|
|
601
|
+
};
|
|
602
|
+
const sizeTo = (id, size) => {
|
|
603
|
+
const next = resizeItem(committed, id, size);
|
|
604
|
+
const item = next.find((it) => it.id === id);
|
|
605
|
+
if (!item) return;
|
|
606
|
+
commit(next);
|
|
607
|
+
say(`Resized ${nameOf(id)} to ${sizeWords(item.w, item.h)}.`);
|
|
608
|
+
};
|
|
609
|
+
const watchRemoval = (id) => {
|
|
610
|
+
const cells = cellsRef.current;
|
|
611
|
+
const trigger = partOf(cells, PART_ATTR.menu, id);
|
|
612
|
+
const cell = partOf(cells, "data-dashboard-cell", id);
|
|
613
|
+
const entry = { id, name: nameOf(id) };
|
|
614
|
+
removing.current = entry;
|
|
615
|
+
setTimeout(() => {
|
|
616
|
+
if (removing.current === entry) removing.current = null;
|
|
617
|
+
}, WATCH_MS);
|
|
618
|
+
if (trigger && cell && cells) focusAfterSelfRemoval(trigger, { item: cell, group: cells });
|
|
619
|
+
};
|
|
620
|
+
const menuActions = { moveTo, sizeTo, chose: watchRemoval, onSelect, itemActions };
|
|
621
|
+
const add = (kind, index, then) => {
|
|
622
|
+
if (!onAdd) {
|
|
623
|
+
warnOnce("DashboardGrid", "a widget was added from the palette, but the grid has no onAdd: give it one that adds the page's widget and its layout entry (addItem) and returns the new widget's id.");
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const failed = (why, error) => {
|
|
627
|
+
console.error(`DashboardGrid: ${why}, so nothing was added.`, error);
|
|
628
|
+
say(`Couldn't add ${kind.name}.`);
|
|
629
|
+
};
|
|
630
|
+
let result;
|
|
631
|
+
try {
|
|
632
|
+
result = onAdd(kind, index ?? committed.length);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
failed("onAdd threw", error);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const land = (id) => {
|
|
638
|
+
if (typeof id !== "string" || id === "") {
|
|
639
|
+
warnOnce("DashboardGrid", "onAdd returned no id: return the new widget's id (or a promise of it), so focus can go to it.");
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
focusLater({ id, part: "handle", added: true });
|
|
643
|
+
setTick((n) => n + 1);
|
|
644
|
+
then?.();
|
|
645
|
+
};
|
|
646
|
+
if (isThenable(result)) return result.then(land, (error) => failed("onAdd's promise was rejected", error));
|
|
647
|
+
land(result);
|
|
648
|
+
};
|
|
649
|
+
const addLatest = useLatest(add);
|
|
650
|
+
const handle = useMemo(() => ({ stacked, editing, add: (kind, then) => addLatest.current(kind, void 0, then) }), [stacked, editing, addLatest]);
|
|
651
|
+
const { publish, connect } = arena;
|
|
652
|
+
useLayoutEffect(() => {
|
|
653
|
+
publish(handle);
|
|
654
|
+
return () => publish(null);
|
|
655
|
+
}, [publish, handle]);
|
|
656
|
+
const pitch = useRef(null);
|
|
657
|
+
const pointOf = (event) => {
|
|
658
|
+
const cells = cellsRef.current;
|
|
659
|
+
const p = pitch.current;
|
|
660
|
+
const rect = event.active.rect.current.translated;
|
|
661
|
+
if (!cells || !p || !rect || p.col <= 0 || p.row <= 0) return null;
|
|
662
|
+
const box = cells.getBoundingClientRect();
|
|
663
|
+
return { col: (p.rtl ? box.right - rect.right : rect.left - box.left) / p.col, row: (rect.top - box.top) / p.row };
|
|
664
|
+
};
|
|
665
|
+
const whereIs = (point) => {
|
|
666
|
+
const box = cellsRef.current?.getBoundingClientRect();
|
|
667
|
+
if (!editing || !box || !point || point.x < box.left || point.x > box.right || point.y < box.top || point.y > box.bottom) return "away";
|
|
668
|
+
if (typeof document.elementsFromPoint !== "function") return "over";
|
|
669
|
+
const top = document.elementsFromPoint(point.x, point.y).find((el) => !el.closest(`.${LIFTED_LAYER}`));
|
|
670
|
+
if (!top) return "away";
|
|
671
|
+
return rootRef.current?.contains(top) ? "over" : "covered";
|
|
672
|
+
};
|
|
673
|
+
const drags = {
|
|
674
|
+
start(drag) {
|
|
675
|
+
pitch.current = cellsRef.current ? pitchOf(cellsRef.current, columns) : null;
|
|
676
|
+
if (drag.type === "move") pickUp(drag.id, "pointer");
|
|
677
|
+
else if (drag.type === "resize") startResize(drag.id, "pointer");
|
|
678
|
+
else say(`Picked up ${drag.kind.name}.`);
|
|
679
|
+
},
|
|
680
|
+
move(drag, event, at) {
|
|
681
|
+
const p = previewRef.current;
|
|
682
|
+
const g = pitch.current;
|
|
683
|
+
if (drag.type === "resize") {
|
|
684
|
+
if (p?.type !== "resize" || !g || g.col <= 0 || g.row <= 0) return;
|
|
685
|
+
const dx = g.rtl ? -event.delta.x : event.delta.x;
|
|
686
|
+
resizeTo(p.from.w + Math.round(dx / g.col), p.from.h + Math.round(event.delta.y / g.row));
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const point = pointOf(event);
|
|
690
|
+
if (drag.type === "move") {
|
|
691
|
+
const item2 = committed.find((it) => it.id === drag.id);
|
|
692
|
+
if (p?.type !== "move" || !point || !g || !item2) return;
|
|
693
|
+
if (whereIs(at) === "covered") {
|
|
694
|
+
if (p.covered) return;
|
|
695
|
+
setPreview({ ...p, index: p.from, covered: true });
|
|
696
|
+
say(`${nameOf(drag.id)} is not over the dashboard.`);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const index2 = nearestIndex(committed.filter((it) => it.id !== drag.id), item2, point, g, columns);
|
|
700
|
+
if (index2 === p.index && !p.covered) return;
|
|
701
|
+
setPreview({ ...p, index: index2, covered: false });
|
|
702
|
+
say(`${nameOf(drag.id)}, position ${index2 + 1} of ${committed.length}.`);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (!point || !g || whereIs(at) !== "over") {
|
|
706
|
+
if (p?.type === "add") {
|
|
707
|
+
setPreview(null);
|
|
708
|
+
say(`${drag.kind.name} is not over the dashboard.`);
|
|
709
|
+
}
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
const [item] = normaliseLayout([{ id: NEW_ID, w: drag.kind.w, h: drag.kind.h }]);
|
|
713
|
+
const index = nearestIndex(committed, item, point, g, columns);
|
|
714
|
+
if (p?.type === "add" && p.index === index) return;
|
|
715
|
+
setPreview({ type: "add", kind: drag.kind, index });
|
|
716
|
+
say(`${drag.kind.name}, position ${index + 1} of ${committed.length + 1}.`);
|
|
717
|
+
},
|
|
718
|
+
refuses(drag, at) {
|
|
719
|
+
const where = whereIs(at);
|
|
720
|
+
return drag.type === "kind" ? where !== "over" : drag.type === "move" && where === "covered";
|
|
721
|
+
},
|
|
722
|
+
end(drag) {
|
|
723
|
+
const p = previewRef.current;
|
|
724
|
+
if (drag.type === "move") dropMove();
|
|
725
|
+
else if (drag.type === "resize") keepResize();
|
|
726
|
+
else {
|
|
727
|
+
setPreview(null);
|
|
728
|
+
if (p?.type === "add") add(drag.kind, p.index);
|
|
729
|
+
else say(`${drag.kind.name} not added.`);
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
cancel(drag) {
|
|
733
|
+
if (drag.type === "move") cancelMove();
|
|
734
|
+
else if (drag.type === "resize") cancelResize();
|
|
735
|
+
else {
|
|
736
|
+
setPreview(null);
|
|
737
|
+
say(`${drag.kind.name} not added.`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
const dragsRef = useLatest(drags);
|
|
742
|
+
useLayoutEffect(() => connect(dragsRef), [connect, dragsRef]);
|
|
743
|
+
const onHandleKey = (id, e) => {
|
|
744
|
+
const p = previewRef.current;
|
|
745
|
+
const key = e.key;
|
|
746
|
+
const plain2 = !e.ctrlKey && !e.metaKey && !e.altKey;
|
|
747
|
+
if (!(p?.type === "move" && p.by === "keyboard" && p.id === id)) {
|
|
748
|
+
if ((key === " " || key === "Enter") && plain2 && !e.repeat && !p) {
|
|
749
|
+
e.preventDefault();
|
|
750
|
+
pickUp(id, "keyboard");
|
|
751
|
+
}
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if (key === " " || key === "Enter") {
|
|
755
|
+
e.preventDefault();
|
|
756
|
+
if (!e.repeat) dropMove();
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (key === "Escape") {
|
|
760
|
+
e.preventDefault();
|
|
761
|
+
cancelMove();
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
const rtl = isRtl(e.currentTarget);
|
|
765
|
+
const step = key === "ArrowLeft" ? rtl ? "later" : "earlier" : key === "ArrowRight" ? rtl ? "earlier" : "later" : key === "ArrowUp" ? "up" : key === "ArrowDown" ? "down" : key === "Home" ? "first" : key === "End" ? "last" : null;
|
|
766
|
+
if (step && plain2) {
|
|
767
|
+
e.preventDefault();
|
|
768
|
+
stepMove(step);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
if ((e.ctrlKey || e.metaKey) && key.toLowerCase() === "z") e.preventDefault();
|
|
772
|
+
};
|
|
773
|
+
const onGripKey = (id, e) => {
|
|
774
|
+
const p = previewRef.current;
|
|
775
|
+
const key = e.key;
|
|
776
|
+
const plain2 = !e.ctrlKey && !e.metaKey && !e.altKey;
|
|
777
|
+
if (!(p?.type === "resize" && p.by === "keyboard" && p.id === id)) {
|
|
778
|
+
if ((key === " " || key === "Enter") && plain2 && !e.repeat && !p) {
|
|
779
|
+
e.preventDefault();
|
|
780
|
+
startResize(id, "keyboard");
|
|
781
|
+
}
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
if (key === " " || key === "Enter") {
|
|
785
|
+
e.preventDefault();
|
|
786
|
+
if (!e.repeat) keepResize();
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (key === "Escape") {
|
|
790
|
+
e.preventDefault();
|
|
791
|
+
cancelResize();
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const rtl = isRtl(e.currentTarget);
|
|
795
|
+
const change = key === (rtl ? "ArrowLeft" : "ArrowRight") ? [1, 0] : key === (rtl ? "ArrowRight" : "ArrowLeft") ? [-1, 0] : key === "ArrowDown" ? [0, 1] : key === "ArrowUp" ? [0, -1] : null;
|
|
796
|
+
if (change && plain2) {
|
|
797
|
+
e.preventDefault();
|
|
798
|
+
stepResize(change[0], change[1]);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if ((e.ctrlKey || e.metaKey) && key.toLowerCase() === "z") e.preventDefault();
|
|
802
|
+
};
|
|
803
|
+
const onControlBlur = (id, part) => {
|
|
804
|
+
const p = previewRef.current;
|
|
805
|
+
if (part === "handle" && p?.type === "move" && p.by === "keyboard" && p.id === id) cancelMove();
|
|
806
|
+
if (part === "grip" && p?.type === "resize" && p.by === "keyboard" && p.id === id) cancelResize();
|
|
807
|
+
};
|
|
808
|
+
useLayoutEffect(() => {
|
|
809
|
+
const target = refocus.current;
|
|
810
|
+
if (!target) return;
|
|
811
|
+
const cells = cellsRef.current;
|
|
812
|
+
const part = target.added && stacked ? "menu" : target.part;
|
|
813
|
+
const el = partOf(cells, PART_ATTR[part], target.id);
|
|
814
|
+
if (!el) return;
|
|
815
|
+
refocus.current = null;
|
|
816
|
+
if (target.added) {
|
|
817
|
+
const at = committed.findIndex((item) => item.id === target.id);
|
|
818
|
+
const active = document.activeElement;
|
|
819
|
+
const free = !active || active === document.body || !active.isConnected || !!rootRef.current?.contains(active) || !!active.closest("[data-dashboard-palette]");
|
|
820
|
+
if (free) el.focus();
|
|
821
|
+
say(`Added ${byId.get(target.id)?.label ?? target.id} at position ${at + 1} of ${committed.length}.`);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
el.focus();
|
|
825
|
+
});
|
|
826
|
+
useLayoutEffect(() => {
|
|
827
|
+
const r = removing.current;
|
|
828
|
+
if (!r || committedIds.split("\n").includes(r.id)) return;
|
|
829
|
+
removing.current = null;
|
|
830
|
+
say(`Removed ${r.name}.`);
|
|
831
|
+
}, [committedIds, say]);
|
|
832
|
+
useLayoutEffect(() => {
|
|
833
|
+
if (preview?.type !== "move" || preview.by !== "keyboard") return;
|
|
834
|
+
partOf(cellsRef.current, "data-dashboard-cell", preview.id)?.scrollIntoView?.({ block: "nearest", inline: "nearest" });
|
|
835
|
+
}, [preview]);
|
|
836
|
+
const shownKey = shown.map((item) => `${item.id}:${item.w}x${item.h}`).join(",");
|
|
837
|
+
const places = useRef(null);
|
|
838
|
+
const movingId = preview && preview.type !== "add" ? preview.id : null;
|
|
839
|
+
useLayoutEffect(() => {
|
|
840
|
+
const cells = cellsRef.current;
|
|
841
|
+
const at = new Map(placeLayout(shown, columns).map((p) => [p.id, { col: p.col, row: p.row }]));
|
|
842
|
+
const was = places.current;
|
|
843
|
+
places.current = { columns, at };
|
|
844
|
+
if (!cells || !was || was.columns !== columns) return;
|
|
845
|
+
const speed = speedOf(cells);
|
|
846
|
+
if (speed <= 0) return;
|
|
847
|
+
const g = pitchOf(cells, columns);
|
|
848
|
+
for (const el of Array.from(cells.children)) {
|
|
849
|
+
const id = el.getAttribute("data-dashboard-cell");
|
|
850
|
+
if (!id || id === movingId || typeof el.animate !== "function") continue;
|
|
851
|
+
const from = was.at.get(id);
|
|
852
|
+
const to = at.get(id);
|
|
853
|
+
if (!from || !to || from.col === to.col && from.row === to.row) continue;
|
|
854
|
+
const dx = (from.col - to.col) * g.col * (g.rtl ? -1 : 1);
|
|
855
|
+
const dy = (from.row - to.row) * g.row;
|
|
856
|
+
el.animate([{ transform: `translate(${dx}px, ${dy}px)` }, { transform: "none" }], { duration: speed, easing: "ease" });
|
|
857
|
+
}
|
|
858
|
+
}, [shownKey, columns]);
|
|
859
|
+
useEffect(() => {
|
|
860
|
+
if (strays) warnOnce("DashboardGrid", "its children are DashboardItems (an array or a fragment of them): anything else isn't drawn. Put each widget in a DashboardItem with its id and label.");
|
|
861
|
+
if (repeated.length) warnOnce("DashboardGrid", `two DashboardItems share an id, or one has none (${repeated.map((id) => `"${id}"`).join(", ")}): only the first of each id is drawn.`);
|
|
862
|
+
const ids = new Set(items.map((item) => item.id));
|
|
863
|
+
const listed = new Set(normaliseLayout(layout).map((item) => item.id));
|
|
864
|
+
const missing = items.filter((item) => !listed.has(item.id)).map((item) => item.id);
|
|
865
|
+
const orphans = [...listed].filter((id) => !ids.has(id));
|
|
866
|
+
if (missing.length) warnOnce("DashboardGrid", `the layout has no entry for ${missing.map((id) => `"${id}"`).join(", ")}: it's drawn after the rest at ${UNLISTED.w} columns by ${UNLISTED.h} rows. Add each widget's entry (addItem).`);
|
|
867
|
+
if (orphans.length) warnOnce("DashboardGrid", `the layout has entries with no DashboardItem (${orphans.map((id) => `"${id}"`).join(", ")}): they aren't drawn, and a change of layout leaves them out.`);
|
|
868
|
+
if (editing && !onLayoutChange) warnOnce("DashboardGrid", "it's editing with no onLayoutChange, so a move or a size the person chooses isn't kept: keep the layout it passes.");
|
|
869
|
+
});
|
|
870
|
+
const shownAt = new Map(shown.map((item, i) => [item.id, i]));
|
|
871
|
+
const adding = preview?.type === "add" ? shown.find((item) => item.id === NEW_ID) : void 0;
|
|
872
|
+
return /* @__PURE__ */ jsxs("div", { ref: rootRef, "data-dashboard-grid": "", "data-editing": editing ? "" : void 0, "data-stacked": stacked ? "" : void 0, className: cn("relative min-w-0", className), children: [
|
|
873
|
+
editing && stacked && /* @__PURE__ */ jsx("p", { "data-dashboard-narrow": "", className: "mb-ds-3 text-xs text-[var(--ds-text-muted)]", children: "Drag and resize on a larger screen" }),
|
|
874
|
+
/* @__PURE__ */ jsxs(
|
|
875
|
+
"div",
|
|
876
|
+
{
|
|
877
|
+
ref: cellsRef,
|
|
878
|
+
"data-dashboard-cells": "",
|
|
879
|
+
role: editing ? "group" : void 0,
|
|
880
|
+
"aria-label": editing ? ariaLabel : void 0,
|
|
881
|
+
className: "relative isolate grid grid-flow-row",
|
|
882
|
+
style: { gridTemplateColumns: stacked ? "minmax(0, 1fr)" : `repeat(${DASHBOARD_COLUMNS}, minmax(0, 1fr))`, gridAutoRows: ROW, gap: GAP, minHeight: editing ? FLOOR : void 0 },
|
|
883
|
+
children: [
|
|
884
|
+
editing && !stacked && /* @__PURE__ */ jsx("div", { "aria-hidden": "true", "data-dashboard-guides": "", className: "pointer-events-none absolute inset-0 -z-10 grid grid-cols-12", style: { gap: GAP }, children: Array.from({ length: DASHBOARD_COLUMNS }, (_, i) => /* @__PURE__ */ jsx("div", { className: "rounded-[var(--ds-radius-sm)] border border-[color-mix(in_srgb,var(--ds-text-primary)_10%,transparent)]" }, i)) }),
|
|
885
|
+
committed.map((item, at) => {
|
|
886
|
+
const drawn = shown[shownAt.get(item.id) ?? 0] ?? item;
|
|
887
|
+
const own = byId.get(item.id);
|
|
888
|
+
if (!own) return null;
|
|
889
|
+
const state = preview?.type === "move" && preview.id === item.id ? preview.by === "pointer" ? "placeholder" : "lifted" : preview?.type === "resize" && preview.id === item.id ? "resizing" : null;
|
|
890
|
+
return /* @__PURE__ */ jsx(
|
|
891
|
+
Cell,
|
|
892
|
+
{
|
|
893
|
+
prefix,
|
|
894
|
+
id: item.id,
|
|
895
|
+
label: own.label,
|
|
896
|
+
w: drawn.w,
|
|
897
|
+
h: drawn.h,
|
|
898
|
+
order: shownAt.get(item.id) ?? 0,
|
|
899
|
+
editing,
|
|
900
|
+
stacked,
|
|
901
|
+
busy,
|
|
902
|
+
state,
|
|
903
|
+
item,
|
|
904
|
+
at,
|
|
905
|
+
total: committed.length,
|
|
906
|
+
actions: menuActions,
|
|
907
|
+
onSelect,
|
|
908
|
+
onHandleKey,
|
|
909
|
+
onGripKey,
|
|
910
|
+
onControlBlur,
|
|
911
|
+
children: own.content
|
|
912
|
+
},
|
|
913
|
+
item.id
|
|
914
|
+
);
|
|
915
|
+
}),
|
|
916
|
+
adding && /* @__PURE__ */ jsx(
|
|
917
|
+
"div",
|
|
918
|
+
{
|
|
919
|
+
"aria-hidden": "true",
|
|
920
|
+
"data-dashboard-placeholder": "",
|
|
921
|
+
className: PLACEHOLDER,
|
|
922
|
+
style: { gridColumn: stacked ? "1 / -1" : `span ${adding.w}`, gridRow: `span ${adding.h}`, order: shownAt.get(NEW_ID) ?? 0 }
|
|
923
|
+
}
|
|
924
|
+
)
|
|
925
|
+
]
|
|
926
|
+
}
|
|
927
|
+
),
|
|
928
|
+
/* @__PURE__ */ jsx("div", { role: "status", className: "sr-only", children: notice })
|
|
929
|
+
] });
|
|
930
|
+
}
|
|
931
|
+
function GripMark() {
|
|
932
|
+
return /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", viewBox: "0 0 12 12", width: "12", height: "12", className: "rtl:-scale-x-100", children: /* @__PURE__ */ jsx("path", { d: "M11 4.5L4.5 11M11 8L8 11", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", fill: "none" }) });
|
|
933
|
+
}
|
|
934
|
+
function Cell({ prefix, id, label, w, h, order, editing, stacked, busy, state, item, at, total, actions, onSelect, onHandleKey, onGripKey, onControlBlur, children }) {
|
|
935
|
+
const cellRef = useRef(null);
|
|
936
|
+
const contentRef = useRef(null);
|
|
937
|
+
const instructions = useId();
|
|
938
|
+
const gripInstructions = useId();
|
|
939
|
+
const draggable = editing && !stacked;
|
|
940
|
+
const picture = useCallback(() => cellRef.current, []);
|
|
941
|
+
const { setNodeRef, setActivatorNodeRef, listeners } = useDraggable({ id: `${prefix}move:${id}`, data: { type: "move", id, picture }, disabled: !draggable });
|
|
942
|
+
const { setNodeRef: setGripRef, listeners: gripListeners } = useDraggable({ id: `${prefix}resize:${id}`, data: { type: "resize", id, picture }, disabled: !draggable });
|
|
943
|
+
const setCell = useCallback((el) => {
|
|
944
|
+
cellRef.current = el;
|
|
945
|
+
setNodeRef(el);
|
|
946
|
+
}, [setNodeRef]);
|
|
947
|
+
const value = useMemo(() => ({ label, editing }), [label, editing]);
|
|
948
|
+
useCellFrameScroll(contentRef, label);
|
|
949
|
+
const pressed = useRef(false);
|
|
950
|
+
useEffect(() => {
|
|
951
|
+
const frame = contentRef.current?.firstElementChild;
|
|
952
|
+
if (!frame) return;
|
|
953
|
+
const title = (frame.hasAttribute("data-stats-card") ? frame.querySelector("span") : frame.querySelector("h1, h2, h3, h4, h5, h6"))?.textContent?.trim();
|
|
954
|
+
if (!title) warnOnce("DashboardGrid", `the widget "${label}" has no title on screen: give its frame one (ChartContainer's or Table's title, a CardHeader with a CardTitle, a StatsCard's label), as every widget needs one.`);
|
|
955
|
+
else if (title !== label.trim()) warnOnce("DashboardGrid", `the widget "${id}" is labelled "${label}", but its frame's title is "${title}": give its DashboardItem the frame's title as its label, so what a screen reader hears ("Move ${label}") names the widget people see.`);
|
|
956
|
+
});
|
|
957
|
+
useEffect(() => {
|
|
958
|
+
const content = contentRef.current;
|
|
959
|
+
if (!content || typeof MutationObserver === "undefined") return;
|
|
960
|
+
const misused = (what) => warnOnce("DashboardGrid", `the widget "${label}" ${what}: put its frame (a ChartContainer, a StatsCard, a titled Table or a Card) straight in its DashboardItem, alone, since the grid sizes the item's one child and anything else spills over the widgets beside it.`);
|
|
961
|
+
let timer;
|
|
962
|
+
const read = () => {
|
|
963
|
+
clearTimeout(timer);
|
|
964
|
+
const shown = [...content.children].filter((el) => !(el instanceof HTMLElement && el.style.display === "none"));
|
|
965
|
+
const [child] = shown;
|
|
966
|
+
if (shown.length > 1) return misused("holds more than one child");
|
|
967
|
+
if (!child || child.matches(FRAMES) || child.matches('[data-gs="skeleton"]')) return;
|
|
968
|
+
if (child.querySelector(FRAMES)) return misused("has a wrapper round its frame");
|
|
969
|
+
timer = setTimeout(() => {
|
|
970
|
+
if (child.parentElement === content) misused("isn't one of the library's frames");
|
|
971
|
+
}, SETTLE_MS);
|
|
972
|
+
};
|
|
973
|
+
read();
|
|
974
|
+
const observer = new MutationObserver(read);
|
|
975
|
+
observer.observe(content, { childList: true });
|
|
976
|
+
return () => {
|
|
977
|
+
observer.disconnect();
|
|
978
|
+
clearTimeout(timer);
|
|
979
|
+
};
|
|
980
|
+
}, [label]);
|
|
981
|
+
const placeholder = state === "placeholder";
|
|
982
|
+
const hidden = placeholder ? "opacity-0" : void 0;
|
|
983
|
+
return /* @__PURE__ */ jsxs(
|
|
984
|
+
"div",
|
|
985
|
+
{
|
|
986
|
+
ref: setCell,
|
|
987
|
+
"data-dashboard-cell": id,
|
|
988
|
+
"data-editing": editing ? "" : void 0,
|
|
989
|
+
"data-stacked": editing && stacked ? "" : void 0,
|
|
990
|
+
role: editing ? "group" : void 0,
|
|
991
|
+
"aria-label": editing ? label : void 0,
|
|
992
|
+
className: cn("@container/dashboard-cell relative flex min-h-0 min-w-0 flex-col", editing && onSelect && "cursor-pointer"),
|
|
993
|
+
style: { gridColumn: stacked ? "1 / -1" : `span ${w}`, gridRow: `span ${h}`, order },
|
|
994
|
+
onPointerDown: editing ? (e) => {
|
|
995
|
+
const target = e.target;
|
|
996
|
+
pressed.current = e.currentTarget.contains(target) && !target.closest("[data-dashboard-control]");
|
|
997
|
+
} : void 0,
|
|
998
|
+
onClick: editing && onSelect ? (e) => {
|
|
999
|
+
const target = e.target;
|
|
1000
|
+
const began = pressed.current;
|
|
1001
|
+
pressed.current = false;
|
|
1002
|
+
if (!began || !e.currentTarget.contains(target) || target.closest("[data-dashboard-control]")) return;
|
|
1003
|
+
onSelect(id);
|
|
1004
|
+
} : void 0,
|
|
1005
|
+
children: [
|
|
1006
|
+
/* @__PURE__ */ jsx(
|
|
1007
|
+
"div",
|
|
1008
|
+
{
|
|
1009
|
+
ref: contentRef,
|
|
1010
|
+
"data-dashboard-content": "",
|
|
1011
|
+
inert: editing,
|
|
1012
|
+
className: cn("flex min-h-0 flex-1 flex-col *:min-h-0 *:flex-auto", state === "lifted" && LIFTED, hidden),
|
|
1013
|
+
children: /* @__PURE__ */ jsx(DashboardCellContext, { value, children: /* @__PURE__ */ jsx(FillContext, { value: true, children }) })
|
|
1014
|
+
}
|
|
1015
|
+
),
|
|
1016
|
+
placeholder && /* @__PURE__ */ jsx("div", { "aria-hidden": "true", "data-dashboard-place": "", className: cn("pointer-events-none absolute inset-0", PLACEHOLDER) }),
|
|
1017
|
+
editing && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1018
|
+
draggable && /* @__PURE__ */ jsx(AutoTooltip, { content: `Move ${label}`, disabled: busy, children: /* @__PURE__ */ jsx(
|
|
1019
|
+
"button",
|
|
1020
|
+
{
|
|
1021
|
+
type: "button",
|
|
1022
|
+
ref: setActivatorNodeRef,
|
|
1023
|
+
...listeners,
|
|
1024
|
+
"data-dashboard-handle": id,
|
|
1025
|
+
"data-dashboard-control": "",
|
|
1026
|
+
"aria-label": `Move ${label}`,
|
|
1027
|
+
"aria-describedby": instructions,
|
|
1028
|
+
onKeyDown: (e) => onHandleKey(id, e),
|
|
1029
|
+
onBlur: () => onControlBlur(id, "handle"),
|
|
1030
|
+
className: cn(CONTROL, PLACE_TOP, PLACE_START, "cursor-grab active:cursor-grabbing touch-manipulation select-none [-webkit-touch-callout:none]", hidden),
|
|
1031
|
+
children: /* @__PURE__ */ jsx(Icon, { name: "grip-vertical", size: 16 })
|
|
1032
|
+
}
|
|
1033
|
+
) }),
|
|
1034
|
+
/* @__PURE__ */ jsx(
|
|
1035
|
+
DropdownMenu,
|
|
1036
|
+
{
|
|
1037
|
+
items: menuEntries(item, at, total, stacked, actions),
|
|
1038
|
+
trigger: /* @__PURE__ */ jsx(AutoTooltip, { content: `${label} options`, disabled: busy, children: /* @__PURE__ */ jsx("button", { type: "button", "data-dashboard-menu": id, "data-dashboard-control": "", "aria-label": `${label} options`, className: cn(CONTROL, PLACE_TOP, PLACE_END, hidden), children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: 16 }) }) })
|
|
1039
|
+
}
|
|
1040
|
+
),
|
|
1041
|
+
draggable && /* @__PURE__ */ jsx(AutoTooltip, { content: `Resize ${label}`, disabled: busy, children: /* @__PURE__ */ jsx(
|
|
1042
|
+
"button",
|
|
1043
|
+
{
|
|
1044
|
+
type: "button",
|
|
1045
|
+
ref: setGripRef,
|
|
1046
|
+
...gripListeners,
|
|
1047
|
+
"data-dashboard-resize": id,
|
|
1048
|
+
"data-dashboard-control": "",
|
|
1049
|
+
"aria-label": `Resize ${label}`,
|
|
1050
|
+
"aria-describedby": gripInstructions,
|
|
1051
|
+
onKeyDown: (e) => onGripKey(id, e),
|
|
1052
|
+
onBlur: () => onControlBlur(id, "grip"),
|
|
1053
|
+
className: cn(GRIP, hidden),
|
|
1054
|
+
children: /* @__PURE__ */ jsx(GripMark, {})
|
|
1055
|
+
}
|
|
1056
|
+
) }),
|
|
1057
|
+
state === "resizing" && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", "data-dashboard-size": "", className: READOUT, children: `${w} \xD7 ${h}` }),
|
|
1058
|
+
draggable && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1059
|
+
/* @__PURE__ */ jsx("span", { id: instructions, hidden: true, children: `Press Space to pick up ${label}, use the arrow keys to move it, and press Space again to drop it, or Escape to cancel.` }),
|
|
1060
|
+
/* @__PURE__ */ jsx("span", { id: gripInstructions, hidden: true, children: `Press Space to resize ${label}, use the arrow keys to change its width and height, and press Space again to keep it, or Escape to cancel.` })
|
|
1061
|
+
] })
|
|
1062
|
+
] })
|
|
1063
|
+
]
|
|
1064
|
+
}
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
var HINT = (rowsDrag) => rowsDrag ? "Select or drag to add" : "Select a widget to add it";
|
|
1068
|
+
var isThenable2 = (value) => value != null && typeof value.then === "function";
|
|
1069
|
+
function DashboardPalette({ kinds, label = "Widget library", hint = HINT, className }) {
|
|
1070
|
+
const arena = useContext(DashboardArenaCtx);
|
|
1071
|
+
const close = useContext(OverlayCloseCtx);
|
|
1072
|
+
const prefix = useId();
|
|
1073
|
+
const grid = arena?.grid ?? null;
|
|
1074
|
+
useEffect(() => {
|
|
1075
|
+
if (!arena) warnOnce("DashboardPalette", "it adds widgets to a DashboardGrid through a DashboardEditor round them both: wrap the palette and the grid in one DashboardEditor.");
|
|
1076
|
+
}, [arena]);
|
|
1077
|
+
const draggable = !!grid && grid.editing && !grid.stacked;
|
|
1078
|
+
const words = grid?.editing && kinds.length > 0 && hint ? hint(draggable) : null;
|
|
1079
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1080
|
+
words && /* @__PURE__ */ jsx("p", { "data-dashboard-palette-hint": "", className: "mb-ds-2 text-sm text-[var(--ds-text-secondary)]", children: words }),
|
|
1081
|
+
/* @__PURE__ */ jsx("ul", { "data-dashboard-palette": "", "aria-label": label, className: cn("flex flex-col", className), children: kinds.map((kind) => /* @__PURE__ */ jsx(PaletteKind, { prefix, kind, grid: grid?.editing ? grid : null, draggable, close }, kind.id)) })
|
|
1082
|
+
] });
|
|
1083
|
+
}
|
|
1084
|
+
var ROW2 = cn(
|
|
1085
|
+
"flex flex-1 min-w-0 items-center gap-ds-3 py-ds-3 px-ds-2 text-start rounded-[var(--ds-radius-sm)]",
|
|
1086
|
+
"transition-colors duration-[var(--ds-transition-speed)] hover:bg-[var(--ds-bg-emphasis)]",
|
|
1087
|
+
"aria-disabled:cursor-not-allowed aria-disabled:not-aria-busy:opacity-disabled aria-busy:cursor-wait",
|
|
1088
|
+
FOCUS_RING
|
|
1089
|
+
);
|
|
1090
|
+
function PaletteKind({ prefix, kind, grid, draggable, close }) {
|
|
1091
|
+
const rowRef = useRef(null);
|
|
1092
|
+
const picture = useCallback(() => rowRef.current, []);
|
|
1093
|
+
const { setNodeRef, setActivatorNodeRef, listeners, isDragging } = useDraggable({ id: `${prefix}kind:${kind.id}`, data: { type: "kind", kind, picture }, disabled: !draggable });
|
|
1094
|
+
const setRow = useCallback((el) => {
|
|
1095
|
+
rowRef.current = el;
|
|
1096
|
+
setNodeRef(el);
|
|
1097
|
+
setActivatorNodeRef(el);
|
|
1098
|
+
}, [setNodeRef, setActivatorNodeRef]);
|
|
1099
|
+
const describedBy = useId();
|
|
1100
|
+
const dragged = useRef(false);
|
|
1101
|
+
useLayoutEffect(() => {
|
|
1102
|
+
if (isDragging) dragged.current = true;
|
|
1103
|
+
}, [isDragging]);
|
|
1104
|
+
const [adding, setAdding] = useState(false);
|
|
1105
|
+
const unavailable = !grid || adding;
|
|
1106
|
+
const onAdd = (e) => {
|
|
1107
|
+
if (dragged.current && e.detail > 0 || !grid || adding) return;
|
|
1108
|
+
const result = grid.add(kind, close ? () => close() : void 0);
|
|
1109
|
+
if (!isThenable2(result)) return;
|
|
1110
|
+
setAdding(true);
|
|
1111
|
+
result.then(() => setAdding(false), () => setAdding(false));
|
|
1112
|
+
};
|
|
1113
|
+
return /* @__PURE__ */ jsx("li", { className: "flex -mx-ds-2 border-b-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)] last:border-b-0", children: /* @__PURE__ */ jsxs(
|
|
1114
|
+
"button",
|
|
1115
|
+
{
|
|
1116
|
+
type: "button",
|
|
1117
|
+
ref: setRow,
|
|
1118
|
+
...listeners,
|
|
1119
|
+
onPointerDown: (e) => {
|
|
1120
|
+
dragged.current = false;
|
|
1121
|
+
listeners?.onPointerDown?.(e);
|
|
1122
|
+
},
|
|
1123
|
+
onClick: onAdd,
|
|
1124
|
+
"data-dashboard-kind": kind.id,
|
|
1125
|
+
"data-dashboard-kind-draggable": draggable ? "" : void 0,
|
|
1126
|
+
"aria-label": `Add ${kind.name}`,
|
|
1127
|
+
"aria-describedby": kind.description ? describedBy : void 0,
|
|
1128
|
+
"aria-busy": adding || void 0,
|
|
1129
|
+
"aria-disabled": unavailable || void 0,
|
|
1130
|
+
className: cn(ROW2, draggable ? "cursor-grab active:cursor-grabbing touch-manipulation select-none [-webkit-touch-callout:none]" : "cursor-pointer"),
|
|
1131
|
+
children: [
|
|
1132
|
+
kind.icon && // a StatsCard's tile
|
|
1133
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-[var(--ds-radius-sm)] bg-[var(--ds-bg-muted)]", children: /* @__PURE__ */ jsx(Icon, { name: kind.icon, size: 16, className: "text-[var(--ds-text-muted)]" }) }),
|
|
1134
|
+
/* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
|
|
1135
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-[var(--ds-text-primary)] break-words", children: kind.name }),
|
|
1136
|
+
kind.description && /* @__PURE__ */ jsx("span", { id: describedBy, className: "text-xs text-[var(--ds-text-muted)] break-words", children: kind.description })
|
|
1137
|
+
] }),
|
|
1138
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "flex shrink-0 items-center text-[var(--ds-text-muted)]", children: adding ? /* @__PURE__ */ jsx(Spinner, { size: "sm", color: "inherit" }) : /* @__PURE__ */ jsx(Icon, { name: "plus", size: 16 }) })
|
|
1139
|
+
]
|
|
1140
|
+
}
|
|
1141
|
+
) });
|
|
1142
|
+
}
|
|
1143
|
+
function plain(value) {
|
|
1144
|
+
if (Array.isArray(value)) return true;
|
|
1145
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1146
|
+
const proto = Object.getPrototypeOf(value);
|
|
1147
|
+
return proto === Object.prototype || proto === null;
|
|
1148
|
+
}
|
|
1149
|
+
function same(a, b) {
|
|
1150
|
+
if (Object.is(a, b)) return true;
|
|
1151
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
|
|
1152
|
+
if (!plain(a) || !plain(b) || Array.isArray(a) !== Array.isArray(b)) return false;
|
|
1153
|
+
const ak = Object.keys(a);
|
|
1154
|
+
const bk = Object.keys(b);
|
|
1155
|
+
return ak.length === bk.length && ak.every((k) => Object.prototype.hasOwnProperty.call(b, k) && same(a[k], b[k]));
|
|
1156
|
+
}
|
|
1157
|
+
function typesText(target) {
|
|
1158
|
+
if (!(target instanceof Element)) return false;
|
|
1159
|
+
if (target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement || target.isContentEditable) return true;
|
|
1160
|
+
if (target.closest('[contenteditable]:not([contenteditable="false"])')) return true;
|
|
1161
|
+
if (target instanceof HTMLInputElement) return !/^(?:checkbox|radio|button|submit|reset|range|color|file|image)$/.test(target.type);
|
|
1162
|
+
return /^(?:textbox|searchbox|combobox|spinbutton)$/.test(target.getAttribute("role") ?? "");
|
|
1163
|
+
}
|
|
1164
|
+
function useDraftHistory(initial, { shortcuts = true } = {}) {
|
|
1165
|
+
const [store, setStore] = useState(() => {
|
|
1166
|
+
const first = typeof initial === "function" ? initial() : initial;
|
|
1167
|
+
return { past: [], present: first, future: [], saved: first, merge: null };
|
|
1168
|
+
});
|
|
1169
|
+
const set = useCallback((next, merge) => {
|
|
1170
|
+
setStore((s) => {
|
|
1171
|
+
const value = typeof next === "function" ? next(s.present) : next;
|
|
1172
|
+
if (Object.is(value, s.present)) return s;
|
|
1173
|
+
const key = merge ?? null;
|
|
1174
|
+
const past = key !== null && key === s.merge && s.future.length === 0 ? s.past : [...s.past, s.present];
|
|
1175
|
+
return { ...s, past, present: value, future: [], merge: key };
|
|
1176
|
+
});
|
|
1177
|
+
}, []);
|
|
1178
|
+
const undo = useCallback(() => {
|
|
1179
|
+
setStore((s) => s.past.length === 0 ? s : { ...s, past: s.past.slice(0, -1), present: s.past[s.past.length - 1], future: [s.present, ...s.future], merge: null });
|
|
1180
|
+
}, []);
|
|
1181
|
+
const redo = useCallback(() => {
|
|
1182
|
+
setStore((s) => s.future.length === 0 ? s : { ...s, past: [...s.past, s.present], present: s.future[0], future: s.future.slice(1), merge: null });
|
|
1183
|
+
}, []);
|
|
1184
|
+
const reset = useCallback((...args) => {
|
|
1185
|
+
setStore((s) => {
|
|
1186
|
+
const value = args.length > 0 ? args[0] : s.present;
|
|
1187
|
+
return { past: [], present: value, future: [], saved: value, merge: null };
|
|
1188
|
+
});
|
|
1189
|
+
}, []);
|
|
1190
|
+
const canUndo = store.past.length > 0;
|
|
1191
|
+
const canRedo = store.future.length > 0;
|
|
1192
|
+
useEffect(() => {
|
|
1193
|
+
if (!shortcuts || typeof document === "undefined") return;
|
|
1194
|
+
const onKeyDown = (e) => {
|
|
1195
|
+
if (e.defaultPrevented || e.altKey || !(e.ctrlKey || e.metaKey) || e.key.toLowerCase() !== "z" || typesText(e.target)) return;
|
|
1196
|
+
if (e.shiftKey ? !canRedo : !canUndo) return;
|
|
1197
|
+
e.preventDefault();
|
|
1198
|
+
if (e.shiftKey) redo();
|
|
1199
|
+
else undo();
|
|
1200
|
+
};
|
|
1201
|
+
document.addEventListener("keydown", onKeyDown);
|
|
1202
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
1203
|
+
}, [shortcuts, canUndo, canRedo, undo, redo]);
|
|
1204
|
+
const dirty = useMemo(() => !same(store.present, store.saved), [store.present, store.saved]);
|
|
1205
|
+
return { value: store.present, set, undo, redo, canUndo, canRedo, dirty, reset };
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
export { DashboardEditor, DashboardGrid, DashboardItem, DashboardPalette, useDraftHistory };
|
|
1209
|
+
//# sourceMappingURL=dashboard.js.map
|
|
1210
|
+
//# sourceMappingURL=dashboard.js.map
|