@parallelworks/ui 0.84.1 → 0.85.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.
@@ -1,36 +1,65 @@
1
1
  import { cx } from "../cx.js";
2
2
  import { useLocale, useStrings } from "./Provider.js";
3
- import { CATEGORY_CLASSES, categoryOf } from "./category.js";
4
- import { useCallback, useMemo, useState } from "react";
3
+ import { CATEGORY_CLASSES } from "./category.js";
4
+ import { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
5
5
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
6
  //#region src/components/chart.tsx
7
- var SLOTS = CATEGORY_CLASSES.length;
8
- /** A series' mark color for palette slot 1–8: the theme's category hue, between the slot's
9
- * soft fill and its text and twice as saturated, so a bar or a line stands out from the page
10
- * in light and dark alike and keeps the hue the slot has everywhere else. */
11
- function seriesSlotColor(slot) {
12
- const n = ((slot - 1) % SLOTS + SLOTS) % SLOTS + 1;
7
+ /** How many series a chart colors: a series past the eighth folds into "Other" (`foldSeries`)
8
+ * rather than taking a hue made up for it. */
9
+ var SERIES_LIMIT = CATEGORY_CLASSES.length;
10
+ /** The theme's category slots in the order series take them. Each run from the first — three,
11
+ * four or all eight — keeps neighbors apart in hue and lightness, for color-blind readers too,
12
+ * in light and dark, where the theme's own order steps through neighboring hues. */
13
+ var SERIES_ORDER = [
14
+ 5,
15
+ 2,
16
+ 8,
17
+ 3,
18
+ 6,
19
+ 4,
20
+ 7,
21
+ 1
22
+ ];
23
+ /** The key `foldSeries` gives the series that sums those past the limit. */
24
+ var OTHER_SERIES_KEY = "__other__";
25
+ /** The mark color of the "Other" series: a neutral gray, so it never reads as one more series. */
26
+ var OTHER_SERIES_COLOR = "color-mix(in oklch, var(--theme-muted-text-color) 75%, var(--theme-panel-bg))";
27
+ /** The color of a chart's nth series, 1–8: the theme's category hue, between the slot's soft
28
+ * fill and its text and twice as saturated, so a bar or a line stands out from the page in
29
+ * light and dark alike. Past the eighth it is the "Other" gray; a hue is never repeated. */
30
+ function seriesSlotColor(position) {
31
+ const n = SERIES_ORDER[position - 1];
32
+ if (n === void 0) return OTHER_SERIES_COLOR;
13
33
  return `oklch(from color-mix(in oklch, var(--theme-category-${n}), var(--theme-category-${n}-text)) l calc(c * 2) h)`;
14
34
  }
15
- /** A series' color by its key, any CSS color for a fill, a stroke or a style: the same key
16
- * gets the same color on every chart, as `categoryOf` gives a name its avatar color. */
17
- function seriesColor(key) {
18
- return seriesSlotColor(categoryOf(key));
19
- }
20
- /** Colors for a chart's series, each key its `seriesColor` unless another key took that slot
21
- * first, when it moves to the next free one: up to eight series never share a color. */
35
+ /** Colors for a chart's series by their place in `keys`: the first takes the first color, the
36
+ * second the next, so the first few are always the most distinct. Pass every key the chart can
37
+ * show, in an order that doesn't depend on the data shown, such as by name or as configured:
38
+ * a filter that hides a series then leaves the others their colors. `OTHER_SERIES_KEY`, and any
39
+ * key past the eighth, is the "Other" gray; fold them first with `foldSeries`. */
22
40
  function seriesColors(keys) {
23
- const taken = /* @__PURE__ */ new Set();
24
41
  const colors = {};
42
+ let position = 0;
25
43
  for (const key of keys) {
26
44
  if (key in colors) continue;
27
- let slot = categoryOf(key);
28
- if (taken.size < SLOTS) while (taken.has(slot)) slot = slot % SLOTS + 1;
29
- taken.add(slot);
30
- colors[key] = seriesSlotColor(slot);
45
+ colors[key] = key === "__other__" ? OTHER_SERIES_COLOR : seriesSlotColor(++position);
31
46
  }
32
47
  return colors;
33
48
  }
49
+ /** Splits a chart's series at the limit: up to eight show as they are, and more keep the first
50
+ * seven and fold the rest into one "Other" series, `OTHER_SERIES_KEY`, whose value at a point
51
+ * is the sum of `folded`. Name it with the `chart.other` string. */
52
+ function foldSeries(keys, limit = SERIES_LIMIT) {
53
+ const unique = [...new Set(keys)];
54
+ if (unique.length <= limit) return {
55
+ keys: unique,
56
+ folded: []
57
+ };
58
+ return {
59
+ keys: [...unique.slice(0, limit - 1), OTHER_SERIES_KEY],
60
+ folded: unique.slice(limit - 1)
61
+ };
62
+ }
34
63
  /** A series' color as a mark, for a legend or a tooltip's row. Only seen: the series' name
35
64
  * beside it says which it is. */
36
65
  function SeriesSwatch({ color, shape = "square", className }) {
@@ -107,7 +136,7 @@ function ChartTooltipContent({ label, items, formatValue, total, highlighted, hi
107
136
  const sum = rows.reduce((acc, item) => acc + item.value, 0);
108
137
  const showTotal = total ?? rows.length > 1;
109
138
  return /* @__PURE__ */ jsxs("div", {
110
- className: cx("min-w-40 rounded-lg border border-(--theme-border) bg-(--theme-panel-bg) p-3 text-xs text-(--theme-app) shadow-(--theme-shadow-lg)", className),
139
+ className: cx("min-w-[var(--chart-tooltip-min,10rem)] rounded-lg border border-(--theme-border) bg-(--theme-panel-bg) p-3 text-xs text-(--theme-app) shadow-(--theme-shadow-lg)", className),
111
140
  style,
112
141
  children: [
113
142
  label != null && label !== "" && /* @__PURE__ */ jsx("div", {
@@ -141,5 +170,71 @@ function ChartTooltipContent({ label, items, formatValue, total, highlighted, hi
141
170
  ]
142
171
  });
143
172
  }
173
+ /** Where a chart's tooltip goes beside a crosshair at `x`, so it never covers the column under
174
+ * the pointer: `gap` to the `prefer` side, or the other when it doesn't fit between `x` and
175
+ * that edge, or held to the wider side when neither fits. `start` and `end` bound it, in the
176
+ * same coordinates as `x`, such as the chart's box or the viewport. A charting library's
177
+ * tooltip takes `left` as its position. */
178
+ function chartTooltipPlacement({ x, width, start, end, gap = 12, prefer = "right" }) {
179
+ const room = {
180
+ left: x - gap - start,
181
+ right: end - x - gap
182
+ };
183
+ let side = (prefer === "right" ? ["right", "left"] : ["left", "right"]).find((s) => width <= room[s]);
184
+ let maxWidth = null;
185
+ if (side === void 0) {
186
+ side = room.right >= room.left ? "right" : "left";
187
+ maxWidth = Math.max(0, room[side]);
188
+ }
189
+ const shown = maxWidth ?? width;
190
+ return {
191
+ left: side === "right" ? x + gap : x - gap - shown,
192
+ side,
193
+ maxWidth
194
+ };
195
+ }
196
+ /** Keeps a tooltip clear of the viewport's edge by this much. */
197
+ var VIEWPORT_MARGIN = 8;
198
+ /** Holds a chart's tooltip beside its crosshair, inside a positioned box around the plot: `x`
199
+ * is the crosshair in that box, and the tooltip sits `top` from its top, to the crosshair's
200
+ * inline end, or its start where the box has no room. When neither side of the box has room,
201
+ * as on a phone, it may reach past the box to the viewport's edge, and is held narrower when
202
+ * even that is too small. It never covers the hovered column or the pointer on it. Nothing
203
+ * shows while `x` is null. */
204
+ function ChartTooltipBeside({ x, top = 0, gap = 12, className, children }) {
205
+ const ref = useRef(null);
206
+ useLayoutEffect(() => {
207
+ const el = ref.current;
208
+ if (!el || x === null) return;
209
+ el.style.maxWidth = "";
210
+ el.style.removeProperty("--chart-tooltip-min");
211
+ const width = el.offsetWidth;
212
+ const box = el.offsetParent;
213
+ const origin = box ? box.getBoundingClientRect().left + box.clientLeft : 0;
214
+ const view = document.documentElement.clientWidth || window.innerWidth;
215
+ const place = (start, end) => chartTooltipPlacement({
216
+ x,
217
+ width,
218
+ start,
219
+ end,
220
+ gap,
221
+ prefer: getComputedStyle(el).direction === "rtl" ? "left" : "right"
222
+ });
223
+ let placement = box ? place(0, box.clientWidth) : null;
224
+ if (placement === null || placement.maxWidth !== null) placement = place(VIEWPORT_MARGIN - origin, view - VIEWPORT_MARGIN - origin);
225
+ el.style.left = `${placement.left}px`;
226
+ if (placement.maxWidth !== null) {
227
+ el.style.maxWidth = `${placement.maxWidth}px`;
228
+ el.style.setProperty("--chart-tooltip-min", "0px");
229
+ }
230
+ });
231
+ if (x === null) return null;
232
+ return /* @__PURE__ */ jsx("div", {
233
+ ref,
234
+ className: cx("pointer-events-none absolute z-10 w-max", className),
235
+ style: { top },
236
+ children
237
+ });
238
+ }
144
239
  //#endregion
145
- export { ChartLegendList, ChartTooltipContent, SeriesSwatch, seriesColor, seriesColors, seriesOpacity, seriesSlotColor, useSeriesHighlight };
240
+ export { ChartLegendList, ChartTooltipBeside, ChartTooltipContent, OTHER_SERIES_COLOR, OTHER_SERIES_KEY, SERIES_LIMIT, SeriesSwatch, chartTooltipPlacement, foldSeries, seriesColors, seriesOpacity, seriesSlotColor, useSeriesHighlight };
@@ -0,0 +1,13 @@
1
+ import { type CSSProperties } from 'react';
2
+ /** Which ends of a sideways scroller hide some of its content, kept up to date as it
3
+ * scrolls or resizes. */
4
+ export type HiddenEnds = {
5
+ start: boolean;
6
+ end: boolean;
7
+ };
8
+ export declare function useHiddenEnds(active: boolean): {
9
+ ref: import("react").RefObject<HTMLElement | null>;
10
+ ends: HiddenEnds;
11
+ onScroll: () => void;
12
+ };
13
+ export declare function fadeMask({ start, end }: HiddenEnds): CSSProperties | undefined;
@@ -0,0 +1,51 @@
1
+ import { useEffectEvent, useLayoutEffect, useRef, useState } from "react";
2
+ //#region src/components/scrollFade.ts
3
+ var NO_ENDS = {
4
+ start: false,
5
+ end: false
6
+ };
7
+ function useHiddenEnds(active) {
8
+ const ref = useRef(null);
9
+ const [ends, setEnds] = useState(NO_ENDS);
10
+ const measureNow = () => {
11
+ const el = ref.current;
12
+ if (!el || !active) {
13
+ setEnds(NO_ENDS);
14
+ return;
15
+ }
16
+ const scrolled = Math.abs(el.scrollLeft);
17
+ const start = scrolled > 1;
18
+ const end = scrolled + el.clientWidth < el.scrollWidth - 1;
19
+ setEnds((was) => was.start === start && was.end === end ? was : {
20
+ start,
21
+ end
22
+ });
23
+ };
24
+ const measure = useEffectEvent(measureNow);
25
+ useLayoutEffect(() => {
26
+ measure();
27
+ });
28
+ useLayoutEffect(() => {
29
+ const el = ref.current;
30
+ if (!el || !active || typeof ResizeObserver === "undefined") return;
31
+ const observer = new ResizeObserver(() => measure());
32
+ observer.observe(el);
33
+ return () => observer.disconnect();
34
+ }, [active]);
35
+ return {
36
+ ref,
37
+ ends,
38
+ onScroll: measureNow
39
+ };
40
+ }
41
+ var FADE = "1.25rem";
42
+ function fadeMask({ start, end }) {
43
+ if (!start && !end) return;
44
+ const image = `linear-gradient(to var(--fade-end, right), ${start ? `transparent, #000 ${FADE}` : "#000"}, ${end ? `#000 calc(100% - ${FADE}), transparent` : "#000"})`;
45
+ return {
46
+ maskImage: image,
47
+ WebkitMaskImage: image
48
+ };
49
+ }
50
+ //#endregion
51
+ export { fadeMask, useHiddenEnds };
@@ -551,6 +551,14 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
551
551
  const triggerBrowserDownload = (url, filename) => {
552
552
  const href = safeUrl(url);
553
553
  if (!href) return;
554
+ if (new URL(href, window.location.href).origin !== window.location.origin) {
555
+ const frame = document.createElement("iframe");
556
+ frame.hidden = true;
557
+ frame.src = href;
558
+ document.body.appendChild(frame);
559
+ setTimeout(() => frame.remove(), 6e4);
560
+ return;
561
+ }
554
562
  const a = document.createElement("a");
555
563
  a.href = href;
556
564
  a.download = filename;
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ export { ContextMenuProvider, type MenuCloseTrigger, type MenuOpener, type OpenM
15
15
  export { CopyButton, type CopyButtonProps, CopyCodeBlock, type CopyCodeBlockProps, } from './components/CopyButton.js';
16
16
  export { CreateModal, CreateModalSelect, CreateModalSwitch, } from './components/CreateModal.js';
17
17
  export { categoryClasses, categoryOf } from './components/category.js';
18
- export { type ChartLegendItem, ChartLegendList, ChartTooltipContent, type ChartTooltipItem, SeriesSwatch, type SeriesSwatchShape, seriesColor, seriesColors, seriesOpacity, seriesSlotColor, useSeriesHighlight, } from './components/chart.js';
18
+ export { type ChartLegendItem, ChartLegendList, ChartTooltipBeside, ChartTooltipContent, type ChartTooltipItem, type ChartTooltipPlacement, chartTooltipPlacement, foldSeries, OTHER_SERIES_COLOR, OTHER_SERIES_KEY, SERIES_LIMIT, SeriesSwatch, type SeriesSwatchShape, seriesColors, seriesOpacity, seriesSlotColor, useSeriesHighlight, } from './components/chart.js';
19
19
  export { Calendar, type CalendarProps, DatePicker, type DatePickerProps, } from './components/DatePicker.js';
20
20
  export { DescriptionListGroup, DescriptionListItem, type DescriptionListItemProps, type DescriptionListProps, type DescriptionListSize, type DescriptionListVariant, default as DescriptionList, } from './components/DescriptionList.js';
21
21
  export { collapsePanelClassName, Disclosure, DisclosurePanel, type DisclosureProps, } from './components/Disclosure.js';
package/dist/index.js CHANGED
@@ -45,7 +45,7 @@ import { ConfirmModal, useConfirm } from "./components/ConfirmModal.js";
45
45
  import { Disclosure, DisclosurePanel, collapsePanelClassName } from "./components/Disclosure.js";
46
46
  import { Switch } from "./components/Switch.js";
47
47
  import { CreateModal, CreateModalSelect, CreateModalSwitch } from "./components/CreateModal.js";
48
- import { ChartLegendList, ChartTooltipContent, SeriesSwatch, seriesColor, seriesColors, seriesOpacity, seriesSlotColor, useSeriesHighlight } from "./components/chart.js";
48
+ import { ChartLegendList, ChartTooltipBeside, ChartTooltipContent, OTHER_SERIES_COLOR, OTHER_SERIES_KEY, SERIES_LIMIT, SeriesSwatch, chartTooltipPlacement, foldSeries, seriesColors, seriesOpacity, seriesSlotColor, useSeriesHighlight } from "./components/chart.js";
49
49
  import { Calendar, DatePicker } from "./components/DatePicker.js";
50
50
  import DescriptionList, { DescriptionListGroup, DescriptionListItem } from "./components/DescriptionList.js";
51
51
  import { Drawer } from "./components/Drawer.js";
@@ -84,4 +84,4 @@ import { ToggleGroup } from "./components/ToggleGroup.js";
84
84
  import { useClosingValue } from "./components/useClosingValue.js";
85
85
  import { useCssIsDark } from "./components/useCssIsDark.js";
86
86
  import { useApplyTheme, useColorMode } from "./components/useTheme.js";
87
- export { ActionMenu, AppLink, Autocomplete, Avatar, AvatarGroup, Breadcrumbs, Button, CODE_STYLES, Calendar, Callout, Card, CardHeader, ChartLegendList, ChartTooltipContent, Checkbox, CheckboxGroup, CodeBlock, ColorInput, Combobox, ConfirmModal, ContextMenuProvider, CopyButton, CopyCodeBlock, CountBadge, CreateModal, CreateModalSelect, CreateModalSwitch, DEFAULT_CONTRAST, DEFAULT_PRESET, DEFAULT_THEME, DatePicker, DescriptionList, DescriptionListGroup, DescriptionListItem, Disclosure, DisclosurePanel, Drawer, Elapsed, EmptyState, ErrorPage, ErrorText, ExternalLink, Field, FieldOption, FieldValue, FileButton, HoverCard, IconButton, InfoTip, Input, JND, Kbd, LetterBadge, ListboxGroup, ListboxOption, ListboxPanel, Loader, LoadingRegion, Markdown, MenuCheckboxItem, MenuEntries, MenuFilter, MenuGroupLabel, MenuItem, MenuLinkItem, MenuPopup, MenuRadioItem, MenuSeparator, MenuSubmenuTrigger, Meter, MethodButton, Modal, ModalDescription, ModalTitle, NumberInput, OneTimeCodeInput, Overview, PAGE_SIZES, PageHeader, PageHeaderAction, PageHeaderAddButton, PageHeaderBar, PageHeaderMenu, PageHeaderProvider, Pagination, Popover, PopoverClose, ProgressBar, Radio, RadioGroup, RefreshButton, RelativeTime, RequiredMark, RevealToggle, RunStatusIcon, SIDEBAR_TOKENS, SearchInput, Section, SegmentedControl, Select, SelectionCopy, SeriesSwatch, SettingModeOption, SettingRow, SettingsCard, SettingsHeading, Skeleton, Slider, StatusBadge, StatusDot, StickyBar, Switch, THEME_PRESETS, THEME_TOKENS, TabPanel, Table, Tabs, Tag, TagList, Textarea, ThemeEditor, ThemePreview, TitleInput, ToggleGroup, Tooltip, TooltipProvider, UIProvider, apcaContrast, applyTheme, ariaKeyShortcuts, avatarInitials, buttonClasses, categoryClasses, categoryOf, collapsePanelClassName, composite, contrastRatio, cx, deltaE, deriveTheme, errorPageKind, fieldBox, fitSeed, formatBytes, formatCount, formatDateTime, formatDuration, formatInfoTipContent, formatShortcut, formatTimeLeft, generateTheme, isApplePlatform, isDarkColor, isMenuDivider, isMenuGroup, isMenuSubmenu, isPlainClick, isTextEntryContext, levelTone, menuDivider, modalPanelClassName, modeOf, normalizeTags, parseColor, parseTheme, prefersWideGamut, resolveMode, runStatusTone, schemeFor, serializeTheme, seriesColor, seriesColors, seriesOpacity, seriesSlotColor, shortcutText, statusTextClass, tagClasses, themeStylesheet, toHex, toP3, toast, useApplyTheme, useAvatarColoring, useClock, useCloseMenu, useClosePopover, useClosingValue, useColorMode, useConfirm, useContextMenu, useCopy, useCssIsDark, useElapsed, useFieldControl, useFieldLabelledControl, useListboxNavigation, useLocale, useMenuOpenedBy, useNavigation, useOpenMenu, useOptionalWorkflowEngine, usePersistedState, useProblemText, useProvisionCorsRules, useRelativeTime, useRunFile, useSeriesHighlight, useShortcutFormat, useShortcutKey, useSlots, useStrings, useWorkflowActions, useWorkflowEditing, useWorkflowEngine, useWorkflowEngineLoader };
87
+ export { ActionMenu, AppLink, Autocomplete, Avatar, AvatarGroup, Breadcrumbs, Button, CODE_STYLES, Calendar, Callout, Card, CardHeader, ChartLegendList, ChartTooltipBeside, ChartTooltipContent, Checkbox, CheckboxGroup, CodeBlock, ColorInput, Combobox, ConfirmModal, ContextMenuProvider, CopyButton, CopyCodeBlock, CountBadge, CreateModal, CreateModalSelect, CreateModalSwitch, DEFAULT_CONTRAST, DEFAULT_PRESET, DEFAULT_THEME, DatePicker, DescriptionList, DescriptionListGroup, DescriptionListItem, Disclosure, DisclosurePanel, Drawer, Elapsed, EmptyState, ErrorPage, ErrorText, ExternalLink, Field, FieldOption, FieldValue, FileButton, HoverCard, IconButton, InfoTip, Input, JND, Kbd, LetterBadge, ListboxGroup, ListboxOption, ListboxPanel, Loader, LoadingRegion, Markdown, MenuCheckboxItem, MenuEntries, MenuFilter, MenuGroupLabel, MenuItem, MenuLinkItem, MenuPopup, MenuRadioItem, MenuSeparator, MenuSubmenuTrigger, Meter, MethodButton, Modal, ModalDescription, ModalTitle, NumberInput, OTHER_SERIES_COLOR, OTHER_SERIES_KEY, OneTimeCodeInput, Overview, PAGE_SIZES, PageHeader, PageHeaderAction, PageHeaderAddButton, PageHeaderBar, PageHeaderMenu, PageHeaderProvider, Pagination, Popover, PopoverClose, ProgressBar, Radio, RadioGroup, RefreshButton, RelativeTime, RequiredMark, RevealToggle, RunStatusIcon, SERIES_LIMIT, SIDEBAR_TOKENS, SearchInput, Section, SegmentedControl, Select, SelectionCopy, SeriesSwatch, SettingModeOption, SettingRow, SettingsCard, SettingsHeading, Skeleton, Slider, StatusBadge, StatusDot, StickyBar, Switch, THEME_PRESETS, THEME_TOKENS, TabPanel, Table, Tabs, Tag, TagList, Textarea, ThemeEditor, ThemePreview, TitleInput, ToggleGroup, Tooltip, TooltipProvider, UIProvider, apcaContrast, applyTheme, ariaKeyShortcuts, avatarInitials, buttonClasses, categoryClasses, categoryOf, chartTooltipPlacement, collapsePanelClassName, composite, contrastRatio, cx, deltaE, deriveTheme, errorPageKind, fieldBox, fitSeed, foldSeries, formatBytes, formatCount, formatDateTime, formatDuration, formatInfoTipContent, formatShortcut, formatTimeLeft, generateTheme, isApplePlatform, isDarkColor, isMenuDivider, isMenuGroup, isMenuSubmenu, isPlainClick, isTextEntryContext, levelTone, menuDivider, modalPanelClassName, modeOf, normalizeTags, parseColor, parseTheme, prefersWideGamut, resolveMode, runStatusTone, schemeFor, serializeTheme, seriesColors, seriesOpacity, seriesSlotColor, shortcutText, statusTextClass, tagClasses, themeStylesheet, toHex, toP3, toast, useApplyTheme, useAvatarColoring, useClock, useCloseMenu, useClosePopover, useClosingValue, useColorMode, useConfirm, useContextMenu, useCopy, useCssIsDark, useElapsed, useFieldControl, useFieldLabelledControl, useListboxNavigation, useLocale, useMenuOpenedBy, useNavigation, useOpenMenu, useOptionalWorkflowEngine, usePersistedState, useProblemText, useProvisionCorsRules, useRelativeTime, useRunFile, useSeriesHighlight, useShortcutFormat, useShortcutKey, useSlots, useStrings, useWorkflowActions, useWorkflowEditing, useWorkflowEngine, useWorkflowEngineLoader };
@@ -80,6 +80,33 @@ 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, detail, 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
+ /** A secondary detail after the label, such as an ID, that gives way first: it shows only
96
+ * where the list is at least 36rem wide, and truncates before the label does. */
97
+ detail?: ReactNode;
98
+ /** How many rows the group holds, shown after the label. */
99
+ count?: number | undefined;
100
+ /** Set against the row's far end, such as "2/3 running". */
101
+ trailing?: ReactNode;
102
+ /** The whole header, laid out in a row, in place of `label`, `count` and `trailing`. */
103
+ children?: ReactNode;
104
+ /** Cells the list's rows have before its columns, such as a selection checkbox. */
105
+ leading?: number | undefined;
106
+ /** Outside a list view, the columns the row spans. */
107
+ colSpan?: number | undefined;
108
+ className?: string | undefined;
109
+ }): import("react").JSX.Element;
83
110
  /** Name column min-width is by role, not position, so a leading checkbox
84
111
  * doesn't shift it. A column with an `orderBy` the view offers sorts by it.
85
112
  * While the view's column headers are off, the header row collapses: its
@@ -4,11 +4,11 @@ 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 { CellPlacementContext, 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";
11
- import { Children, cloneElement, createContext, isValidElement, useContext, useMemo } from "react";
11
+ import { Children, cloneElement, createContext, isValidElement, useContext, useLayoutEffect, useMemo, useRef, useState } from "react";
12
12
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
13
13
  //#region src/list/ListPage.tsx
14
14
  /** Defaults mean rows outside a ListView fully show actions (no view = show everything). */
@@ -16,19 +16,22 @@ var RowActionsContext = createContext({
16
16
  show: true,
17
17
  always: false,
18
18
  columnClasses: [],
19
- columnStacks: []
19
+ columnStacks: [],
20
+ lastLocked: false
20
21
  });
21
22
  function useRowActions(view) {
22
23
  return useMemo(() => ({
23
24
  show: view?.showActions ?? true,
24
25
  always: view?.alwaysShowActions ?? false,
25
26
  columnClasses: view?.columnClasses ?? [],
26
- columnStacks: view?.columnStacks ?? []
27
+ columnStacks: view?.columnStacks ?? [],
28
+ lastLocked: view?.lastColumnLocked ?? false
27
29
  }), [
28
30
  view?.showActions,
29
31
  view?.alwaysShowActions,
30
32
  view?.columnClasses,
31
- view?.columnStacks
33
+ view?.columnStacks,
34
+ view?.lastColumnLocked
32
35
  ]);
33
36
  }
34
37
  function ListRowActionsProvider({ view, children }) {
@@ -86,10 +89,11 @@ function ListPage({ bleed = false, toolbar, filterBar, view, loading = false, er
86
89
  });
87
90
  }
88
91
  var STACKED = { stacked: true };
92
+ var STACKED_LINE = "mt-1 flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1 empty:hidden @[36rem]:hidden";
89
93
  var isCellElement = (child) => child.type === Table.Item || child.type === "td";
90
94
  function ListRow({ href, onActivate, items, goTo, openMenu, pinnedActions, selected, selectionActive, onRowSelect, leading, className, children }) {
91
95
  const { active: menuActive, openMenu: openRowMenu } = useRowMenuActive(openMenu);
92
- const { show: showActions, always: alwaysShowActions, columnClasses, columnStacks } = useContext(RowActionsContext);
96
+ const { show: showActions, always: alwaysShowActions, columnClasses, columnStacks, lastLocked } = useContext(RowActionsContext);
93
97
  const cells = Children.toArray(children);
94
98
  const stacked = cells.flatMap((child, i) => {
95
99
  if (i === 0 || !columnStacks[i] || !isValidElement(child)) return [];
@@ -99,14 +103,18 @@ function ListRow({ href, onActivate, items, goTo, openMenu, pinnedActions, selec
99
103
  }
100
104
  return [child];
101
105
  });
102
- const under = stacked.length > 0 ? stacked.map((child, j) => isValidElement(child) && child.type === "td" ? /* @__PURE__ */ jsx("div", {
103
- className: "mt-1 flex min-w-0 items-center @[36rem]:hidden",
104
- "data-stacked": true,
105
- children: child.props.children
106
- }, j) : /* @__PURE__ */ jsx(CellPlacementContext, {
107
- value: STACKED,
108
- children: child
109
- }, j)) : void 0;
106
+ const under = stacked.length > 0 ? /* @__PURE__ */ jsx("div", {
107
+ className: STACKED_LINE,
108
+ "data-stacked-line": true,
109
+ children: stacked.map((child, j) => isValidElement(child) && child.type === "td" ? /* @__PURE__ */ jsx("div", {
110
+ className: STACKED_ITEM,
111
+ "data-stacked": true,
112
+ children: child.props.children
113
+ }, j) : /* @__PURE__ */ jsx(CellPlacementContext, {
114
+ value: STACKED,
115
+ children: child
116
+ }, j))
117
+ }) : void 0;
110
118
  const styledCells = cells.map((child, i) => {
111
119
  const cls = cx(columnClasses[i], i > 0 && stacked.length > 0 && !columnStacks[i] && STAYS_BESIDE_STACK);
112
120
  const after = i === 0 ? under : void 0;
@@ -126,7 +134,7 @@ function ListRow({ href, onActivate, items, goTo, openMenu, pinnedActions, selec
126
134
  children: child
127
135
  }, child.key);
128
136
  });
129
- const floating = !alwaysShowActions && !(pinnedActions && pinnedActions.length > 0);
137
+ const floating = !alwaysShowActions && !(pinnedActions && pinnedActions.length > 0) && !lastLocked;
130
138
  const stretch = !!href && !selectionActive;
131
139
  const handleRowClick = (e) => {
132
140
  if (e.target.closest("a,button,input,label")) return;
@@ -151,9 +159,9 @@ function ListRow({ href, onActivate, items, goTo, openMenu, pinnedActions, selec
151
159
  children: styledCells
152
160
  }),
153
161
  showActions && /* @__PURE__ */ jsx(Table.Item, {
154
- className: cx("relative py-2.5", floating && "pointer-fine:w-0! pointer-fine:min-w-0! pointer-fine:pl-0! pointer-fine:pr-1!"),
162
+ 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!"),
155
163
  children: /* @__PURE__ */ jsxs("div", {
156
- 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"),
164
+ className: cx("absolute inset-y-0 right-0 flex items-center gap-0.5 pr-4", stacked.length > 0 && "@max-[36rem]:items-start @max-[36rem]:pt-2", 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"),
157
165
  children: [pinnedActions && pinnedActions.length > 0 && /* @__PURE__ */ jsx("span", {
158
166
  className: "hidden items-center gap-0.5 @[44rem]:flex",
159
167
  children: pinnedActions.map((action) => /* @__PURE__ */ jsx(PinnedActionButton, {
@@ -171,6 +179,111 @@ function ListRow({ href, onActivate, items, goTo, openMenu, pinnedActions, selec
171
179
  ]
172
180
  });
173
181
  }
182
+ /**
183
+ * A group's header row in a list: its label, such as a status or a cluster,
184
+ * and, against the row's far end, what sums it up, such as "2/3 running".
185
+ *
186
+ * Inside a `ListPage` with a view, it has one cell per column, each hiding
187
+ * with its column as the list narrows, so a group never holds a hidden
188
+ * column's width open; its content lays out across the row's visible width
189
+ * from the first cell. Elsewhere it is one cell spanning `colSpan` columns.
190
+ */
191
+ function GroupRow({ label, detail, count, trailing, children, leading = 0, colSpan, className }) {
192
+ const { show: showActions, columnClasses } = useContext(RowActionsContext);
193
+ const gutters = useRowGutters();
194
+ const content = children ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [
195
+ /* @__PURE__ */ jsx("span", {
196
+ className: "flex min-w-0 items-center gap-2 text-(--theme-app)",
197
+ children: label
198
+ }),
199
+ detail !== void 0 && detail !== null && detail !== false && /* @__PURE__ */ jsx("span", {
200
+ className: "ml-2 hidden min-w-0 shrink-[100] truncate text-(--theme-muted-text-color) @[36rem]:block",
201
+ "data-group-detail": true,
202
+ children: detail
203
+ }),
204
+ count !== void 0 && /* @__PURE__ */ jsxs("span", {
205
+ className: "shrink-0 text-(--theme-muted-text-color)",
206
+ children: [/* @__PURE__ */ jsx("span", {
207
+ className: "mx-1.5 opacity-50",
208
+ "aria-hidden": "true",
209
+ children: "·"
210
+ }), count]
211
+ }),
212
+ trailing !== void 0 && trailing !== null && trailing !== false && /* @__PURE__ */ jsx("span", {
213
+ className: "ml-auto flex shrink-0 items-center gap-2 pl-3 text-(--theme-muted-text-color) tabular-nums",
214
+ children: trailing
215
+ })
216
+ ] });
217
+ const line = "flex h-8 min-w-0 items-center text-xs font-medium whitespace-nowrap";
218
+ const row = cx("bg-(--theme-muted-panel-bg)", className);
219
+ if (columnClasses.length === 0) return /* @__PURE__ */ jsx("tr", {
220
+ className: row,
221
+ children: /* @__PURE__ */ jsx("td", {
222
+ colSpan,
223
+ children: /* @__PURE__ */ jsx("div", {
224
+ className: line,
225
+ children: content
226
+ })
227
+ })
228
+ });
229
+ const cells = leading + columnClasses.length + (showActions ? 1 : 0);
230
+ return /* @__PURE__ */ jsx("tr", {
231
+ className: row,
232
+ "data-group-row": true,
233
+ ref: gutters.ref,
234
+ style: gutters.style,
235
+ children: Array.from({ length: cells }, (_, i) => {
236
+ const column = i - leading;
237
+ const cls = column >= 0 && column < columnClasses.length ? columnClasses[column] : void 0;
238
+ return /* @__PURE__ */ jsx("td", {
239
+ className: cx("p-0", cls),
240
+ children: i === 0 && /* @__PURE__ */ jsx("div", {
241
+ className: "w-0",
242
+ children: /* @__PURE__ */ jsx("div", {
243
+ className: line,
244
+ style: { width: "calc(100cqw - var(--group-start, 1rem) - var(--group-end, 1rem))" },
245
+ children: content
246
+ })
247
+ })
248
+ }, i);
249
+ })
250
+ });
251
+ }
252
+ /** The padding at a row's ends, read from its first and last shown cells, as custom properties a
253
+ * group row's header subtracts from the list's width: so it lines up with the rows however the
254
+ * host pads its list. Until it has measured, 16px, as `listTableProps` pads. */
255
+ function useRowGutters() {
256
+ const ref = useRef(null);
257
+ const [gutters, setGutters] = useState(null);
258
+ useLayoutEffect(() => {
259
+ const row = ref.current;
260
+ if (!row || typeof getComputedStyle === "undefined") return;
261
+ const measure = () => {
262
+ const cells = Array.from(row.cells).filter((cell) => cell.getClientRects().length > 0);
263
+ const first = cells[0];
264
+ const last = cells.at(-1);
265
+ if (!first || !last) return;
266
+ const start = getComputedStyle(first).paddingInlineStart;
267
+ const end = getComputedStyle(last).paddingInlineEnd;
268
+ setGutters((was) => was?.start === start && was.end === end ? was : {
269
+ start,
270
+ end
271
+ });
272
+ };
273
+ measure();
274
+ if (typeof ResizeObserver === "undefined") return;
275
+ const observer = new ResizeObserver(measure);
276
+ observer.observe(row);
277
+ return () => observer.disconnect();
278
+ }, []);
279
+ return {
280
+ ref,
281
+ style: gutters ? {
282
+ "--group-start": gutters.start,
283
+ "--group-end": gutters.end
284
+ } : void 0
285
+ };
286
+ }
174
287
  function PinnedActionButton({ action, visible }) {
175
288
  warnIfInert(action, "ListPage pinned action");
176
289
  const runAction = useRunAction();
@@ -205,18 +318,57 @@ function columnSort(view, orderBy) {
205
318
  if (view.orderBy !== orderBy) return "none";
206
319
  return view.orderDir === "asc" ? "ascending" : "descending";
207
320
  }
321
+ var SHARE_WIDTH = "w-(--share-narrow) @[36rem]:w-(--share-medium) @[52rem]:w-(--share-wide)";
322
+ function percent(width) {
323
+ const match = typeof width === "string" ? /^(\d+(?:\.\d+)?)%$/.exec(width.trim()) : null;
324
+ return match ? Number(match[1]) : void 0;
325
+ }
326
+ /** Each column's width at each tier, for a column whose width is a share of the list. The
327
+ * shares of the columns a tier drops go to those left, each in proportion to its own: the
328
+ * name's, the rest of the list, among them. */
329
+ function shareWidths(columns, stacks) {
330
+ const shares = columns.map((col) => percent(col.width));
331
+ const shows = (col, i, tier) => tier === "wide" || (tier === "medium" ? col.priority !== "low" : !col.priority && !stacks[i]);
332
+ const total = shares.reduce((sum, share) => sum + (share ?? 0), 0);
333
+ const scale = (tier) => {
334
+ const kept = columns.reduce((sum, col, i) => sum + (shows(col, i, tier) ? shares[i] ?? 0 : 0), 0);
335
+ const left = 100 - (total - kept);
336
+ return left > 0 ? 100 / left : 1;
337
+ };
338
+ const factors = {
339
+ wide: 1,
340
+ medium: scale("medium"),
341
+ narrow: scale("narrow")
342
+ };
343
+ return shares.map((share) => share === void 0 ? void 0 : {
344
+ wide: share,
345
+ medium: share * factors.medium,
346
+ narrow: share * factors.narrow
347
+ });
348
+ }
349
+ function headerStyle(col, tiers) {
350
+ const style = {};
351
+ if (col.alwaysVisible) style["minWidth"] = 200;
352
+ if (tiers) {
353
+ style["--share-wide"] = `${tiers.wide}%`;
354
+ style["--share-medium"] = `${tiers.medium}%`;
355
+ style["--share-narrow"] = `${tiers.narrow}%`;
356
+ } else if (col.width !== void 0) style["width"] = col.width;
357
+ return Object.keys(style).length > 0 ? style : void 0;
358
+ }
208
359
  /** Name column min-width is by role, not position, so a leading checkbox
209
360
  * doesn't shift it. A column with an `orderBy` the view offers sorts by it.
210
361
  * While the view's column headers are off, the header row collapses: its
211
362
  * cells still size the columns, but nobody sees or hears them, as with any
212
363
  * other display option that is off. */
213
364
  function ListColumns({ view }) {
365
+ const widths = shareWidths(view.visibleColumns, view.columnStacks);
214
366
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [view.visibleColumns.map((col, i) => {
215
367
  const orderBy = view.orderBys.some((o) => o.value === col.orderBy) ? col.orderBy : void 0;
216
368
  return /* @__PURE__ */ jsx(Table.Header, {
217
369
  caps: false,
218
- className: cx(col.headerClassName, view.columnClasses[i], !view.showColumnHeaders && "list-headers-off"),
219
- style: col.alwaysVisible ? { minWidth: 200 } : void 0,
370
+ className: cx(col.headerClassName, view.columnClasses[i], widths[i] && SHARE_WIDTH, !view.showColumnHeaders && "list-headers-off"),
371
+ style: headerStyle(col, widths[i]),
220
372
  sort: orderBy === void 0 ? void 0 : columnSort(view, orderBy),
221
373
  onSort: orderBy === void 0 ? void 0 : () => view.sortBy(orderBy),
222
374
  children: col.label
@@ -229,4 +381,4 @@ function useListNavigate() {
229
381
  return useNavigation().goTo;
230
382
  }
231
383
  //#endregion
232
- export { ListColumns, ListPage, ListRow, ListRowActionsProvider, useListNavigate };
384
+ 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, }: {
@@ -117,6 +113,7 @@ export declare function ListActionsHeader({ view, actionCount, className, }: {
117
113
  showActions: boolean;
118
114
  actionsWidth: number;
119
115
  alwaysShowActions?: boolean;
116
+ lastColumnLocked?: boolean | undefined;
120
117
  };
121
118
  actionCount?: number;
122
119
  className?: string;