@parallelworks/ui 0.84.0 → 0.84.2

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.
@@ -14,10 +14,12 @@ export interface LoaderProps {
14
14
  * label: hidden from assistive technology, and in the text's color. */
15
15
  decorative?: boolean;
16
16
  /** Centers it in the room it stands for, so the page needs no box of its
17
- * own: `screen` the whole window, such as while the app starts; `page` the
18
- * page's scrolling region, such as while a route loads; `section` a pane
19
- * or a card with no height of its own. */
20
- fill?: 'screen' | 'page' | 'section' | undefined;
17
+ * own: `screen` the whole window, such as while the app starts; `content`
18
+ * the app's content area beside its sidebar and under its top bar, however
19
+ * the page around it is laid out; `page` the page's scrolling region, such
20
+ * as while a route loads; `section` a pane or a card with no height of its
21
+ * own. */
22
+ fill?: 'screen' | 'content' | 'page' | 'section' | undefined;
21
23
  }
22
24
  /** Something on the way that has no shape to sketch yet. When its shape is
23
25
  * known, such as rows of a list, sketch it with `Skeleton` instead. */
@@ -1,7 +1,9 @@
1
1
  import { cx } from "../cx.js";
2
2
  import { RunningIcon } from "../icons.js";
3
+ import { pinnedAbove, scrollContainer } from "./stickyTop.js";
3
4
  import { useStrings } from "./Provider.js";
4
- import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { useLayoutEffect, useRef, useState } from "react";
6
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
7
  //#region src/components/Loader.tsx
6
8
  var RIPPLE_STAGGER_S = .22;
7
9
  var LOGO_RINGS = [
@@ -19,6 +21,10 @@ var fillClasses = {
19
21
  * known, such as rows of a list, sketch it with `Skeleton` instead. */
20
22
  function Loader({ fill, ...props }) {
21
23
  if (!fill) return /* @__PURE__ */ jsx(LoaderMark, { ...props });
24
+ if (fill === "content") return /* @__PURE__ */ jsx(ContentArea, { children: /* @__PURE__ */ jsx(LoaderMark, {
25
+ ...props,
26
+ size: props.size ?? 48
27
+ }) });
22
28
  return /* @__PURE__ */ jsx("div", {
23
29
  className: fillClasses[fill],
24
30
  children: /* @__PURE__ */ jsx(LoaderMark, {
@@ -68,5 +74,51 @@ function LoaderMark({ size = 48, label, variant = "spinner", inline = false, col
68
74
  })]
69
75
  });
70
76
  }
77
+ /** Centers its content in the box of the scrolling region it sits in, which in an app with a
78
+ * sidebar is its content area, below what that region pins at its top and held to what of it
79
+ * the window shows; the window, where the document itself scrolls. It lays nothing out around
80
+ * it and takes no pointer. */
81
+ function ContentArea({ children }) {
82
+ const anchor = useRef(null);
83
+ const [box, setBox] = useState(null);
84
+ useLayoutEffect(() => {
85
+ const el = anchor.current;
86
+ if (!el) return;
87
+ const area = scrollContainer(el);
88
+ if (area === el.ownerDocument.documentElement) return;
89
+ const view = el.ownerDocument.defaultView ?? window;
90
+ const pinned = pinnedAbove(el);
91
+ const place = () => {
92
+ const rect = area.getBoundingClientRect();
93
+ const chrome = Math.max(0, ...pinned.chrome.map((c) => c.getBoundingClientRect().bottom));
94
+ const top = Math.max(rect.top, chrome, 0);
95
+ const bottom = Math.min(rect.bottom, view.innerHeight);
96
+ setBox({
97
+ top,
98
+ left: rect.left,
99
+ width: rect.width,
100
+ height: Math.max(bottom - top, 0)
101
+ });
102
+ };
103
+ place();
104
+ view.addEventListener("resize", place);
105
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(place);
106
+ observer?.observe(area);
107
+ for (const chrome of pinned.chrome) observer?.observe(chrome);
108
+ return () => {
109
+ view.removeEventListener("resize", place);
110
+ observer?.disconnect();
111
+ };
112
+ }, []);
113
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
114
+ ref: anchor,
115
+ hidden: true
116
+ }), /* @__PURE__ */ jsx("div", {
117
+ "data-loader-area": "",
118
+ className: "pointer-events-none fixed inset-0 flex items-center justify-center",
119
+ style: box ?? void 0,
120
+ children
121
+ })] });
122
+ }
71
123
  //#endregion
72
124
  export { Loader };
@@ -14,8 +14,9 @@ export interface RefreshButtonProps {
14
14
  /** A button's look, `ghost` by default; `inline` is text-sized muted text with no box, for
15
15
  * a line of details such as a header's "Updated 2 minutes ago". */
16
16
  variant?: ButtonVariant | 'inline' | undefined;
17
- /** Only the icon shows, with "Refresh" as its name and tooltip. */
18
- iconOnly?: boolean | undefined;
17
+ /** Only the icon shows, with "Refresh" as its name and tooltip. `phone` shows the label on a
18
+ * wider screen and only the icon on a phone, the label still its name for a screen reader. */
19
+ iconOnly?: boolean | 'phone' | undefined;
19
20
  className?: string | undefined;
20
21
  }
21
22
  /** Fetches again on demand, and says so while it does, out loud too. */
@@ -7,6 +7,12 @@ import { useRelativeTime } from "./relativeTime.js";
7
7
  import { useState } from "react";
8
8
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
9
9
  //#region src/components/RefreshButton.tsx
10
+ var phoneSquares = {
11
+ xs: "max-sm:w-6 max-sm:px-0",
12
+ sm: "max-sm:w-7 max-sm:px-0",
13
+ md: "max-sm:w-8 max-sm:px-0",
14
+ lg: "max-sm:w-10 max-sm:px-0"
15
+ };
10
16
  var inlineClasses = "inline-flex cursor-pointer items-center gap-1 rounded px-1 py-0.5 text-(--theme-muted-text-color) transition-colors hover:bg-(--theme-muted-panel-bg) hover:text-(--theme-app) focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-(--theme-focus) [&_svg]:size-3";
11
17
  /** Fetches again on demand, and says so while it does, out loud too. */
12
18
  function RefreshButton({ onRefresh, refreshing = false, updatedAt, size = "md", variant = "ghost", iconOnly = false, className }) {
@@ -14,6 +20,8 @@ function RefreshButton({ onRefresh, refreshing = false, updatedAt, size = "md",
14
20
  const [pending, setPending] = useState(false);
15
21
  const busy = refreshing || pending;
16
22
  const ago = useRelativeTime(updatedAt);
23
+ const phone = iconOnly === "phone";
24
+ const square = iconOnly === true;
17
25
  const refresh = () => {
18
26
  if (busy) return;
19
27
  const result = onRefresh();
@@ -27,27 +35,30 @@ function RefreshButton({ onRefresh, refreshing = false, updatedAt, size = "md",
27
35
  type: "button",
28
36
  onClick: refresh,
29
37
  "aria-busy": busy,
30
- "aria-label": iconOnly ? t.refresh : void 0,
38
+ "aria-label": square ? t.refresh : void 0,
31
39
  className: cx(variant === "inline" ? inlineClasses : buttonClasses({
32
40
  variant,
33
41
  size,
34
- iconOnly
35
- }), "group aria-busy:cursor-progress", className),
42
+ iconOnly: square
43
+ }), phone && variant !== "inline" && phoneSquares[size], "group aria-busy:cursor-progress", className),
36
44
  children: [/* @__PURE__ */ jsx(RefreshIcon, {
37
45
  "aria-hidden": "true",
38
46
  className: cx("shrink-0 motion-reduce:animate-none motion-reduce:transition-none", busy ? "animate-spin" : "transition-transform duration-300 group-hover:rotate-180")
39
- }), !iconOnly && (ago ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("span", {
40
- className: "sr-only",
41
- children: [t.refresh, ". "]
42
- }), t.updated(ago)] }) : busy ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
43
- className: "sr-only",
44
- children: t.refresh
45
- }), /* @__PURE__ */ jsx("span", {
46
- "aria-hidden": "true",
47
- children: t.refreshing
48
- })] }) : t.refresh)]
47
+ }), !square && /* @__PURE__ */ jsx("span", {
48
+ className: phone ? "max-sm:sr-only" : void 0,
49
+ children: ago ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("span", {
50
+ className: "sr-only",
51
+ children: [t.refresh, ". "]
52
+ }), t.updated(ago)] }) : busy ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
53
+ className: "sr-only",
54
+ children: t.refresh
55
+ }), /* @__PURE__ */ jsx("span", {
56
+ "aria-hidden": "true",
57
+ children: t.refreshing
58
+ })] }) : t.refresh
59
+ })]
49
60
  });
50
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [iconOnly ? /* @__PURE__ */ jsx(Tooltip, {
61
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [square ? /* @__PURE__ */ jsx(Tooltip, {
51
62
  content: t.refresh,
52
63
  children: button
53
64
  }) : button, /* @__PURE__ */ jsx("span", {
@@ -5,11 +5,24 @@ export type SortDirection = 'ascending' | 'descending' | 'none';
5
5
  * height, such as a key and value list or a dashboard card's runs; `dense`
6
6
  * sets small type in tight rows, for a panel of figures. */
7
7
  export type TableDensity = 'comfortable' | 'compact' | 'dense';
8
+ /** What a list row asks of the cell in a place, through whatever component of the host's renders
9
+ * it: its column's classes, content to show after its own, or that it shows stacked, under the
10
+ * first cell's, as a line in place of a cell. */
11
+ export interface CellPlacement {
12
+ className?: string | undefined;
13
+ after?: React.ReactNode;
14
+ stacked?: boolean | undefined;
15
+ }
16
+ /** A stacked column's value on the line under a row's name: it flows inline with the others and
17
+ * wraps between them, never inside a value, which truncates if it is wider than the line. */
18
+ export declare const STACKED_ITEM = "flex min-w-0 max-w-full items-center whitespace-nowrap [&>*]:min-w-0 [&>*]:truncate";
19
+ /** Set by a list row around each of its cells; a `Table.Item` takes what it asks. */
20
+ export declare const CellPlacementContext: React.Context<CellPlacement | null>;
8
21
  interface ItemProps extends React.TdHTMLAttributes<HTMLTableCellElement> {
9
22
  /** Lets the text wrap; by default a cell keeps to one line. */
10
23
  wrap?: boolean;
11
24
  }
12
- declare function Item({ wrap, children, className, ...tdProps }: ItemProps): React.JSX.Element;
25
+ declare function Item({ wrap, children, className, ...tdProps }: ItemProps): React.JSX.Element | null;
13
26
  interface HeaderProps {
14
27
  children?: React.ReactNode;
15
28
  className?: string | undefined;
@@ -1,7 +1,7 @@
1
1
  import { cx } from "../cx.js";
2
2
  import { SortDownIcon, SortUnsortedIcon, SortUpIcon } from "../icons.js";
3
- import { Loader } from "./Loader.js";
4
3
  import { useStickyTop } from "./stickyTop.js";
4
+ import { Loader } from "./Loader.js";
5
5
  import React, { createContext, useContext, useState } from "react";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
7
  //#region src/components/Table.tsx
@@ -23,12 +23,26 @@ var headerY = {
23
23
  dense: "py-1"
24
24
  };
25
25
  var cellX = "px-3";
26
+ /** A stacked column's value on the line under a row's name: it flows inline with the others and
27
+ * wraps between them, never inside a value, which truncates if it is wider than the line. */
28
+ var STACKED_ITEM = "flex min-w-0 max-w-full items-center whitespace-nowrap [&>*]:min-w-0 [&>*]:truncate";
29
+ /** Set by a list row around each of its cells; a `Table.Item` takes what it asks. */
30
+ var CellPlacementContext = createContext(null);
26
31
  function Item({ wrap = false, children, className, ...tdProps }) {
27
32
  const density = useContext(DensityContext);
33
+ const placed = useContext(CellPlacementContext);
34
+ if (placed?.stacked) return React.Children.toArray(children).length > 0 ? /* @__PURE__ */ jsx("div", {
35
+ className: STACKED_ITEM,
36
+ "data-stacked": true,
37
+ children
38
+ }) : null;
28
39
  return /* @__PURE__ */ jsx("td", {
29
40
  ...tdProps,
30
- className: cx(cellX, cellY[density], wrap ? wrapY[density] : "whitespace-nowrap", className),
31
- children
41
+ className: cx(cellX, cellY[density], wrap ? wrapY[density] : "overflow-clip text-ellipsis whitespace-nowrap [overflow-clip-margin:4px]", className, placed?.className),
42
+ children: /* @__PURE__ */ jsxs(CellPlacementContext, {
43
+ value: null,
44
+ children: [children, placed?.after]
45
+ })
32
46
  });
33
47
  }
34
48
  var sortIcons = {
@@ -107,4 +121,4 @@ function Table({ density = "comfortable", columns, caps = true, flush = false, c
107
121
  Table.Header = Header;
108
122
  Table.Item = Item;
109
123
  //#endregion
110
- export { Table };
124
+ export { CellPlacementContext, STACKED_ITEM, Table };
@@ -1,8 +1,8 @@
1
1
  import { cx } from "../cx.js";
2
+ import { useStickyTop } from "./stickyTop.js";
2
3
  import { Tooltip } from "./Tooltip.js";
3
4
  import { AppLink } from "./AppLink.js";
4
5
  import { SegmentContent, segmentClassName, segmentGroupClassName } from "./segmented.js";
5
- import { useStickyTop } from "./stickyTop.js";
6
6
  import { useState } from "react";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
8
  import { Tabs } from "@base-ui/react/tabs";
@@ -1,6 +1,15 @@
1
1
  /** Pins a bar to the top of its scroller, over what scrolls under it.
2
2
  * `useStickyTop` moves it below whatever else is pinned there. */
3
3
  export declare const stickyClasses = "sticky top-0 z-(--layer-sticky) bg-(--theme-app-bg)";
4
+ /** The box a sticky element sticks to: its nearest ancestor that scrolls, or
5
+ * the document's when none does. */
6
+ export declare function scrollContainer(el: Element): Element;
7
+ /** What a page pins at the top of `el`'s scroller over it, such as a page header and a toolbar:
8
+ * those elements, and how far below the scroller's top their bottom edge sits. */
9
+ export declare function pinnedAbove(el: Element): {
10
+ chrome: HTMLElement[];
11
+ bottom: number;
12
+ };
4
13
  /**
5
14
  * Pins a `sticky` element just below whatever its scroller already pins at
6
15
  * the top, such as a page header, a toolbar and a tab row, rather than at
@@ -47,6 +47,15 @@ function pinnedBottom(chrome) {
47
47
  const height = chrome.getBoundingClientRect().height;
48
48
  return height > 0 ? (Number.parseFloat(getComputedStyle(chrome).top) || 0) + height : 0;
49
49
  }
50
+ /** What a page pins at the top of `el`'s scroller over it, such as a page header and a toolbar:
51
+ * those elements, and how far below the scroller's top their bottom edge sits. */
52
+ function pinnedAbove(el) {
53
+ const { chrome } = chromeAbove(el, scrollContainer(el));
54
+ return {
55
+ chrome,
56
+ bottom: Math.max(0, ...chrome.map(pinnedBottom))
57
+ };
58
+ }
50
59
  /**
51
60
  * Pins a `sticky` element just below whatever its scroller already pins at
52
61
  * the top, such as a page header, a toolbar and a tab row, rather than at
@@ -95,4 +104,4 @@ function useStickyTop(el, enabled) {
95
104
  }, [el, enabled]);
96
105
  }
97
106
  //#endregion
98
- export { stickyClasses, useStickyTop };
107
+ export { pinnedAbove, scrollContainer, stickyClasses, useStickyTop };
@@ -1,5 +1,6 @@
1
1
  import { cx } from "../cx.js";
2
2
  import { ArrowLeftIcon, ArrowRightIcon, ChevronLeftIcon, ChevronRightIcon, CloseIcon, FileTextIcon, ZoomInIcon, ZoomOutIcon } from "../icons.js";
3
+ import { useStickyTop } from "../components/stickyTop.js";
3
4
  import { Loader } from "../components/Loader.js";
4
5
  import { useLocale, useRunFile, useSlots, useStrings, useWorkflowEngine } from "../components/Provider.js";
5
6
  import { Button } from "../components/Button.js";
@@ -10,7 +11,6 @@ import { DisclosurePanel } from "../components/Disclosure.js";
10
11
  import { formatDateTime } from "../format.js";
11
12
  import { Elapsed } from "../components/relativeTime.js";
12
13
  import { RunStatusIcon } from "../components/RunStatusIcon.js";
13
- import { useStickyTop } from "../components/stickyTop.js";
14
14
  import { Tabs } from "../components/Tabs.js";
15
15
  import { LogViewer } from "../logviewer/LogViewer.js";
16
16
  import { initializeValues } from "../form/lib.js";
@@ -3,12 +3,12 @@ import { buttonClasses } from "../components/buttonClasses.js";
3
3
  import { MoreIcon } from "../icons.js";
4
4
  import { MenuPopup, MenuSeparator } from "../components/Menu.js";
5
5
  import { isDev } from "../dev.js";
6
+ import { useStickyTop } from "../components/stickyTop.js";
6
7
  import { Loader } from "../components/Loader.js";
7
8
  import { Tooltip } from "../components/Tooltip.js";
8
9
  import { useStrings } from "../components/Provider.js";
9
10
  import { AppLink } from "../components/AppLink.js";
10
11
  import { MenuEntries, useRunAction, warnIfInert } from "../components/MenuEntries.js";
11
- import { useStickyTop } from "../components/stickyTop.js";
12
12
  import { Menu } from "@base-ui/react/menu";
13
13
  import { Fragment, cloneElement, createContext, isValidElement, use, useLayoutEffect, useRef, useState } from "react";
14
14
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -80,6 +80,30 @@ export declare function ListRow({ href, onActivate, items, goTo, openMenu, pinne
80
80
  leading?: ReactNode;
81
81
  children: ReactNode;
82
82
  }): import("react").JSX.Element;
83
+ /**
84
+ * A group's header row in a list: its label, such as a status or a cluster,
85
+ * and, against the row's far end, what sums it up, such as "2/3 running".
86
+ *
87
+ * Inside a `ListPage` with a view, it has one cell per column, each hiding
88
+ * with its column as the list narrows, so a group never holds a hidden
89
+ * column's width open; its content lays out across the row's visible width
90
+ * from the first cell. Elsewhere it is one cell spanning `colSpan` columns.
91
+ */
92
+ export declare function GroupRow({ label, count, trailing, children, leading, colSpan, className, }: {
93
+ /** The group's name: text, or an icon, a name with its tooltip, badges. */
94
+ label?: ReactNode;
95
+ /** How many rows the group holds, shown after the label. */
96
+ count?: number | undefined;
97
+ /** Set against the row's far end, such as "2/3 running". */
98
+ trailing?: ReactNode;
99
+ /** The whole header, laid out in a row, in place of `label`, `count` and `trailing`. */
100
+ children?: ReactNode;
101
+ /** Cells the list's rows have before its columns, such as a selection checkbox. */
102
+ leading?: number | undefined;
103
+ /** Outside a list view, the columns the row spans. */
104
+ colSpan?: number | undefined;
105
+ className?: string | undefined;
106
+ }): import("react").JSX.Element;
83
107
  /** Name column min-width is by role, not position, so a leading checkbox
84
108
  * doesn't shift it. A column with an `orderBy` the view offers sorts by it.
85
109
  * While the view's column headers are off, the header row collapses: its
@@ -4,7 +4,7 @@ import { Tooltip } from "../components/Tooltip.js";
4
4
  import { useNavigation } from "../components/Provider.js";
5
5
  import { AppLink, isPlainClick } from "../components/AppLink.js";
6
6
  import { isMenuDivider, isMenuGroup, isMenuSubmenu, useRunAction, warnIfInert } from "../components/MenuEntries.js";
7
- import { Table } from "../components/Table.js";
7
+ import { CellPlacementContext, STACKED_ITEM, Table } from "../components/Table.js";
8
8
  import { ListFilterBar } from "./ListFilterBar.js";
9
9
  import { ListActionsHeader, ListError, ListSkeleton, StretchRowLinkContext } from "./ListTable.js";
10
10
  import { MoreButton, rowContextMenuProps, useRowMenuActive } from "./rowMenu.js";
@@ -85,28 +85,51 @@ function ListPage({ bleed = false, toolbar, filterBar, view, loading = false, er
85
85
  ]
86
86
  });
87
87
  }
88
+ var STACKED = { stacked: true };
89
+ var STACKED_LINE = "mt-1 flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1 empty:hidden @[36rem]:hidden";
90
+ var isCellElement = (child) => child.type === Table.Item || child.type === "td";
88
91
  function ListRow({ href, onActivate, items, goTo, openMenu, pinnedActions, selected, selectionActive, onRowSelect, leading, className, children }) {
89
92
  const { active: menuActive, openMenu: openRowMenu } = useRowMenuActive(openMenu);
90
93
  const { show: showActions, always: alwaysShowActions, columnClasses, columnStacks } = useContext(RowActionsContext);
91
94
  const cells = Children.toArray(children);
92
95
  const stacked = cells.flatMap((child, i) => {
93
96
  if (i === 0 || !columnStacks[i] || !isValidElement(child)) return [];
94
- const content = child.props.children;
95
- return Children.toArray(content).length > 0 ? [content] : [];
97
+ if (isCellElement(child)) {
98
+ const content = child.props.children;
99
+ return Children.toArray(content).length > 0 ? [child] : [];
100
+ }
101
+ return [child];
96
102
  });
103
+ const under = stacked.length > 0 ? /* @__PURE__ */ jsx("div", {
104
+ className: STACKED_LINE,
105
+ "data-stacked-line": true,
106
+ children: stacked.map((child, j) => isValidElement(child) && child.type === "td" ? /* @__PURE__ */ jsx("div", {
107
+ className: STACKED_ITEM,
108
+ "data-stacked": true,
109
+ children: child.props.children
110
+ }, j) : /* @__PURE__ */ jsx(CellPlacementContext, {
111
+ value: STACKED,
112
+ children: child
113
+ }, j))
114
+ }) : void 0;
97
115
  const styledCells = cells.map((child, i) => {
98
116
  const cls = cx(columnClasses[i], i > 0 && stacked.length > 0 && !columnStacks[i] && STAYS_BESIDE_STACK);
99
- if (!isValidElement(child) || !cls && !(i === 0 && stacked.length > 0)) return child;
100
- const el = child;
101
- if (i === 0 && stacked.length > 0) return cloneElement(el, {
102
- className: cx(el.props.className, cls),
103
- children: /* @__PURE__ */ jsxs(Fragment$1, { children: [el.props.children, stacked.map((content, j) => /* @__PURE__ */ jsx("div", {
104
- className: "mt-1 flex min-w-0 items-center @[36rem]:hidden",
105
- "data-stacked": true,
106
- children: content
107
- }, j))] })
108
- });
109
- return cloneElement(el, { className: cx(el.props.className, cls) });
117
+ const after = i === 0 ? under : void 0;
118
+ if (!isValidElement(child) || !cls && !after) return child;
119
+ if (child.type === "td") {
120
+ const el = child;
121
+ return cloneElement(el, {
122
+ className: cx(el.props.className, cls),
123
+ ...after ? { children: /* @__PURE__ */ jsxs(Fragment$1, { children: [el.props.children, after] }) } : {}
124
+ });
125
+ }
126
+ return /* @__PURE__ */ jsx(CellPlacementContext, {
127
+ value: {
128
+ className: cls || void 0,
129
+ after
130
+ },
131
+ children: child
132
+ }, child.key);
110
133
  });
111
134
  const floating = !alwaysShowActions && !(pinnedActions && pinnedActions.length > 0);
112
135
  const stretch = !!href && !selectionActive;
@@ -133,7 +156,7 @@ function ListRow({ href, onActivate, items, goTo, openMenu, pinnedActions, selec
133
156
  children: styledCells
134
157
  }),
135
158
  showActions && /* @__PURE__ */ jsx(Table.Item, {
136
- className: cx("relative py-2.5", floating && "pointer-fine:w-0! pointer-fine:min-w-0! pointer-fine:pl-0! pointer-fine:pr-1!"),
159
+ className: cx("relative overflow-visible py-2.5", floating && "pointer-fine:w-0! pointer-fine:min-w-0! pointer-fine:pl-0! pointer-fine:pr-1!"),
137
160
  children: /* @__PURE__ */ jsxs("div", {
138
161
  className: cx("absolute inset-y-0 right-0 flex items-center gap-0.5 pr-4", floating && !menuActive && "pointer-fine:opacity-0 pointer-fine:group-hover:opacity-100 pointer-fine:focus-within:opacity-100", floating && "pointer-fine:bg-linear-to-l pointer-fine:from-(--theme-hover) pointer-fine:from-70% pointer-fine:to-transparent pointer-fine:pl-6"),
139
162
  children: [pinnedActions && pinnedActions.length > 0 && /* @__PURE__ */ jsx("span", {
@@ -153,6 +176,68 @@ function ListRow({ href, onActivate, items, goTo, openMenu, pinnedActions, selec
153
176
  ]
154
177
  });
155
178
  }
179
+ /**
180
+ * A group's header row in a list: its label, such as a status or a cluster,
181
+ * and, against the row's far end, what sums it up, such as "2/3 running".
182
+ *
183
+ * Inside a `ListPage` with a view, it has one cell per column, each hiding
184
+ * with its column as the list narrows, so a group never holds a hidden
185
+ * column's width open; its content lays out across the row's visible width
186
+ * from the first cell. Elsewhere it is one cell spanning `colSpan` columns.
187
+ */
188
+ function GroupRow({ label, count, trailing, children, leading = 0, colSpan, className }) {
189
+ const { show: showActions, columnClasses } = useContext(RowActionsContext);
190
+ const content = children ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [
191
+ /* @__PURE__ */ jsx("span", {
192
+ className: "flex min-w-0 items-center gap-2 text-(--theme-app)",
193
+ children: label
194
+ }),
195
+ count !== void 0 && /* @__PURE__ */ jsxs("span", {
196
+ className: "shrink-0 text-(--theme-muted-text-color)",
197
+ children: [/* @__PURE__ */ jsx("span", {
198
+ className: "mx-1.5 opacity-50",
199
+ "aria-hidden": "true",
200
+ children: "·"
201
+ }), count]
202
+ }),
203
+ trailing !== void 0 && trailing !== null && trailing !== false && /* @__PURE__ */ jsx("span", {
204
+ className: "ml-auto flex shrink-0 items-center gap-2 pl-3 text-(--theme-muted-text-color) tabular-nums",
205
+ children: trailing
206
+ })
207
+ ] });
208
+ const line = "flex h-8 min-w-0 items-center text-xs font-medium whitespace-nowrap";
209
+ const row = cx("bg-(--theme-muted-panel-bg)", className);
210
+ if (columnClasses.length === 0) return /* @__PURE__ */ jsx("tr", {
211
+ className: row,
212
+ children: /* @__PURE__ */ jsx("td", {
213
+ colSpan,
214
+ children: /* @__PURE__ */ jsx("div", {
215
+ className: line,
216
+ children: content
217
+ })
218
+ })
219
+ });
220
+ const cells = leading + columnClasses.length + (showActions ? 1 : 0);
221
+ return /* @__PURE__ */ jsx("tr", {
222
+ className: row,
223
+ "data-group-row": true,
224
+ children: Array.from({ length: cells }, (_, i) => {
225
+ const column = i - leading;
226
+ const cls = column >= 0 && column < columnClasses.length ? columnClasses[column] : void 0;
227
+ return /* @__PURE__ */ jsx("td", {
228
+ className: cx("p-0", cls),
229
+ children: i === 0 && /* @__PURE__ */ jsx("div", {
230
+ className: "w-0",
231
+ children: /* @__PURE__ */ jsx("div", {
232
+ className: line,
233
+ style: { width: "calc(100cqw - 2rem)" },
234
+ children: content
235
+ })
236
+ })
237
+ }, i);
238
+ })
239
+ });
240
+ }
156
241
  function PinnedActionButton({ action, visible }) {
157
242
  warnIfInert(action, "ListPage pinned action");
158
243
  const runAction = useRunAction();
@@ -198,7 +283,10 @@ function ListColumns({ view }) {
198
283
  return /* @__PURE__ */ jsx(Table.Header, {
199
284
  caps: false,
200
285
  className: cx(col.headerClassName, view.columnClasses[i], !view.showColumnHeaders && "list-headers-off"),
201
- style: col.alwaysVisible ? { minWidth: 200 } : void 0,
286
+ style: col.alwaysVisible || col.width !== void 0 ? {
287
+ ...col.alwaysVisible && { minWidth: 200 },
288
+ ...col.width !== void 0 && { width: col.width }
289
+ } : void 0,
202
290
  sort: orderBy === void 0 ? void 0 : columnSort(view, orderBy),
203
291
  onSort: orderBy === void 0 ? void 0 : () => view.sortBy(orderBy),
204
292
  children: col.label
@@ -211,4 +299,4 @@ function useListNavigate() {
211
299
  return useNavigation().goTo;
212
300
  }
213
301
  //#endregion
214
- export { ListColumns, ListPage, ListRow, ListRowActionsProvider, useListNavigate };
302
+ export { GroupRow, ListColumns, ListPage, ListRow, ListRowActionsProvider, useListNavigate };
@@ -54,17 +54,13 @@ export declare function NameCell({ icon, iconTile, name, subtitle, href, badge,
54
54
  }): import("react").JSX.Element;
55
55
  /** A row's column text: muted 13px, or monospace for an ID, a path or a
56
56
  * hash. The style sits on the text rather than the cell, so it carries over
57
- * when a narrow list stacks the column under the name. */
57
+ * when a narrow list stacks the column under the name. Text wider than its
58
+ * column is cut off with an ellipsis, and shows whole in a tooltip. */
58
59
  export declare function ListCellText({ mono, className, children, }: {
59
60
  mono?: boolean | undefined;
60
61
  className?: string | undefined;
61
62
  children: ReactNode;
62
63
  }): import("react").JSX.Element;
63
- export declare function GroupRow({ label, count, colSpan, }: {
64
- label: string;
65
- count: number;
66
- colSpan: number;
67
- }): import("react").JSX.Element;
68
64
  /** Says a list couldn't load. It never shows the error's own text, which is
69
65
  * the server's words, in its language and not written for the reader. */
70
66
  export declare function ListError({ noun, message, }: {
@@ -1,11 +1,12 @@
1
1
  import { cx } from "../cx.js";
2
2
  import { ErrorIcon } from "../icons.js";
3
+ import { Tooltip } from "../components/Tooltip.js";
3
4
  import { useStrings } from "../components/Provider.js";
4
5
  import { AppLink } from "../components/AppLink.js";
5
6
  import { positionKeys } from "../components/keys.js";
6
7
  import { LoadingRegion, Skeleton } from "../components/Skeleton.js";
7
8
  import { Checkbox } from "../components/Checkbox.js";
8
- import { Table } from "../components/Table.js";
9
+ import { CellPlacementContext, Table } from "../components/Table.js";
9
10
  import { createContext, useContext } from "react";
10
11
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
11
12
  //#region src/list/ListTable.tsx
@@ -43,6 +44,8 @@ var STRETCHED_LINK = "after:absolute after:inset-0 after:content-[''] focus-visi
43
44
  var STRETCHED_LINK_ROW = "relative [&_:is(a,button,input,label,[role=checkbox]):not([data-row-link])]:relative [&_:is(a,button,input,label,[role=checkbox]):not([data-row-link])]:z-[1]";
44
45
  function NameCell({ icon, iconTile = false, name, subtitle, href, badge, title, children }) {
45
46
  const stretch = useContext(StretchRowLinkContext);
47
+ const placed = useContext(CellPlacementContext);
48
+ const under = placed?.after;
46
49
  const hasIcon = icon !== null && icon !== void 0 && icon !== false;
47
50
  const columns = hasIcon ? "col-span-2 grid grid-cols-subgrid items-center" : "flex items-center";
48
51
  const inner = /* @__PURE__ */ jsxs(Fragment$1, { children: [hasIcon && (iconTile && subtitle ? /* @__PURE__ */ jsx("span", {
@@ -65,7 +68,7 @@ function NameCell({ icon, iconTile = false, name, subtitle, href, badge, title,
65
68
  children: subtitle
66
69
  })]
67
70
  })] });
68
- return /* @__PURE__ */ jsx(Table.Item, {
71
+ const cell = /* @__PURE__ */ jsx(Table.Item, {
69
72
  className: "py-2.5",
70
73
  children: /* @__PURE__ */ jsxs("div", {
71
74
  className: cx("grid min-w-0", hasIcon ? "grid-cols-[auto_minmax(0,1fr)] gap-x-2.5" : "grid-cols-[minmax(0,1fr)]"),
@@ -77,41 +80,34 @@ function NameCell({ icon, iconTile = false, name, subtitle, href, badge, title,
77
80
  }) : /* @__PURE__ */ jsx("div", {
78
81
  className: cx("min-w-0", columns),
79
82
  children: inner
80
- }), !!children && /* @__PURE__ */ jsx("div", {
83
+ }), (!!children || !!under) && /* @__PURE__ */ jsxs("div", {
81
84
  className: hasIcon ? "col-start-2 min-w-0" : "min-w-0",
82
- children
85
+ children: [children, under]
83
86
  })]
84
87
  })
85
88
  });
89
+ return placed?.after ? /* @__PURE__ */ jsx(CellPlacementContext, {
90
+ value: {
91
+ ...placed,
92
+ after: void 0
93
+ },
94
+ children: cell
95
+ }) : cell;
86
96
  }
87
97
  /** A row's column text: muted 13px, or monospace for an ID, a path or a
88
98
  * hash. The style sits on the text rather than the cell, so it carries over
89
- * when a narrow list stacks the column under the name. */
99
+ * when a narrow list stacks the column under the name. Text wider than its
100
+ * column is cut off with an ellipsis, and shows whole in a tooltip. */
90
101
  function ListCellText({ mono = false, className, children }) {
91
- return /* @__PURE__ */ jsx("span", {
92
- className: cx("text-(--theme-muted-text-color)", mono ? "font-mono text-xs" : "text-[13px]", className),
102
+ const text = /* @__PURE__ */ jsx("span", {
103
+ className: cx("block min-w-0 truncate text-(--theme-muted-text-color)", mono ? "font-mono text-xs" : "text-[13px]", className),
93
104
  children
94
105
  });
95
- }
96
- function GroupRow({ label, count, colSpan }) {
97
- return /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsxs("td", {
98
- colSpan,
99
- className: "bg-(--theme-muted-panel-bg) py-1.5 text-xs font-medium",
100
- children: [
101
- /* @__PURE__ */ jsx("span", {
102
- className: "text-(--theme-app)",
103
- children: label
104
- }),
105
- /* @__PURE__ */ jsx("span", {
106
- className: "mx-1.5 opacity-50",
107
- children: "·"
108
- }),
109
- /* @__PURE__ */ jsx("span", {
110
- className: "text-(--theme-muted-text-color)",
111
- children: count
112
- })
113
- ]
114
- }) });
106
+ return typeof children === "string" || typeof children === "number" ? /* @__PURE__ */ jsx(Tooltip, {
107
+ content: children,
108
+ overflowOnly: true,
109
+ children: text
110
+ }) : text;
115
111
  }
116
112
  /** Says a list couldn't load. It never shows the error's own text, which is
117
113
  * the server's words, in its language and not written for the reader. */
@@ -235,4 +231,4 @@ function ListActionsHeader({ view, actionCount = 0, className }) {
235
231
  }
236
232
  ListActionsHeader.isTableHeader = true;
237
233
  //#endregion
238
- export { FLOATING_ACTIONS_LANE, GroupRow, HEADERS_OFF, ListActionsHeader, ListCellText, ListError, ListSelectAllHeader, ListSkeleton, NAME_MIN_WIDTH_PX, NameCell, RowSelectCheckbox, STRETCHED_LINK, STRETCHED_LINK_ROW, StretchRowLinkContext, actionsColumnWidth, listTableFixedProps, listTableProps };
234
+ export { FLOATING_ACTIONS_LANE, HEADERS_OFF, ListActionsHeader, ListCellText, ListError, ListSelectAllHeader, ListSkeleton, NAME_MIN_WIDTH_PX, NameCell, RowSelectCheckbox, STRETCHED_LINK, STRETCHED_LINK_ROW, StretchRowLinkContext, actionsColumnWidth, listTableFixedProps, listTableProps };