@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.
Files changed (42) hide show
  1. package/dist/chunks/{CodePreviewEditor-CSPRILMp.js → CodePreviewEditor-B3688Fx4.js} +1 -1
  2. package/dist/chunks/{Markdown-C5Re2ezs.js → Markdown-iFhq8Gb0.js} +1 -1
  3. package/dist/chunks/{Monaco-CC4ak1JG.js → Monaco-BEj--GTx.js} +10 -5
  4. package/dist/chunks/{Provider-DvVvScTQ.js → Provider-uUHgWOWc.js} +55 -2
  5. package/dist/chunks/{SwitchToggle-2uRw8ZeJ.js → SwitchToggle-BqD0dE-L.js} +36 -29
  6. package/dist/chunks/{editor-D55tumuZ.js → editor-CdQbKtYO.js} +2 -2
  7. package/dist/chunks/list-D5f5Fq-O.js +1259 -0
  8. package/dist/components/Button.d.ts +18 -0
  9. package/dist/components/Card.d.ts +19 -0
  10. package/dist/components/Filter.d.ts +15 -0
  11. package/dist/components/FilterPill.d.ts +7 -0
  12. package/dist/components/IconButton.d.ts +17 -0
  13. package/dist/components/Input.d.ts +14 -0
  14. package/dist/components/LetterBadge.d.ts +6 -0
  15. package/dist/components/Pagination.d.ts +15 -0
  16. package/dist/components/Provider.d.ts +25 -0
  17. package/dist/components/SettingModeOption.d.ts +13 -0
  18. package/dist/components/SettingRow.d.ts +18 -0
  19. package/dist/components/SettingsCard.d.ts +24 -0
  20. package/dist/components/SettingsGroup.d.ts +8 -0
  21. package/dist/components/Sort.d.ts +9 -0
  22. package/dist/components/Toggle.d.ts +6 -0
  23. package/dist/components/dropdownUtils.d.ts +10 -0
  24. package/dist/components/inputClasses.d.ts +1 -0
  25. package/dist/editor/index.js +1 -1
  26. package/dist/file-explorer/index.js +8 -7
  27. package/dist/form/index.js +2 -2
  28. package/dist/index.d.ts +15 -0
  29. package/dist/index.js +429 -5
  30. package/dist/list/HoverCard.d.ts +22 -0
  31. package/dist/list/ListPager.d.ts +7 -0
  32. package/dist/list/ListViewControls.d.ts +12 -0
  33. package/dist/list/index.d.ts +6 -0
  34. package/dist/list/index.js +2 -2
  35. package/dist/list/listSearch.d.ts +27 -0
  36. package/dist/list/listView.d.ts +129 -0
  37. package/dist/list/textEntry.d.ts +2 -0
  38. package/dist/logviewer/index.js +1 -1
  39. package/dist/styles.css +1 -1
  40. package/dist/workflow/index.js +1 -1
  41. package/package.json +2 -6
  42. 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 };