@parallelworks/ui 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{CodePreviewEditor-CSPRILMp.js → CodePreviewEditor-B3688Fx4.js} +1 -1
- package/dist/chunks/{Markdown-C5Re2ezs.js → Markdown-iFhq8Gb0.js} +1 -1
- package/dist/chunks/{Monaco-CC4ak1JG.js → Monaco-BEj--GTx.js} +10 -5
- package/dist/chunks/{Provider-DvVvScTQ.js → Provider-uUHgWOWc.js} +55 -2
- package/dist/chunks/{SwitchToggle-2uRw8ZeJ.js → SwitchToggle-BqD0dE-L.js} +36 -29
- package/dist/chunks/{editor-D55tumuZ.js → editor-CdQbKtYO.js} +2 -2
- package/dist/chunks/list-D5f5Fq-O.js +1259 -0
- package/dist/components/Button.d.ts +18 -0
- package/dist/components/Card.d.ts +19 -0
- package/dist/components/Filter.d.ts +15 -0
- package/dist/components/FilterPill.d.ts +7 -0
- package/dist/components/IconButton.d.ts +17 -0
- package/dist/components/Input.d.ts +14 -0
- package/dist/components/LetterBadge.d.ts +6 -0
- package/dist/components/Pagination.d.ts +15 -0
- package/dist/components/Provider.d.ts +25 -0
- package/dist/components/SettingModeOption.d.ts +13 -0
- package/dist/components/SettingRow.d.ts +18 -0
- package/dist/components/SettingsCard.d.ts +24 -0
- package/dist/components/SettingsGroup.d.ts +8 -0
- package/dist/components/Sort.d.ts +9 -0
- package/dist/components/Toggle.d.ts +6 -0
- package/dist/components/dropdownUtils.d.ts +10 -0
- package/dist/components/inputClasses.d.ts +1 -0
- package/dist/editor/index.js +1 -1
- package/dist/file-explorer/index.js +8 -7
- package/dist/form/index.js +2 -2
- package/dist/index.d.ts +15 -0
- package/dist/index.js +429 -5
- package/dist/list/HoverCard.d.ts +22 -0
- package/dist/list/ListPager.d.ts +7 -0
- package/dist/list/ListViewControls.d.ts +12 -0
- package/dist/list/index.d.ts +6 -0
- package/dist/list/index.js +2 -2
- package/dist/list/listSearch.d.ts +27 -0
- package/dist/list/listView.d.ts +129 -0
- package/dist/list/textEntry.d.ts +2 -0
- package/dist/logviewer/index.js +1 -1
- package/dist/styles.css +1 -1
- package/dist/workflow/index.js +1 -1
- package/package.json +2 -6
- package/dist/chunks/list-7PyDF1tz.js +0 -465
|
@@ -0,0 +1,1259 @@
|
|
|
1
|
+
import { CheckIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronUpIcon, ClipboardIcon, CopyIcon, DisplayIcon, ErrorIcon, FilterIcon, IdCardIcon, KeyIcon, LinkIcon, MailIcon, MoreIcon, SearchIcon, UserIcon, XIcon } from "../icons.js";
|
|
2
|
+
import { c as useStrings, i as useNotify, n as useLink } from "./Provider-uUHgWOWc.js";
|
|
3
|
+
import { n as TOOLTIP_ID, r as TooltipInfo } from "./Tooltip-WgbiC_nd.js";
|
|
4
|
+
import { n as Table } from "./Table-C5z6qWjM.js";
|
|
5
|
+
import cx from "classnames";
|
|
6
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
8
|
+
import { createPortal } from "react-dom";
|
|
9
|
+
import { useLocalStorage } from "usehooks-ts";
|
|
10
|
+
//#region src/list/RowContextMenu.tsx
|
|
11
|
+
/** Converts a single row action to a menu item: a `to` makes it a link, otherwise an action. */
|
|
12
|
+
function toRowMenuItem(action) {
|
|
13
|
+
if (action.to) return {
|
|
14
|
+
kind: "link",
|
|
15
|
+
label: action.label,
|
|
16
|
+
icon: action.icon,
|
|
17
|
+
to: action.to,
|
|
18
|
+
destructive: action.destructive
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
kind: "action",
|
|
22
|
+
label: action.label,
|
|
23
|
+
icon: action.icon,
|
|
24
|
+
onSelect: action.onSelect ?? (() => {}),
|
|
25
|
+
disabled: action.disabled,
|
|
26
|
+
destructive: action.destructive
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Bridges a toolbar action (icon component, `onClick`/`href`) onto the shared
|
|
30
|
+
* `RowAction` shape. Typed structurally so any toolbar action shape matching
|
|
31
|
+
* these fields is assignable. */
|
|
32
|
+
function toolbarActionToRowAction(action) {
|
|
33
|
+
const Icon = action.icon;
|
|
34
|
+
return {
|
|
35
|
+
key: action.key,
|
|
36
|
+
label: action.label,
|
|
37
|
+
icon: /* @__PURE__ */ jsx(Icon, {}),
|
|
38
|
+
onSelect: action.onClick,
|
|
39
|
+
to: action.href,
|
|
40
|
+
disabled: action.disabled,
|
|
41
|
+
destructive: action.key === "delete" || action.key === "destroy"
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Assembles a row's context menu in the shared order: leading link(s) → middle
|
|
45
|
+
* actions → extra items → Copy submenu → trailing (destructive) actions last. */
|
|
46
|
+
function assembleRowMenu({ lead = [], actions, exclude, order, trailing = ["delete"], extra = [], copy }) {
|
|
47
|
+
const trailingSet = new Set(trailing);
|
|
48
|
+
const excludeSet = new Set(exclude);
|
|
49
|
+
let middle = actions.filter((a) => !trailingSet.has(a.key) && !excludeSet.has(a.key));
|
|
50
|
+
if (order) {
|
|
51
|
+
const byKey = new Map(middle.map((a) => [a.key, a]));
|
|
52
|
+
middle = order.map((key) => byKey.get(key)).filter((a) => a !== void 0);
|
|
53
|
+
}
|
|
54
|
+
const trailingItems = trailing.map((key) => actions.find((a) => a.key === key)).filter((a) => a !== void 0);
|
|
55
|
+
return [
|
|
56
|
+
...lead,
|
|
57
|
+
...middle.map(toRowMenuItem),
|
|
58
|
+
...extra,
|
|
59
|
+
...copy ? [copy] : [],
|
|
60
|
+
...trailingItems.map(toRowMenuItem)
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
function useRowMenu() {
|
|
64
|
+
const [menu, setMenu] = useState(null);
|
|
65
|
+
const onCloseRef = useRef(void 0);
|
|
66
|
+
return {
|
|
67
|
+
openMenu: useCallback((x, y, items, onClose) => {
|
|
68
|
+
if (items.length > 0) {
|
|
69
|
+
onCloseRef.current?.();
|
|
70
|
+
onCloseRef.current = onClose;
|
|
71
|
+
setMenu({
|
|
72
|
+
x,
|
|
73
|
+
y,
|
|
74
|
+
items
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}, []),
|
|
78
|
+
contextMenu: /* @__PURE__ */ jsx(RowContextMenu, {
|
|
79
|
+
state: menu,
|
|
80
|
+
onClose: useCallback(() => {
|
|
81
|
+
onCloseRef.current?.();
|
|
82
|
+
onCloseRef.current = void 0;
|
|
83
|
+
setMenu(null);
|
|
84
|
+
}, [])
|
|
85
|
+
})
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** The full-screen menu overlay drops CSS :hover on the triggering row; wrap openMenu here to substitute a JS-driven `active` highlight. */
|
|
89
|
+
function useRowMenuActive(openMenu) {
|
|
90
|
+
const [active, setActive] = useState(false);
|
|
91
|
+
return {
|
|
92
|
+
active,
|
|
93
|
+
openMenu: useCallback((x, y, items) => {
|
|
94
|
+
setActive(true);
|
|
95
|
+
openMenu(x, y, items, () => setActive(false));
|
|
96
|
+
}, [openMenu])
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function rowContextMenuProps(openMenu, items) {
|
|
100
|
+
return { onContextMenu: (e) => {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
if (items.length > 0) e.stopPropagation();
|
|
103
|
+
openMenu(e.clientX, e.clientY, items);
|
|
104
|
+
} };
|
|
105
|
+
}
|
|
106
|
+
function RowContextMenu({ state, onClose }) {
|
|
107
|
+
const ref = useRef(null);
|
|
108
|
+
const [pos, setPos] = useState(null);
|
|
109
|
+
useLayoutEffect(() => {
|
|
110
|
+
if (!state || !ref.current) {
|
|
111
|
+
setPos(null);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const { width, height } = ref.current.getBoundingClientRect();
|
|
115
|
+
const pad = 8;
|
|
116
|
+
const top = height > window.innerHeight - pad - state.y && state.y - height - pad >= 0 ? state.y - height : Math.min(state.y, window.innerHeight - height - pad);
|
|
117
|
+
setPos({
|
|
118
|
+
left: Math.max(pad, Math.min(state.x, window.innerWidth - width - pad)),
|
|
119
|
+
top: Math.max(pad, top)
|
|
120
|
+
});
|
|
121
|
+
}, [state]);
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!state) return;
|
|
124
|
+
const close = () => onClose();
|
|
125
|
+
const onKey = (e) => {
|
|
126
|
+
if (e.key === "Escape") onClose();
|
|
127
|
+
};
|
|
128
|
+
window.addEventListener("scroll", close, true);
|
|
129
|
+
window.addEventListener("resize", close);
|
|
130
|
+
window.addEventListener("keydown", onKey);
|
|
131
|
+
return () => {
|
|
132
|
+
window.removeEventListener("scroll", close, true);
|
|
133
|
+
window.removeEventListener("resize", close);
|
|
134
|
+
window.removeEventListener("keydown", onKey);
|
|
135
|
+
};
|
|
136
|
+
}, [state, onClose]);
|
|
137
|
+
if (!state) return null;
|
|
138
|
+
const side = (pos?.left ?? state.x) > window.innerWidth * .6 ? "left" : "right";
|
|
139
|
+
return createPortal(/* @__PURE__ */ jsx("div", {
|
|
140
|
+
className: "fixed inset-0 z-50",
|
|
141
|
+
onClick: onClose,
|
|
142
|
+
onContextMenu: (e) => {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
if (ref.current?.contains(e.target)) {
|
|
145
|
+
onClose();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const { clientX, clientY } = e;
|
|
149
|
+
const overlay = e.currentTarget;
|
|
150
|
+
overlay.style.pointerEvents = "none";
|
|
151
|
+
const under = document.elementFromPoint(clientX, clientY);
|
|
152
|
+
overlay.style.pointerEvents = "";
|
|
153
|
+
onClose();
|
|
154
|
+
under?.dispatchEvent(new MouseEvent("contextmenu", {
|
|
155
|
+
bubbles: true,
|
|
156
|
+
cancelable: true,
|
|
157
|
+
clientX,
|
|
158
|
+
clientY
|
|
159
|
+
}));
|
|
160
|
+
},
|
|
161
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
162
|
+
ref,
|
|
163
|
+
role: "menu",
|
|
164
|
+
className: "absolute min-w-44 rounded-lg border border-(--theme-border) bg-(--theme-panel-bg) py-1 shadow-lg backdrop-blur-xl",
|
|
165
|
+
style: {
|
|
166
|
+
left: pos?.left ?? state.x,
|
|
167
|
+
top: pos?.top ?? state.y,
|
|
168
|
+
visibility: pos ? "visible" : "hidden"
|
|
169
|
+
},
|
|
170
|
+
onClick: (e) => e.stopPropagation(),
|
|
171
|
+
children: /* @__PURE__ */ jsx(MenuItemList, {
|
|
172
|
+
items: state.items,
|
|
173
|
+
onClose,
|
|
174
|
+
side
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
}), document.body);
|
|
178
|
+
}
|
|
179
|
+
var MENU_ITEM_CLASSES = "flex w-full items-center gap-2.5 px-3 py-1.5 text-left text-[13px] hover:bg-(--theme-muted-panel-bg) transition-colors cursor-pointer [&>svg]:h-4 [&>svg]:w-4 [&>svg]:shrink-0";
|
|
180
|
+
function MenuItemList({ items, onClose, side }) {
|
|
181
|
+
return items.map((item, i) => item.kind === "divider" ? /* @__PURE__ */ jsx("div", {
|
|
182
|
+
"aria-hidden": "true",
|
|
183
|
+
className: "my-1 h-px bg-(--theme-border)"
|
|
184
|
+
}, `divider-${i}`) : /* @__PURE__ */ jsx(MenuRow, {
|
|
185
|
+
item,
|
|
186
|
+
onClose,
|
|
187
|
+
side
|
|
188
|
+
}, `${item.label}-${i}`));
|
|
189
|
+
}
|
|
190
|
+
function MenuRow({ item, onClose, side }) {
|
|
191
|
+
const Link = useLink();
|
|
192
|
+
const notify = useNotify();
|
|
193
|
+
const { list: t } = useStrings();
|
|
194
|
+
const [openSub, setOpenSub] = useState(false);
|
|
195
|
+
const closeTimer = useRef(void 0);
|
|
196
|
+
if (item.kind === "submenu") {
|
|
197
|
+
const open = () => {
|
|
198
|
+
clearTimeout(closeTimer.current);
|
|
199
|
+
setOpenSub(true);
|
|
200
|
+
};
|
|
201
|
+
const close = () => {
|
|
202
|
+
closeTimer.current = setTimeout(() => setOpenSub(false), 160);
|
|
203
|
+
};
|
|
204
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
205
|
+
className: "relative",
|
|
206
|
+
onMouseEnter: open,
|
|
207
|
+
onMouseLeave: close,
|
|
208
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
209
|
+
className: cx(MENU_ITEM_CLASSES, "justify-between text-(--theme-app)"),
|
|
210
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
211
|
+
className: "flex items-center gap-2.5",
|
|
212
|
+
children: [item.icon, item.label]
|
|
213
|
+
}), /* @__PURE__ */ jsx(ChevronRightIcon, { className: "h-3 w-3 shrink-0 opacity-60" })]
|
|
214
|
+
}), openSub && /* @__PURE__ */ jsx("div", {
|
|
215
|
+
className: cx("absolute -top-1 z-10 min-w-44 rounded-lg border border-(--theme-border) bg-(--theme-panel-bg) py-1 shadow-lg", side === "left" ? "right-full" : "left-full"),
|
|
216
|
+
children: /* @__PURE__ */ jsx(MenuItemList, {
|
|
217
|
+
items: item.items,
|
|
218
|
+
onClose,
|
|
219
|
+
side
|
|
220
|
+
})
|
|
221
|
+
})]
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if (item.kind === "link") return /* @__PURE__ */ jsxs(Link, {
|
|
225
|
+
to: item.to,
|
|
226
|
+
onClick: onClose,
|
|
227
|
+
className: cx(MENU_ITEM_CLASSES, item.destructive ? "text-red-500" : "text-(--theme-app)"),
|
|
228
|
+
children: [item.icon, item.label]
|
|
229
|
+
});
|
|
230
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
231
|
+
type: "button",
|
|
232
|
+
disabled: item.disabled,
|
|
233
|
+
onClick: () => {
|
|
234
|
+
if (item.copy) {
|
|
235
|
+
const { text, label } = item.copy;
|
|
236
|
+
navigator.clipboard?.writeText(text).then(() => notify.success(t.copied(label)), () => notify.error(t.couldntCopy(label)));
|
|
237
|
+
}
|
|
238
|
+
item.onSelect?.();
|
|
239
|
+
onClose();
|
|
240
|
+
},
|
|
241
|
+
className: cx(MENU_ITEM_CLASSES, item.destructive ? "text-red-500" : "text-(--theme-app)", item.disabled && "opacity-40 cursor-not-allowed hover:bg-transparent"),
|
|
242
|
+
children: [
|
|
243
|
+
item.icon,
|
|
244
|
+
item.label,
|
|
245
|
+
item.selected && /* @__PURE__ */ jsx(CheckIcon, { className: "ml-auto opacity-80" })
|
|
246
|
+
]
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/** Builds the Copy submenu entries for whichever fields a row has, in stable
|
|
250
|
+
* order. The menu itself performs the copy and notifies; `t` is an optional
|
|
251
|
+
* host translator for the field labels (falls back to the provider defaults'
|
|
252
|
+
* English). */
|
|
253
|
+
function copySubmenu({ name, nameLabel, email, username, uid, id, url, extra, t }) {
|
|
254
|
+
const items = [
|
|
255
|
+
{
|
|
256
|
+
label: nameLabel ?? (t ? t("rowContextMenu.labelName") : "name"),
|
|
257
|
+
value: name,
|
|
258
|
+
icon: /* @__PURE__ */ jsx(CopyIcon, {})
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
label: t ? t("rowContextMenu.labelEmail") : "email",
|
|
262
|
+
value: email,
|
|
263
|
+
icon: /* @__PURE__ */ jsx(MailIcon, {})
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
label: t ? t("rowContextMenu.labelUsername") : "username",
|
|
267
|
+
value: username,
|
|
268
|
+
icon: /* @__PURE__ */ jsx(UserIcon, {})
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
label: t ? t("rowContextMenu.labelUid") : "UID",
|
|
272
|
+
value: uid === null || uid === void 0 ? null : String(uid),
|
|
273
|
+
icon: /* @__PURE__ */ jsx(KeyIcon, {})
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
label: t ? t("rowContextMenu.labelId") : "ID",
|
|
277
|
+
value: id,
|
|
278
|
+
icon: /* @__PURE__ */ jsx(IdCardIcon, {})
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
label: t ? t("rowContextMenu.labelUrl") : "URL",
|
|
282
|
+
value: url,
|
|
283
|
+
icon: /* @__PURE__ */ jsx(LinkIcon, {})
|
|
284
|
+
},
|
|
285
|
+
...(extra ?? []).map((e) => ({
|
|
286
|
+
label: e.label,
|
|
287
|
+
value: e.value,
|
|
288
|
+
icon: e.icon ?? /* @__PURE__ */ jsx(CopyIcon, {})
|
|
289
|
+
}))
|
|
290
|
+
].filter((f) => f.value).map((f) => ({
|
|
291
|
+
kind: "action",
|
|
292
|
+
label: t ? t("rowContextMenu.copy", { label: f.label }) : `Copy ${f.label}`,
|
|
293
|
+
icon: f.icon,
|
|
294
|
+
copy: {
|
|
295
|
+
text: f.value,
|
|
296
|
+
label: f.label
|
|
297
|
+
}
|
|
298
|
+
}));
|
|
299
|
+
return {
|
|
300
|
+
kind: "submenu",
|
|
301
|
+
label: t ? t("common.copy") : "Copy",
|
|
302
|
+
icon: /* @__PURE__ */ jsx(ClipboardIcon, {}),
|
|
303
|
+
items
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
/** Stays visible while active (menu open) so it doesn't vanish behind the overlay. */
|
|
307
|
+
function MoreButton({ onOpen, active, className }) {
|
|
308
|
+
const { list: t } = useStrings();
|
|
309
|
+
return /* @__PURE__ */ jsx("button", {
|
|
310
|
+
type: "button",
|
|
311
|
+
"aria-label": t.moreActions,
|
|
312
|
+
onClick: (e) => {
|
|
313
|
+
e.stopPropagation();
|
|
314
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
315
|
+
onOpen(r.right, r.bottom);
|
|
316
|
+
},
|
|
317
|
+
className: cx("rounded p-1 cursor-pointer text-(--theme-muted-text-color) transition-colors hover:bg-(--theme-muted-panel-bg) hover:text-(--theme-app) focus-visible:opacity-100", active ? "opacity-100" : "opacity-0 group-hover:opacity-100", className),
|
|
318
|
+
children: /* @__PURE__ */ jsx(MoreIcon, { className: "h-4 w-4" })
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
//#endregion
|
|
322
|
+
//#region src/list/ListTable.tsx
|
|
323
|
+
var NAME_TEXT = "font-medium text-[13px] leading-tight text-(--theme-app) truncate";
|
|
324
|
+
var SUBTITLE_TEXT = "text-xs leading-tight text-(--theme-muted-text-color) truncate";
|
|
325
|
+
/** table-auto + horizontal scroll; the name column has a min-width floor so it can't be crushed. */
|
|
326
|
+
var listTableProps = {
|
|
327
|
+
borderless: true,
|
|
328
|
+
panel: false,
|
|
329
|
+
rounded: false,
|
|
330
|
+
theadClassName: "text-(--theme-muted-text-color)",
|
|
331
|
+
tbodyClassName: "divide-y border-b border-(--theme-border) divide-(--theme-border)",
|
|
332
|
+
className: "table-auto w-full [&_td:first-child]:pl-4 [&_th:first-child]:pl-4 [&_td:last-child]:pr-4 [&_th:last-child]:pr-4"
|
|
333
|
+
};
|
|
334
|
+
/** Widths come only from the header row, so a windowed body cannot shift the
|
|
335
|
+
* columns as different content scrolls into view. */
|
|
336
|
+
var listTableFixedProps = {
|
|
337
|
+
...listTableProps,
|
|
338
|
+
className: listTableProps.className.replace("table-auto", "table-fixed")
|
|
339
|
+
};
|
|
340
|
+
function NameCell({ icon, name, subtitle, href }) {
|
|
341
|
+
const Link = useLink();
|
|
342
|
+
const inner = /* @__PURE__ */ jsxs(Fragment, { children: [icon, /* @__PURE__ */ jsxs("div", {
|
|
343
|
+
className: "min-w-0",
|
|
344
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
345
|
+
className: NAME_TEXT,
|
|
346
|
+
children: name
|
|
347
|
+
}), subtitle && /* @__PURE__ */ jsx("div", {
|
|
348
|
+
className: SUBTITLE_TEXT,
|
|
349
|
+
children: subtitle
|
|
350
|
+
})]
|
|
351
|
+
})] });
|
|
352
|
+
return /* @__PURE__ */ jsx(Table.Item, {
|
|
353
|
+
className: "py-2.5",
|
|
354
|
+
children: href ? /* @__PURE__ */ jsx(Link, {
|
|
355
|
+
to: href,
|
|
356
|
+
className: "flex items-center gap-2.5 min-w-0 cursor-pointer",
|
|
357
|
+
children: inner
|
|
358
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
359
|
+
className: "flex items-center gap-2.5 min-w-0",
|
|
360
|
+
children: inner
|
|
361
|
+
})
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
function GroupRow({ label, count, colSpan }) {
|
|
365
|
+
return /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsxs("td", {
|
|
366
|
+
colSpan,
|
|
367
|
+
className: "bg-(--theme-muted-panel-bg) py-1.5 text-xs font-medium",
|
|
368
|
+
children: [
|
|
369
|
+
/* @__PURE__ */ jsx("span", {
|
|
370
|
+
className: "text-(--theme-app)",
|
|
371
|
+
children: label
|
|
372
|
+
}),
|
|
373
|
+
/* @__PURE__ */ jsx("span", {
|
|
374
|
+
className: "mx-1.5 opacity-50",
|
|
375
|
+
children: "·"
|
|
376
|
+
}),
|
|
377
|
+
/* @__PURE__ */ jsx("span", {
|
|
378
|
+
className: "text-(--theme-muted-text-color)",
|
|
379
|
+
children: count
|
|
380
|
+
})
|
|
381
|
+
]
|
|
382
|
+
}) });
|
|
383
|
+
}
|
|
384
|
+
function ListError({ error, noun = "data" }) {
|
|
385
|
+
const { list: t } = useStrings();
|
|
386
|
+
const message = error && typeof error === "object" && "message" in error ? String(error.message) : void 0;
|
|
387
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
388
|
+
className: "flex flex-col items-center justify-center gap-3 py-20 text-center",
|
|
389
|
+
children: [/* @__PURE__ */ jsx(ErrorIcon, { className: "h-10 w-10 text-red-400 opacity-80" }), /* @__PURE__ */ jsxs("div", {
|
|
390
|
+
className: "space-y-1",
|
|
391
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
392
|
+
className: "text-sm font-medium text-(--theme-app)",
|
|
393
|
+
children: t.couldNotLoad(noun)
|
|
394
|
+
}), message && /* @__PURE__ */ jsx("p", {
|
|
395
|
+
className: "max-w-md text-xs text-(--theme-muted-text-color)",
|
|
396
|
+
children: message
|
|
397
|
+
})]
|
|
398
|
+
})]
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
function ListSkeleton({ rows = 6 }) {
|
|
402
|
+
return /* @__PURE__ */ jsx("div", {
|
|
403
|
+
className: "divide-y border-b border-(--theme-border) divide-(--theme-border)",
|
|
404
|
+
children: Array.from({ length: rows }).map((_, i) => /* @__PURE__ */ jsxs("div", {
|
|
405
|
+
className: "flex items-center gap-3 px-4 py-2.5 animate-pulse",
|
|
406
|
+
children: [
|
|
407
|
+
/* @__PURE__ */ jsx("div", { className: "h-5 w-5 rounded bg-(--theme-muted-panel-bg)" }),
|
|
408
|
+
/* @__PURE__ */ jsxs("div", {
|
|
409
|
+
className: "space-y-1.5",
|
|
410
|
+
children: [/* @__PURE__ */ jsx("div", { className: "h-3 w-40 rounded bg-(--theme-muted-panel-bg)" }), /* @__PURE__ */ jsx("div", { className: "h-2.5 w-24 rounded bg-(--theme-muted-panel-bg)" })]
|
|
411
|
+
}),
|
|
412
|
+
/* @__PURE__ */ jsx("div", { className: "ml-auto h-3 w-20 rounded bg-(--theme-muted-panel-bg)" })
|
|
413
|
+
]
|
|
414
|
+
}, i))
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
/** Prevents the name column from being crushed under table-auto layout during horizontal overflow. */
|
|
418
|
+
var NAME_MIN_WIDTH_PX = 200;
|
|
419
|
+
/** Trailing cell geometry: icon-button size + gap + padding, sized to fit exactly its pinned buttons. */
|
|
420
|
+
var ACTION_BUTTON_PX = 24;
|
|
421
|
+
var ACTION_GAP_PX = 2;
|
|
422
|
+
var ACTIONS_CELL_PAD_PX = 28;
|
|
423
|
+
/** Width the trailing actions column reserves for `count` always-visible
|
|
424
|
+
* buttons plus the hover `⋯` trigger. */
|
|
425
|
+
function actionsColumnWidth(count) {
|
|
426
|
+
const buttons = count + 1;
|
|
427
|
+
return buttons * ACTION_BUTTON_PX + (buttons - 1) * ACTION_GAP_PX + ACTIONS_CELL_PAD_PX;
|
|
428
|
+
}
|
|
429
|
+
/** Shared leading selection checkbox for multi-select rows: stops the row-click
|
|
430
|
+
* and suppresses shift-click text selection. Wrap in <Table.Item> for `leading`. */
|
|
431
|
+
function RowSelectCheckbox({ checked, label, onToggle }) {
|
|
432
|
+
return /* @__PURE__ */ jsx("input", {
|
|
433
|
+
type: "checkbox",
|
|
434
|
+
"aria-label": label,
|
|
435
|
+
checked,
|
|
436
|
+
onChange: () => {},
|
|
437
|
+
onClick: (e) => {
|
|
438
|
+
e.stopPropagation();
|
|
439
|
+
onToggle(e);
|
|
440
|
+
},
|
|
441
|
+
onMouseDown: (e) => {
|
|
442
|
+
if (e.shiftKey) e.preventDefault();
|
|
443
|
+
},
|
|
444
|
+
className: "h-4 w-4 shrink-0 rounded border-(--theme-border) text-blue-500 focus:ring-blue-500 focus:ring-offset-0 cursor-pointer"
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
/** Sizes to fit pinned buttons + ⋯; below ~44rem only ⋯ remains. Pass a view's
|
|
448
|
+
* chrome (structurally: showActions + actionsWidth) or actionCount. */
|
|
449
|
+
function ListActionsHeader({ view, actionCount = 0, className }) {
|
|
450
|
+
if (view && !view.showActions) return null;
|
|
451
|
+
const width = view ? view.actionsWidth : actionsColumnWidth(actionCount);
|
|
452
|
+
return /* @__PURE__ */ jsx(Table.Header, {
|
|
453
|
+
caps: false,
|
|
454
|
+
style: {
|
|
455
|
+
width,
|
|
456
|
+
minWidth: width
|
|
457
|
+
},
|
|
458
|
+
className: cx("py-2 text-right @max-[43rem]:w-[52px]! @max-[43rem]:min-w-[52px]!", className),
|
|
459
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
460
|
+
className: "sr-only",
|
|
461
|
+
children: "Actions"
|
|
462
|
+
})
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
ListActionsHeader.isTableHeader = true;
|
|
466
|
+
//#endregion
|
|
467
|
+
//#region src/list/listView.tsx
|
|
468
|
+
var PRIORITY_SHOW_CLASS = {
|
|
469
|
+
low: "hidden @[52rem]:table-cell",
|
|
470
|
+
medium: "hidden @[36rem]:table-cell"
|
|
471
|
+
};
|
|
472
|
+
function columnResponsiveClass(col) {
|
|
473
|
+
return col.priority ? PRIORITY_SHOW_CLASS[col.priority] : "";
|
|
474
|
+
}
|
|
475
|
+
var DEFAULT_ORDER = "__default";
|
|
476
|
+
var FAVORITE_ORDER = "__favorite";
|
|
477
|
+
/** Persisted grouping + column-visibility + field-filter state for a list page,
|
|
478
|
+
* driving the shared Display/Filter menus. */
|
|
479
|
+
function useListView({ storageKey, sharedFiltersKey, columns, groupBys = [], defaultGroupBy, orderBys = [], defaultOrderBy, facets = [], actions = [], actionCount, defaultShowColumnHeaders = false, defaultShowActions = true, isFavorite, isOn, createdAt }) {
|
|
480
|
+
const t = useStrings().list;
|
|
481
|
+
const tiebreak = orderBys[0]?.compare;
|
|
482
|
+
const effectiveOrderBys = useMemo(() => {
|
|
483
|
+
if (!isFavorite) return orderBys;
|
|
484
|
+
const favoriteRank = (row) => isFavorite(row) ? 0 : 1;
|
|
485
|
+
const onRank = (row) => isOn?.(row) ? 0 : 1;
|
|
486
|
+
const createdMs = (row) => {
|
|
487
|
+
const v = createdAt?.(row);
|
|
488
|
+
return v === null || v === void 0 || Number.isNaN(v) ? 0 : v;
|
|
489
|
+
};
|
|
490
|
+
const newestFirst = (a, b) => createdMs(b) - createdMs(a);
|
|
491
|
+
return [...[{
|
|
492
|
+
value: DEFAULT_ORDER,
|
|
493
|
+
label: t.orderDefault,
|
|
494
|
+
compare: (a, b) => onRank(a) - onRank(b) || favoriteRank(a) - favoriteRank(b) || newestFirst(a, b) || (tiebreak?.(a, b) ?? 0)
|
|
495
|
+
}, {
|
|
496
|
+
value: FAVORITE_ORDER,
|
|
497
|
+
label: t.orderFavorites,
|
|
498
|
+
compare: (a, b) => favoriteRank(a) - favoriteRank(b) || (tiebreak?.(a, b) ?? 0)
|
|
499
|
+
}], ...orderBys];
|
|
500
|
+
}, [
|
|
501
|
+
orderBys,
|
|
502
|
+
isFavorite,
|
|
503
|
+
isOn,
|
|
504
|
+
createdAt,
|
|
505
|
+
tiebreak,
|
|
506
|
+
t
|
|
507
|
+
]);
|
|
508
|
+
const defaultGroupByValue = defaultGroupBy ?? groupBys[0]?.value ?? "";
|
|
509
|
+
const defaultOrderByValue = isFavorite ? DEFAULT_ORDER : defaultOrderBy ?? orderBys[0]?.value ?? "";
|
|
510
|
+
const defaultHiddenKeys = useMemo(() => columns.filter((c) => c.defaultHidden && !c.alwaysVisible).map((c) => c.key), [columns]);
|
|
511
|
+
const defaultPinnedKeys = useMemo(() => actions.filter((a) => a.defaultPinned).map((a) => a.key), [actions]);
|
|
512
|
+
const [hiddenKeys, setHiddenKeys] = useLocalStorage(`${storageKey}:hiddenColumns`, defaultHiddenKeys);
|
|
513
|
+
const [pinnedKeys, setPinnedKeys] = useLocalStorage(`${storageKey}:pinnedActions`, defaultPinnedKeys);
|
|
514
|
+
const [groupBy, setGroupBy] = useLocalStorage(`${storageKey}:groupBy`, defaultGroupByValue);
|
|
515
|
+
const [showColumnHeaders, setShowColumnHeaders] = useLocalStorage(`${storageKey}:showColumnHeaders`, defaultShowColumnHeaders);
|
|
516
|
+
const [showActions, setShowActions] = useLocalStorage(`${storageKey}:showActions`, defaultShowActions);
|
|
517
|
+
const [alwaysShowActions, setAlwaysShowActions] = useLocalStorage(`${storageKey}:alwaysShowActions`, false);
|
|
518
|
+
const [orderBy, setOrderBy] = useLocalStorage(`${storageKey}:orderBy`, defaultOrderByValue);
|
|
519
|
+
const [orderDir, setOrderDir] = useLocalStorage(`${storageKey}:orderDir`, "asc");
|
|
520
|
+
const [localFilters, setLocalFilters] = useLocalStorage(`${storageKey}:filters`, {});
|
|
521
|
+
const [sharedFilters, setSharedFilters] = useLocalStorage(`${sharedFiltersKey ?? storageKey}:sharedFilters`, {});
|
|
522
|
+
const sharedKeys = useMemo(() => new Set(facets.filter((f) => f.shared).map((f) => f.key)), [facets]);
|
|
523
|
+
const filters = useMemo(() => {
|
|
524
|
+
if (sharedKeys.size === 0) return localFilters;
|
|
525
|
+
const merged = {};
|
|
526
|
+
for (const [key, values] of Object.entries(localFilters)) if (!sharedKeys.has(key)) merged[key] = values;
|
|
527
|
+
for (const key of sharedKeys) {
|
|
528
|
+
const values = sharedFilters[key];
|
|
529
|
+
if (values?.length) merged[key] = values;
|
|
530
|
+
}
|
|
531
|
+
return merged;
|
|
532
|
+
}, [
|
|
533
|
+
localFilters,
|
|
534
|
+
sharedFilters,
|
|
535
|
+
sharedKeys
|
|
536
|
+
]);
|
|
537
|
+
const pinnedCount = actionCount ?? pinnedKeys.filter((key) => actions.some((action) => action.key === key)).length;
|
|
538
|
+
const effectiveShowActions = showActions || alwaysShowActions;
|
|
539
|
+
const actionsWidth = effectiveShowActions ? actionsColumnWidth(pinnedCount) : 0;
|
|
540
|
+
const isVisible = useCallback((key) => {
|
|
541
|
+
const col = columns.find((c) => c.key === key);
|
|
542
|
+
return Boolean(col?.alwaysVisible) || !hiddenKeys.includes(key);
|
|
543
|
+
}, [columns, hiddenKeys]);
|
|
544
|
+
const visibleColumns = useMemo(() => columns.filter((c) => !c.inline && (c.alwaysVisible || !hiddenKeys.includes(c.key))), [columns, hiddenKeys]);
|
|
545
|
+
const columnClasses = useMemo(() => visibleColumns.map(columnResponsiveClass), [visibleColumns]);
|
|
546
|
+
const toggleColumn = useCallback((key) => setHiddenKeys((prev) => prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key]), [setHiddenKeys]);
|
|
547
|
+
const setFacetValues = useCallback((key, values) => {
|
|
548
|
+
const update = (prev) => {
|
|
549
|
+
const next = { ...prev };
|
|
550
|
+
if (values.length) next[key] = values;
|
|
551
|
+
else delete next[key];
|
|
552
|
+
return next;
|
|
553
|
+
};
|
|
554
|
+
if (sharedKeys.has(key)) setSharedFilters(update);
|
|
555
|
+
else setLocalFilters(update);
|
|
556
|
+
}, [
|
|
557
|
+
setLocalFilters,
|
|
558
|
+
setSharedFilters,
|
|
559
|
+
sharedKeys
|
|
560
|
+
]);
|
|
561
|
+
const clearFilters = useCallback(() => {
|
|
562
|
+
setLocalFilters({});
|
|
563
|
+
if (sharedKeys.size > 0) setSharedFilters((prev) => {
|
|
564
|
+
const next = { ...prev };
|
|
565
|
+
for (const key of sharedKeys) delete next[key];
|
|
566
|
+
return next;
|
|
567
|
+
});
|
|
568
|
+
}, [
|
|
569
|
+
setLocalFilters,
|
|
570
|
+
setSharedFilters,
|
|
571
|
+
sharedKeys
|
|
572
|
+
]);
|
|
573
|
+
const isPinned = useCallback((key) => pinnedKeys.includes(key), [pinnedKeys]);
|
|
574
|
+
const togglePinned = useCallback((key) => setPinnedKeys((prev) => prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key]), [setPinnedKeys]);
|
|
575
|
+
const toggleColumnHeaders = useCallback(() => setShowColumnHeaders((prev) => !prev), [setShowColumnHeaders]);
|
|
576
|
+
const toggleShowActions = useCallback(() => {
|
|
577
|
+
if (effectiveShowActions) {
|
|
578
|
+
setShowActions(false);
|
|
579
|
+
setAlwaysShowActions(false);
|
|
580
|
+
} else setShowActions(true);
|
|
581
|
+
}, [
|
|
582
|
+
effectiveShowActions,
|
|
583
|
+
setShowActions,
|
|
584
|
+
setAlwaysShowActions
|
|
585
|
+
]);
|
|
586
|
+
const toggleAlwaysShowActions = useCallback(() => {
|
|
587
|
+
const next = !alwaysShowActions;
|
|
588
|
+
setAlwaysShowActions(next);
|
|
589
|
+
if (next) setShowActions(true);
|
|
590
|
+
}, [
|
|
591
|
+
alwaysShowActions,
|
|
592
|
+
setAlwaysShowActions,
|
|
593
|
+
setShowActions
|
|
594
|
+
]);
|
|
595
|
+
const toggleOrderDir = useCallback(() => setOrderDir((d) => d === "asc" ? "desc" : "asc"), [setOrderDir]);
|
|
596
|
+
const applyOrder = useCallback((rows) => {
|
|
597
|
+
const opt = effectiveOrderBys.find((o) => o.value === orderBy);
|
|
598
|
+
if (!opt) return rows;
|
|
599
|
+
const sign = orderDir === "asc" ? 1 : -1;
|
|
600
|
+
return [...rows].sort((a, b) => opt.compare(a, b) * sign);
|
|
601
|
+
}, [
|
|
602
|
+
effectiveOrderBys,
|
|
603
|
+
orderBy,
|
|
604
|
+
orderDir
|
|
605
|
+
]);
|
|
606
|
+
const sameKeys = (a, b) => a.length === b.length && a.every((k) => b.includes(k));
|
|
607
|
+
const isDisplayDefault = groupBy === defaultGroupByValue && orderBy === defaultOrderByValue && orderDir === "asc" && showColumnHeaders === defaultShowColumnHeaders && showActions === defaultShowActions && alwaysShowActions === false && sameKeys(hiddenKeys, defaultHiddenKeys) && sameKeys(pinnedKeys, defaultPinnedKeys);
|
|
608
|
+
const resetDisplay = useCallback(() => {
|
|
609
|
+
setGroupBy(defaultGroupByValue);
|
|
610
|
+
setOrderBy(defaultOrderByValue);
|
|
611
|
+
setOrderDir("asc");
|
|
612
|
+
setShowColumnHeaders(defaultShowColumnHeaders);
|
|
613
|
+
setShowActions(defaultShowActions);
|
|
614
|
+
setAlwaysShowActions(false);
|
|
615
|
+
setHiddenKeys(defaultHiddenKeys);
|
|
616
|
+
setPinnedKeys(defaultPinnedKeys);
|
|
617
|
+
}, [
|
|
618
|
+
setGroupBy,
|
|
619
|
+
setOrderBy,
|
|
620
|
+
setOrderDir,
|
|
621
|
+
defaultOrderByValue,
|
|
622
|
+
setShowColumnHeaders,
|
|
623
|
+
setShowActions,
|
|
624
|
+
setAlwaysShowActions,
|
|
625
|
+
setHiddenKeys,
|
|
626
|
+
setPinnedKeys,
|
|
627
|
+
defaultGroupByValue,
|
|
628
|
+
defaultHiddenKeys,
|
|
629
|
+
defaultPinnedKeys,
|
|
630
|
+
defaultShowColumnHeaders,
|
|
631
|
+
defaultShowActions
|
|
632
|
+
]);
|
|
633
|
+
return {
|
|
634
|
+
columns,
|
|
635
|
+
visibleColumns,
|
|
636
|
+
columnClasses,
|
|
637
|
+
isVisible,
|
|
638
|
+
pinnedCount,
|
|
639
|
+
actionsWidth,
|
|
640
|
+
toggleColumn,
|
|
641
|
+
groupBys,
|
|
642
|
+
groupBy,
|
|
643
|
+
setGroupBy,
|
|
644
|
+
orderBys: effectiveOrderBys,
|
|
645
|
+
orderBy,
|
|
646
|
+
setOrderBy,
|
|
647
|
+
orderDir,
|
|
648
|
+
toggleOrderDir,
|
|
649
|
+
applyOrder,
|
|
650
|
+
facets,
|
|
651
|
+
filters,
|
|
652
|
+
setFacetValues,
|
|
653
|
+
clearFilters,
|
|
654
|
+
activeFilterCount: useMemo(() => Object.values(filters).filter((v) => v && v.length > 0).length, [filters]),
|
|
655
|
+
applyFilters: useCallback((rows) => rows.filter((row) => facets.every((f) => {
|
|
656
|
+
const selected = filters[f.key];
|
|
657
|
+
return !selected || selected.length === 0 ? true : f.matches(row, selected);
|
|
658
|
+
})), [facets, filters]),
|
|
659
|
+
actions,
|
|
660
|
+
isPinned,
|
|
661
|
+
togglePinned,
|
|
662
|
+
showColumnHeaders,
|
|
663
|
+
toggleColumnHeaders,
|
|
664
|
+
showActions: effectiveShowActions,
|
|
665
|
+
toggleShowActions,
|
|
666
|
+
alwaysShowActions,
|
|
667
|
+
toggleAlwaysShowActions,
|
|
668
|
+
isDisplayDefault,
|
|
669
|
+
resetDisplay
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
//#endregion
|
|
673
|
+
//#region src/list/textEntry.ts
|
|
674
|
+
/** Guards list-page shortcuts from firing inside inputs, terminals, editors, and dialogs. */
|
|
675
|
+
function isTextEntryContext(target) {
|
|
676
|
+
const el = target;
|
|
677
|
+
if (!el) return false;
|
|
678
|
+
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable) return true;
|
|
679
|
+
return Boolean(el.closest?.(".xterm, .monaco-editor, .cm-editor, [role=\"dialog\"]"));
|
|
680
|
+
}
|
|
681
|
+
//#endregion
|
|
682
|
+
//#region src/list/ListViewControls.tsx
|
|
683
|
+
/** Shared chrome for the toolbar control buttons (Filter, Display, Search) so
|
|
684
|
+
* they read as a consistent button cluster — matched by ListSearchControl. */
|
|
685
|
+
var listControlButtonClasses = "relative flex h-7 w-7 items-center justify-center rounded-md border border-(--theme-border) text-(--theme-muted-text-color) hover:bg-(--theme-muted-panel-bg) hover:text-(--theme-app) transition-colors";
|
|
686
|
+
var FLYOUT = "min-w-44 rounded-lg border border-(--theme-border) bg-(--theme-app-bg) py-1 shadow-lg";
|
|
687
|
+
/** Toggle pill shared by the Display menu's column + pinned-action pickers. */
|
|
688
|
+
function pillClass(on) {
|
|
689
|
+
return cx("whitespace-nowrap rounded-full border px-2.5 py-1 text-[12px] transition-colors", on ? "border-transparent bg-(--theme-muted-panel-bg) text-(--theme-app)" : "border-(--theme-border) text-(--theme-muted-text-color) hover:bg-(--theme-muted-panel-bg) hover:text-(--theme-app)");
|
|
690
|
+
}
|
|
691
|
+
var SECTION_LABEL = "mb-2 text-[11px] font-medium uppercase tracking-wide text-(--theme-muted-text-color)";
|
|
692
|
+
/** Portals into .ds-root so the panel escapes the toolbar's overflow clipping. */
|
|
693
|
+
function AnchoredPopover({ icon, label, active, shortcut, panelClassName, children }) {
|
|
694
|
+
const btnRef = useRef(null);
|
|
695
|
+
const panelRef = useRef(null);
|
|
696
|
+
const [pos, setPos] = useState(null);
|
|
697
|
+
const close = useCallback(() => setPos(null), []);
|
|
698
|
+
const open = useCallback(() => {
|
|
699
|
+
const r = btnRef.current?.getBoundingClientRect();
|
|
700
|
+
if (r) setPos({
|
|
701
|
+
right: Math.max(8, window.innerWidth - r.right),
|
|
702
|
+
top: r.bottom + 4
|
|
703
|
+
});
|
|
704
|
+
}, []);
|
|
705
|
+
useEffect(() => {
|
|
706
|
+
if (!pos) return;
|
|
707
|
+
const onKey = (e) => {
|
|
708
|
+
if (e.key === "Escape") close();
|
|
709
|
+
};
|
|
710
|
+
const onScroll = (e) => {
|
|
711
|
+
if (e.target instanceof Node && panelRef.current?.contains(e.target)) return;
|
|
712
|
+
close();
|
|
713
|
+
};
|
|
714
|
+
window.addEventListener("keydown", onKey);
|
|
715
|
+
window.addEventListener("scroll", onScroll, true);
|
|
716
|
+
window.addEventListener("resize", close);
|
|
717
|
+
return () => {
|
|
718
|
+
window.removeEventListener("keydown", onKey);
|
|
719
|
+
window.removeEventListener("scroll", onScroll, true);
|
|
720
|
+
window.removeEventListener("resize", close);
|
|
721
|
+
};
|
|
722
|
+
}, [pos, close]);
|
|
723
|
+
useEffect(() => {
|
|
724
|
+
if (!shortcut) return;
|
|
725
|
+
const onKey = (e) => {
|
|
726
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
727
|
+
if (e.key.toLowerCase() !== shortcut.toLowerCase()) return;
|
|
728
|
+
if (isTextEntryContext(e.target)) return;
|
|
729
|
+
e.preventDefault();
|
|
730
|
+
open();
|
|
731
|
+
};
|
|
732
|
+
window.addEventListener("keydown", onKey);
|
|
733
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
734
|
+
}, [shortcut, open]);
|
|
735
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("button", {
|
|
736
|
+
ref: btnRef,
|
|
737
|
+
type: "button",
|
|
738
|
+
onClick: () => pos ? close() : open(),
|
|
739
|
+
"aria-label": label,
|
|
740
|
+
"data-tooltip-id": TOOLTIP_ID,
|
|
741
|
+
"data-tooltip-content": label,
|
|
742
|
+
"data-tooltip-shortcut": shortcut ? shortcut.toUpperCase() : void 0,
|
|
743
|
+
className: cx(listControlButtonClasses, active && "border-(--theme-link) text-(--theme-app)"),
|
|
744
|
+
children: [icon, active && /* @__PURE__ */ jsx("span", { className: "absolute right-0 top-0 h-1.5 w-1.5 rounded-full bg-(--theme-link)" })]
|
|
745
|
+
}), pos && createPortal(/* @__PURE__ */ jsx("div", {
|
|
746
|
+
className: "fixed inset-0 z-50",
|
|
747
|
+
onMouseDown: close,
|
|
748
|
+
onContextMenu: (e) => {
|
|
749
|
+
e.preventDefault();
|
|
750
|
+
close();
|
|
751
|
+
},
|
|
752
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
753
|
+
ref: panelRef,
|
|
754
|
+
role: "menu",
|
|
755
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
756
|
+
className: cx("absolute rounded-lg border border-(--theme-border) bg-(--theme-app-bg) shadow-lg", panelClassName),
|
|
757
|
+
style: {
|
|
758
|
+
right: pos.right,
|
|
759
|
+
top: pos.top
|
|
760
|
+
},
|
|
761
|
+
children
|
|
762
|
+
})
|
|
763
|
+
}), document.querySelector(".ds-root") ?? document.body)] });
|
|
764
|
+
}
|
|
765
|
+
/** Small on/off switch for the Display menu's boolean options. */
|
|
766
|
+
function Switch({ checked, onChange, label }) {
|
|
767
|
+
return /* @__PURE__ */ jsx("button", {
|
|
768
|
+
type: "button",
|
|
769
|
+
role: "switch",
|
|
770
|
+
"aria-checked": checked,
|
|
771
|
+
"aria-label": label,
|
|
772
|
+
onClick: onChange,
|
|
773
|
+
className: cx("relative inline-flex h-4 w-7 shrink-0 items-center rounded-full transition-colors", checked ? "bg-(--theme-link)" : "bg-(--theme-border)"),
|
|
774
|
+
children: /* @__PURE__ */ jsx("span", { className: cx("inline-block h-3 w-3 rounded-full bg-white transition-transform", checked ? "translate-x-3.5" : "translate-x-0.5") })
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
function ListDisplayMenu({ view, label }) {
|
|
778
|
+
const t = useStrings().list;
|
|
779
|
+
const toggleable = view.columns.filter((c) => !c.alwaysVisible);
|
|
780
|
+
const hasGrouping = view.groupBys.length > 0;
|
|
781
|
+
const orderDirLabel = view.orderDir === "asc" ? t.ascending : t.descending;
|
|
782
|
+
return /* @__PURE__ */ jsxs(AnchoredPopover, {
|
|
783
|
+
icon: /* @__PURE__ */ jsx(DisplayIcon, { className: "h-3.5 w-3.5" }),
|
|
784
|
+
label: label ?? t.displayOptions,
|
|
785
|
+
panelClassName: "w-72 p-3",
|
|
786
|
+
children: [
|
|
787
|
+
hasGrouping && /* @__PURE__ */ jsxs("div", {
|
|
788
|
+
className: "flex items-center justify-between gap-3",
|
|
789
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
790
|
+
className: "text-[13px] text-(--theme-app)",
|
|
791
|
+
children: t.grouping
|
|
792
|
+
}), /* @__PURE__ */ jsx(GroupingDropdown, { view })]
|
|
793
|
+
}),
|
|
794
|
+
view.orderBys.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
795
|
+
className: cx("flex items-center justify-between gap-3", hasGrouping && "mt-2.5"),
|
|
796
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
797
|
+
className: "text-[13px] text-(--theme-app)",
|
|
798
|
+
children: t.ordering
|
|
799
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
800
|
+
className: "flex items-center gap-1",
|
|
801
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
802
|
+
type: "button",
|
|
803
|
+
onClick: view.toggleOrderDir,
|
|
804
|
+
"aria-label": orderDirLabel,
|
|
805
|
+
title: orderDirLabel,
|
|
806
|
+
className: "flex h-7 w-7 items-center justify-center rounded-md border border-(--theme-border) text-(--theme-muted-text-color) hover:bg-(--theme-muted-panel-bg) hover:text-(--theme-app)",
|
|
807
|
+
children: view.orderDir === "asc" ? /* @__PURE__ */ jsx(ChevronUpIcon, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(ChevronDownIcon, { className: "h-3 w-3" })
|
|
808
|
+
}), /* @__PURE__ */ jsx(OrderingDropdown, { view })]
|
|
809
|
+
})]
|
|
810
|
+
}),
|
|
811
|
+
/* @__PURE__ */ jsxs("div", {
|
|
812
|
+
className: cx("flex items-center justify-between gap-3", (hasGrouping || view.orderBys.length > 0) && "mt-2.5"),
|
|
813
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
814
|
+
className: "text-[13px] text-(--theme-app)",
|
|
815
|
+
children: t.columnHeaders
|
|
816
|
+
}), /* @__PURE__ */ jsx(Switch, {
|
|
817
|
+
checked: view.showColumnHeaders,
|
|
818
|
+
onChange: view.toggleColumnHeaders,
|
|
819
|
+
label: t.showColumnHeaders
|
|
820
|
+
})]
|
|
821
|
+
}),
|
|
822
|
+
/* @__PURE__ */ jsxs("div", {
|
|
823
|
+
className: "mt-2.5 flex items-center justify-between gap-3",
|
|
824
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
825
|
+
className: "text-[13px] text-(--theme-app)",
|
|
826
|
+
children: t.showActions
|
|
827
|
+
}), /* @__PURE__ */ jsx(Switch, {
|
|
828
|
+
checked: view.showActions,
|
|
829
|
+
onChange: view.toggleShowActions,
|
|
830
|
+
label: t.showRowActions
|
|
831
|
+
})]
|
|
832
|
+
}),
|
|
833
|
+
view.showActions && /* @__PURE__ */ jsxs("div", {
|
|
834
|
+
className: "mt-2.5 flex items-center justify-between gap-3 pl-3",
|
|
835
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
836
|
+
className: "text-[13px] text-(--theme-muted-text-color)",
|
|
837
|
+
children: t.alwaysShowActions
|
|
838
|
+
}), /* @__PURE__ */ jsx(Switch, {
|
|
839
|
+
checked: view.alwaysShowActions,
|
|
840
|
+
onChange: view.toggleAlwaysShowActions,
|
|
841
|
+
label: t.alwaysShowRowActions
|
|
842
|
+
})]
|
|
843
|
+
}),
|
|
844
|
+
toggleable.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
845
|
+
className: "mt-3 border-t border-(--theme-border) pt-3",
|
|
846
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
847
|
+
className: SECTION_LABEL,
|
|
848
|
+
children: t.displayProperties
|
|
849
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
850
|
+
className: "flex flex-wrap gap-1.5",
|
|
851
|
+
children: toggleable.map((c) => /* @__PURE__ */ jsx("button", {
|
|
852
|
+
type: "button",
|
|
853
|
+
onClick: () => view.toggleColumn(c.key),
|
|
854
|
+
className: pillClass(view.isVisible(c.key)),
|
|
855
|
+
children: c.label
|
|
856
|
+
}, c.key))
|
|
857
|
+
})]
|
|
858
|
+
}),
|
|
859
|
+
view.showActions && view.actions.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
860
|
+
className: "mt-3 border-t border-(--theme-border) pt-3",
|
|
861
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
862
|
+
className: cx(SECTION_LABEL, "flex items-center gap-1"),
|
|
863
|
+
children: [t.pinnedActions, /* @__PURE__ */ jsx(TooltipInfo, {
|
|
864
|
+
className: "inline-flex",
|
|
865
|
+
text: t.pinnedActionsHelp
|
|
866
|
+
})]
|
|
867
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
868
|
+
className: "flex flex-wrap gap-1.5",
|
|
869
|
+
children: view.actions.map((a) => /* @__PURE__ */ jsx("button", {
|
|
870
|
+
type: "button",
|
|
871
|
+
onClick: () => view.togglePinned(a.key),
|
|
872
|
+
className: pillClass(view.isPinned(a.key)),
|
|
873
|
+
children: a.label
|
|
874
|
+
}, a.key))
|
|
875
|
+
})]
|
|
876
|
+
}),
|
|
877
|
+
!view.isDisplayDefault && /* @__PURE__ */ jsx("div", {
|
|
878
|
+
className: "mt-3 border-t border-(--theme-border) pt-2",
|
|
879
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
880
|
+
type: "button",
|
|
881
|
+
onClick: view.resetDisplay,
|
|
882
|
+
className: "link text-[12px]",
|
|
883
|
+
children: t.reset
|
|
884
|
+
})
|
|
885
|
+
})
|
|
886
|
+
]
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
/** Capture phase: the popover panel stops mousedown propagation, so a
|
|
890
|
+
* bubble-phase listener never sees clicks inside it. */
|
|
891
|
+
function useCloseOnOutsideClick(open, close) {
|
|
892
|
+
const ref = useRef(null);
|
|
893
|
+
useEffect(() => {
|
|
894
|
+
if (!open) return;
|
|
895
|
+
const onDown = (e) => {
|
|
896
|
+
if (e.target instanceof Node && !ref.current?.contains(e.target)) close();
|
|
897
|
+
};
|
|
898
|
+
document.addEventListener("mousedown", onDown, true);
|
|
899
|
+
return () => document.removeEventListener("mousedown", onDown, true);
|
|
900
|
+
}, [open, close]);
|
|
901
|
+
return ref;
|
|
902
|
+
}
|
|
903
|
+
function GroupingDropdown({ view }) {
|
|
904
|
+
const t = useStrings().list;
|
|
905
|
+
const [open, setOpen] = useState(false);
|
|
906
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
907
|
+
ref: useCloseOnOutsideClick(open, useCallback(() => setOpen(false), [])),
|
|
908
|
+
className: "relative",
|
|
909
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
910
|
+
type: "button",
|
|
911
|
+
onClick: () => setOpen((o) => !o),
|
|
912
|
+
className: "flex items-center gap-1.5 rounded-md border border-(--theme-border) px-2 py-1 text-[12px] text-(--theme-app) hover:bg-(--theme-muted-panel-bg)",
|
|
913
|
+
children: [view.groupBys.find((g) => g.value === view.groupBy)?.label ?? t.none, /* @__PURE__ */ jsx(ChevronDownIcon, { className: "h-2.5 w-2.5 opacity-60" })]
|
|
914
|
+
}), open && /* @__PURE__ */ jsx("div", {
|
|
915
|
+
className: cx("absolute right-0 top-full z-20 mt-1", FLYOUT),
|
|
916
|
+
children: view.groupBys.map((g) => /* @__PURE__ */ jsxs("button", {
|
|
917
|
+
type: "button",
|
|
918
|
+
onClick: () => {
|
|
919
|
+
view.setGroupBy(g.value);
|
|
920
|
+
setOpen(false);
|
|
921
|
+
},
|
|
922
|
+
className: cx("flex w-full items-center justify-between gap-3 px-3 py-1.5 text-left text-[13px] hover:bg-(--theme-muted-panel-bg)", view.groupBy === g.value ? "text-(--theme-app)" : "text-(--theme-muted-text-color)"),
|
|
923
|
+
children: [g.label, view.groupBy === g.value && /* @__PURE__ */ jsx(CheckIcon, { className: "h-3 w-3" })]
|
|
924
|
+
}, g.value))
|
|
925
|
+
})]
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
function OrderingDropdown({ view }) {
|
|
929
|
+
const t = useStrings().list;
|
|
930
|
+
const [open, setOpen] = useState(false);
|
|
931
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
932
|
+
ref: useCloseOnOutsideClick(open, useCallback(() => setOpen(false), [])),
|
|
933
|
+
className: "relative",
|
|
934
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
935
|
+
type: "button",
|
|
936
|
+
onClick: () => setOpen((o) => !o),
|
|
937
|
+
className: "flex items-center gap-1.5 rounded-md border border-(--theme-border) px-2 py-1 text-[12px] text-(--theme-app) hover:bg-(--theme-muted-panel-bg)",
|
|
938
|
+
children: [view.orderBys.find((o) => o.value === view.orderBy)?.label ?? t.none, /* @__PURE__ */ jsx(ChevronDownIcon, { className: "h-2.5 w-2.5 opacity-60" })]
|
|
939
|
+
}), open && /* @__PURE__ */ jsx("div", {
|
|
940
|
+
className: cx("absolute right-0 top-full z-20 mt-1", FLYOUT),
|
|
941
|
+
children: view.orderBys.map((o) => /* @__PURE__ */ jsxs("button", {
|
|
942
|
+
type: "button",
|
|
943
|
+
onClick: () => {
|
|
944
|
+
view.setOrderBy(o.value);
|
|
945
|
+
setOpen(false);
|
|
946
|
+
},
|
|
947
|
+
className: cx("flex w-full items-center justify-between gap-3 px-3 py-1.5 text-left text-[13px] hover:bg-(--theme-muted-panel-bg)", view.orderBy === o.value ? "text-(--theme-app)" : "text-(--theme-muted-text-color)"),
|
|
948
|
+
children: [o.label, view.orderBy === o.value && /* @__PURE__ */ jsx(CheckIcon, { className: "h-3 w-3" })]
|
|
949
|
+
}, o.value))
|
|
950
|
+
})]
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
function ListFilterMenu({ view, label }) {
|
|
954
|
+
const t = useStrings().list;
|
|
955
|
+
if (view.facets.length === 0) return null;
|
|
956
|
+
return /* @__PURE__ */ jsx(AnchoredPopover, {
|
|
957
|
+
icon: /* @__PURE__ */ jsx(FilterIcon, { className: "h-3.5 w-3.5" }),
|
|
958
|
+
label: label ?? t.filter,
|
|
959
|
+
active: view.activeFilterCount > 0,
|
|
960
|
+
shortcut: "f",
|
|
961
|
+
panelClassName: "w-56 py-1",
|
|
962
|
+
children: /* @__PURE__ */ jsx(FilterMenuBody, { view })
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
/** Single-open submenu prevents overlapping; resets on panel remount so Filter opens clean. */
|
|
966
|
+
function FilterMenuBody({ view }) {
|
|
967
|
+
const t = useStrings().list;
|
|
968
|
+
const [openKey, setOpenKey] = useState(null);
|
|
969
|
+
const closeTimer = useRef(void 0);
|
|
970
|
+
const openFacet = (key) => {
|
|
971
|
+
clearTimeout(closeTimer.current);
|
|
972
|
+
setOpenKey(key);
|
|
973
|
+
};
|
|
974
|
+
const scheduleClose = () => {
|
|
975
|
+
closeTimer.current = setTimeout(() => setOpenKey(null), 160);
|
|
976
|
+
};
|
|
977
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
978
|
+
className: "flex items-center justify-between px-3 py-1",
|
|
979
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
980
|
+
className: "text-[11px] font-medium uppercase tracking-wide text-(--theme-muted-text-color)",
|
|
981
|
+
children: t.filter
|
|
982
|
+
}), view.activeFilterCount > 0 && /* @__PURE__ */ jsx("button", {
|
|
983
|
+
type: "button",
|
|
984
|
+
onClick: view.clearFilters,
|
|
985
|
+
className: "text-[12px] text-(--theme-muted-text-color) hover:text-(--theme-app)",
|
|
986
|
+
children: t.clear
|
|
987
|
+
})]
|
|
988
|
+
}), view.facets.map((facet) => /* @__PURE__ */ jsx(FilterFacetRow, {
|
|
989
|
+
view,
|
|
990
|
+
facet,
|
|
991
|
+
open: openKey === facet.key,
|
|
992
|
+
onEnter: () => openFacet(facet.key),
|
|
993
|
+
onLeave: scheduleClose
|
|
994
|
+
}, facet.key))] });
|
|
995
|
+
}
|
|
996
|
+
function FilterFacetRow({ view, facet, open, onEnter, onLeave }) {
|
|
997
|
+
const selected = view.filters[facet.key] ?? [];
|
|
998
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
999
|
+
className: "relative",
|
|
1000
|
+
onMouseEnter: onEnter,
|
|
1001
|
+
onMouseLeave: onLeave,
|
|
1002
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1003
|
+
className: "flex cursor-default items-center justify-between gap-3 px-3 py-1.5 text-[13px] text-(--theme-app) hover:bg-(--theme-muted-panel-bg)",
|
|
1004
|
+
children: [/* @__PURE__ */ jsx("span", { children: facet.label }), /* @__PURE__ */ jsxs("span", {
|
|
1005
|
+
className: "flex items-center gap-1.5",
|
|
1006
|
+
children: [selected.length > 0 && /* @__PURE__ */ jsx("span", {
|
|
1007
|
+
className: "text-[11px] text-(--theme-muted-text-color)",
|
|
1008
|
+
children: selected.length
|
|
1009
|
+
}), /* @__PURE__ */ jsx(ChevronRightIcon, { className: "h-3 w-3 opacity-60" })]
|
|
1010
|
+
})]
|
|
1011
|
+
}), open && /* @__PURE__ */ jsx("div", {
|
|
1012
|
+
className: cx("absolute right-full top-0 z-10 max-h-[70vh] overflow-y-auto", FLYOUT),
|
|
1013
|
+
children: facet.options.map((option) => {
|
|
1014
|
+
const checked = selected.includes(option.value);
|
|
1015
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
1016
|
+
type: "button",
|
|
1017
|
+
onClick: () => view.setFacetValues(facet.key, checked ? selected.filter((v) => v !== option.value) : [...selected, option.value]),
|
|
1018
|
+
className: "flex w-full items-center gap-2 px-3 py-1.5 text-left text-[13px] text-(--theme-app) hover:bg-(--theme-muted-panel-bg)",
|
|
1019
|
+
children: [
|
|
1020
|
+
/* @__PURE__ */ jsx("span", {
|
|
1021
|
+
className: "flex h-3.5 w-3.5 shrink-0 items-center justify-center text-(--theme-link)",
|
|
1022
|
+
children: checked && /* @__PURE__ */ jsx(CheckIcon, { className: "h-3 w-3" })
|
|
1023
|
+
}),
|
|
1024
|
+
option.icon,
|
|
1025
|
+
option.label
|
|
1026
|
+
]
|
|
1027
|
+
}, option.value);
|
|
1028
|
+
})
|
|
1029
|
+
})]
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
//#endregion
|
|
1033
|
+
//#region src/list/listSearch.tsx
|
|
1034
|
+
var NO_TERMS = [];
|
|
1035
|
+
/** Pass `enabled: false` to suspend the global ⌘F/`/` hijack (e.g. when search isn't shown and browser Find should stay active). */
|
|
1036
|
+
function useListSearch(enabled = true, storageKey) {
|
|
1037
|
+
const [filter, setFilter] = useState("");
|
|
1038
|
+
const [searchOpen, setSearchOpen] = useState(false);
|
|
1039
|
+
const [storedTerms, setStoredTerms] = useLocalStorage(`${storageKey ?? "list"}:searchTerms`, NO_TERMS);
|
|
1040
|
+
const terms = storageKey ? storedTerms : NO_TERMS;
|
|
1041
|
+
const inputRef = useRef(null);
|
|
1042
|
+
useEffect(() => {
|
|
1043
|
+
if (searchOpen) inputRef.current?.focus();
|
|
1044
|
+
}, [searchOpen]);
|
|
1045
|
+
useEffect(() => {
|
|
1046
|
+
if (!enabled) return;
|
|
1047
|
+
const onKey = (e) => {
|
|
1048
|
+
if (e.key === "Escape" && searchOpen) {
|
|
1049
|
+
setSearchOpen(false);
|
|
1050
|
+
setFilter("");
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
if (isTextEntryContext(e.target)) return;
|
|
1054
|
+
if ((e.key === "f" || e.key === "F") && (e.metaKey || e.ctrlKey)) {
|
|
1055
|
+
e.preventDefault();
|
|
1056
|
+
setSearchOpen(true);
|
|
1057
|
+
} else if (e.key === "/") {
|
|
1058
|
+
e.preventDefault();
|
|
1059
|
+
setSearchOpen(true);
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
window.addEventListener("keydown", onKey);
|
|
1063
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
1064
|
+
}, [searchOpen, enabled]);
|
|
1065
|
+
const openSearch = useCallback(() => setSearchOpen(true), []);
|
|
1066
|
+
const closeSearch = useCallback(() => {
|
|
1067
|
+
setSearchOpen(false);
|
|
1068
|
+
setFilter("");
|
|
1069
|
+
}, []);
|
|
1070
|
+
const addTerm = useCallback((term) => {
|
|
1071
|
+
const trimmed = term.trim();
|
|
1072
|
+
if (!storageKey || !trimmed) return;
|
|
1073
|
+
setStoredTerms((prev) => prev.some((t) => t.toLowerCase() === trimmed.toLowerCase()) ? prev : [...prev, trimmed]);
|
|
1074
|
+
setFilter("");
|
|
1075
|
+
}, [storageKey, setStoredTerms]);
|
|
1076
|
+
const removeTerm = useCallback((term) => setStoredTerms((prev) => prev.filter((t) => t !== term)), [setStoredTerms]);
|
|
1077
|
+
const clearSearch = useCallback(() => {
|
|
1078
|
+
if (storageKey) setStoredTerms(NO_TERMS);
|
|
1079
|
+
setSearchOpen(false);
|
|
1080
|
+
setFilter("");
|
|
1081
|
+
}, [storageKey, setStoredTerms]);
|
|
1082
|
+
const trimmedFilter = filter.trim();
|
|
1083
|
+
return {
|
|
1084
|
+
filter,
|
|
1085
|
+
setFilter,
|
|
1086
|
+
searchOpen,
|
|
1087
|
+
openSearch,
|
|
1088
|
+
closeSearch,
|
|
1089
|
+
clearSearch,
|
|
1090
|
+
terms,
|
|
1091
|
+
addTerm,
|
|
1092
|
+
removeTerm,
|
|
1093
|
+
queries: trimmedFilter ? [...terms, trimmedFilter] : terms,
|
|
1094
|
+
inputRef
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
function ListSearchControl({ search, placeholder, label = "Search" }) {
|
|
1098
|
+
if (search.searchOpen || search.terms.length > 0) return /* @__PURE__ */ jsxs("div", {
|
|
1099
|
+
className: "flex min-h-7 w-72 max-w-[55vw] flex-wrap items-center gap-1.5 rounded-md border border-(--theme-border) bg-(--theme-muted-panel-bg) px-2 py-0.5",
|
|
1100
|
+
children: [
|
|
1101
|
+
/* @__PURE__ */ jsx(SearchIcon, { className: "w-3.5 h-3.5 shrink-0 text-(--theme-muted-text-color)" }),
|
|
1102
|
+
search.terms.map((term) => /* @__PURE__ */ jsxs("span", {
|
|
1103
|
+
className: "flex max-w-40 items-center gap-1 rounded border border-(--theme-border) bg-(--theme-app-bg) px-1.5 py-px text-[12px] text-(--theme-app)",
|
|
1104
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1105
|
+
className: "truncate",
|
|
1106
|
+
children: term
|
|
1107
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1108
|
+
type: "button",
|
|
1109
|
+
onClick: () => search.removeTerm(term),
|
|
1110
|
+
"aria-label": `Remove filter ${term}`,
|
|
1111
|
+
className: "shrink-0 rounded p-0.5 text-(--theme-muted-text-color) hover:text-(--theme-app) transition-colors",
|
|
1112
|
+
children: /* @__PURE__ */ jsx(XIcon, { className: "w-2.5 h-2.5" })
|
|
1113
|
+
})]
|
|
1114
|
+
}, term)),
|
|
1115
|
+
/* @__PURE__ */ jsx("input", {
|
|
1116
|
+
ref: search.inputRef,
|
|
1117
|
+
value: search.filter,
|
|
1118
|
+
onChange: (e) => search.setFilter(e.target.value),
|
|
1119
|
+
onFocus: search.openSearch,
|
|
1120
|
+
onKeyDown: (e) => {
|
|
1121
|
+
if (e.key === "Enter") {
|
|
1122
|
+
e.preventDefault();
|
|
1123
|
+
search.addTerm(search.filter);
|
|
1124
|
+
} else if (e.key === "Backspace" && !search.filter) {
|
|
1125
|
+
const lastTerm = search.terms.at(-1);
|
|
1126
|
+
if (lastTerm) search.removeTerm(lastTerm);
|
|
1127
|
+
}
|
|
1128
|
+
},
|
|
1129
|
+
placeholder,
|
|
1130
|
+
className: "min-w-24 flex-1 bg-transparent text-[13px] text-(--theme-app) placeholder:text-(--theme-muted-text-color) focus:outline-none"
|
|
1131
|
+
}),
|
|
1132
|
+
/* @__PURE__ */ jsx("button", {
|
|
1133
|
+
type: "button",
|
|
1134
|
+
onClick: search.clearSearch,
|
|
1135
|
+
"aria-label": "Clear search",
|
|
1136
|
+
className: "shrink-0 rounded p-0.5 text-(--theme-muted-text-color) hover:text-(--theme-app) transition-colors",
|
|
1137
|
+
children: /* @__PURE__ */ jsx(XIcon, { className: "w-3 h-3" })
|
|
1138
|
+
})
|
|
1139
|
+
]
|
|
1140
|
+
});
|
|
1141
|
+
return /* @__PURE__ */ jsx("button", {
|
|
1142
|
+
type: "button",
|
|
1143
|
+
onClick: search.openSearch,
|
|
1144
|
+
"aria-label": label,
|
|
1145
|
+
"data-tooltip-id": TOOLTIP_ID,
|
|
1146
|
+
"data-tooltip-content": label,
|
|
1147
|
+
"data-tooltip-shortcut": "⌘+F",
|
|
1148
|
+
className: listControlButtonClasses,
|
|
1149
|
+
children: /* @__PURE__ */ jsx(SearchIcon, { className: "w-3.5 h-3.5" })
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
function ListCount({ total, matches, filterActive, noun, nounPlural }) {
|
|
1153
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1154
|
+
className: "flex items-center h-9",
|
|
1155
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
1156
|
+
className: "text-[13px] text-(--theme-muted-text-color)",
|
|
1157
|
+
children: filterActive ? `${matches} of ${total} ${nounPlural}` : `${total} ${total === 1 ? noun : nounPlural}`
|
|
1158
|
+
})
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
//#endregion
|
|
1162
|
+
//#region src/list/ListPager.tsx
|
|
1163
|
+
/** Footer pager bar for paginated new-nav lists: "{start}–{end} of {total}" with prev/next. Renders nothing for a single page. */
|
|
1164
|
+
function ListPager({ page, pageSize, total, onPageChange }) {
|
|
1165
|
+
const t = useStrings().list;
|
|
1166
|
+
const pageCount = Math.max(1, Math.ceil(total / pageSize));
|
|
1167
|
+
const start = page * pageSize + 1;
|
|
1168
|
+
const end = Math.min(total, (page + 1) * pageSize);
|
|
1169
|
+
if (pageCount <= 1) return null;
|
|
1170
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1171
|
+
className: "flex items-center justify-between px-2.5 h-10 shrink-0 border-t theme-border",
|
|
1172
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1173
|
+
className: "text-xs text-(--theme-muted-text-color)",
|
|
1174
|
+
children: t.pager(start, end, total)
|
|
1175
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1176
|
+
className: "flex items-center gap-1",
|
|
1177
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
1178
|
+
type: "button",
|
|
1179
|
+
"aria-label": t.paginationPrevious,
|
|
1180
|
+
disabled: page === 0,
|
|
1181
|
+
onClick: () => onPageChange(page - 1),
|
|
1182
|
+
className: cx(listControlButtonClasses, "disabled:opacity-40 disabled:pointer-events-none"),
|
|
1183
|
+
children: /* @__PURE__ */ jsx(ChevronLeftIcon, { className: "w-3.5 h-3.5" })
|
|
1184
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1185
|
+
type: "button",
|
|
1186
|
+
"aria-label": t.paginationNext,
|
|
1187
|
+
disabled: page >= pageCount - 1,
|
|
1188
|
+
onClick: () => onPageChange(page + 1),
|
|
1189
|
+
className: cx(listControlButtonClasses, "disabled:opacity-40 disabled:pointer-events-none"),
|
|
1190
|
+
children: /* @__PURE__ */ jsx(ChevronRightIcon, { className: "w-3.5 h-3.5" })
|
|
1191
|
+
})]
|
|
1192
|
+
})]
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
//#endregion
|
|
1196
|
+
//#region src/list/HoverCard.tsx
|
|
1197
|
+
function useHoverCard() {
|
|
1198
|
+
const triggerRef = useRef(null);
|
|
1199
|
+
const [coords, setCoords] = useState(null);
|
|
1200
|
+
const openTimer = useRef(void 0);
|
|
1201
|
+
const closeTimer = useRef(void 0);
|
|
1202
|
+
useEffect(() => () => {
|
|
1203
|
+
clearTimeout(openTimer.current);
|
|
1204
|
+
clearTimeout(closeTimer.current);
|
|
1205
|
+
}, []);
|
|
1206
|
+
const scheduleOpen = () => {
|
|
1207
|
+
clearTimeout(closeTimer.current);
|
|
1208
|
+
openTimer.current = setTimeout(() => {
|
|
1209
|
+
const rect = triggerRef.current?.getBoundingClientRect();
|
|
1210
|
+
if (rect) setCoords({
|
|
1211
|
+
x: rect.left,
|
|
1212
|
+
y: rect.bottom
|
|
1213
|
+
});
|
|
1214
|
+
}, 350);
|
|
1215
|
+
};
|
|
1216
|
+
const scheduleClose = () => {
|
|
1217
|
+
clearTimeout(openTimer.current);
|
|
1218
|
+
closeTimer.current = setTimeout(() => setCoords(null), 150);
|
|
1219
|
+
};
|
|
1220
|
+
const keepOpen = () => clearTimeout(closeTimer.current);
|
|
1221
|
+
return {
|
|
1222
|
+
triggerRef,
|
|
1223
|
+
coords,
|
|
1224
|
+
scheduleOpen,
|
|
1225
|
+
scheduleClose,
|
|
1226
|
+
keepOpen
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
function HoverCardSurface({ x, y, widthClassName, relayoutKey, onKeepOpen, onLeave, children }) {
|
|
1230
|
+
const ref = useRef(null);
|
|
1231
|
+
const [pos, setPos] = useState(null);
|
|
1232
|
+
useLayoutEffect(() => {
|
|
1233
|
+
if (!ref.current) return;
|
|
1234
|
+
const { width, height } = ref.current.getBoundingClientRect();
|
|
1235
|
+
const pad = 8;
|
|
1236
|
+
setPos({
|
|
1237
|
+
left: Math.max(pad, Math.min(x, window.innerWidth - width - pad)),
|
|
1238
|
+
top: y + 8 + height + pad > window.innerHeight ? Math.max(pad, y - 8 - height) : y + 8
|
|
1239
|
+
});
|
|
1240
|
+
}, [
|
|
1241
|
+
x,
|
|
1242
|
+
y,
|
|
1243
|
+
relayoutKey
|
|
1244
|
+
]);
|
|
1245
|
+
return createPortal(/* @__PURE__ */ jsx("div", {
|
|
1246
|
+
ref,
|
|
1247
|
+
onMouseEnter: onKeepOpen,
|
|
1248
|
+
onMouseLeave: onLeave,
|
|
1249
|
+
className: cx("fixed z-[9999] rounded-xl border border-(--theme-border) bg-(--theme-panel-bg) p-3 shadow-xl backdrop-blur-xl", widthClassName),
|
|
1250
|
+
style: {
|
|
1251
|
+
left: pos?.left ?? x,
|
|
1252
|
+
top: pos?.top ?? y + 8,
|
|
1253
|
+
visibility: pos ? "visible" : "hidden"
|
|
1254
|
+
},
|
|
1255
|
+
children
|
|
1256
|
+
}), document.querySelector(".ds-root") ?? document.body);
|
|
1257
|
+
}
|
|
1258
|
+
//#endregion
|
|
1259
|
+
export { useRowMenu as A, listTableProps as C, rowContextMenuProps as D, copySubmenu as E, toRowMenuItem as O, listTableFixedProps as S, assembleRowMenu as T, ListSkeleton as _, ListSearchControl as a, RowSelectCheckbox as b, ListFilterMenu as c, DEFAULT_ORDER as d, FAVORITE_ORDER as f, ListError as g, ListActionsHeader as h, ListCount as i, useRowMenuActive as j, toolbarActionToRowAction as k, listControlButtonClasses as l, GroupRow as m, useHoverCard as n, useListSearch as o, useListView as p, ListPager as r, ListDisplayMenu as s, HoverCardSurface as t, isTextEntryContext as u, NAME_MIN_WIDTH_PX as v, MoreButton as w, actionsColumnWidth as x, NameCell as y };
|