@parallelworks/ui 0.84.2 → 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 };
@@ -89,9 +89,12 @@ export declare function ListRow({ href, onActivate, items, goTo, openMenu, pinne
89
89
  * column's width open; its content lays out across the row's visible width
90
90
  * from the first cell. Elsewhere it is one cell spanning `colSpan` columns.
91
91
  */
92
- export declare function GroupRow({ label, count, trailing, children, leading, colSpan, className, }: {
92
+ export declare function GroupRow({ label, detail, count, trailing, children, leading, colSpan, className, }: {
93
93
  /** The group's name: text, or an icon, a name with its tooltip, badges. */
94
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;
95
98
  /** How many rows the group holds, shown after the label. */
96
99
  count?: number | undefined;
97
100
  /** Set against the row's far end, such as "2/3 running". */
@@ -8,7 +8,7 @@ import { CellPlacementContext, STACKED_ITEM, Table } from "../components/Table.j
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 }) {
@@ -90,7 +93,7 @@ var STACKED_LINE = "mt-1 flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1 emp
90
93
  var isCellElement = (child) => child.type === Table.Item || child.type === "td";
91
94
  function ListRow({ href, onActivate, items, goTo, openMenu, pinnedActions, selected, selectionActive, onRowSelect, leading, className, children }) {
92
95
  const { active: menuActive, openMenu: openRowMenu } = useRowMenuActive(openMenu);
93
- const { show: showActions, always: alwaysShowActions, columnClasses, columnStacks } = useContext(RowActionsContext);
96
+ const { show: showActions, always: alwaysShowActions, columnClasses, columnStacks, lastLocked } = useContext(RowActionsContext);
94
97
  const cells = Children.toArray(children);
95
98
  const stacked = cells.flatMap((child, i) => {
96
99
  if (i === 0 || !columnStacks[i] || !isValidElement(child)) return [];
@@ -131,7 +134,7 @@ function ListRow({ href, onActivate, items, goTo, openMenu, pinnedActions, selec
131
134
  children: child
132
135
  }, child.key);
133
136
  });
134
- const floating = !alwaysShowActions && !(pinnedActions && pinnedActions.length > 0);
137
+ const floating = !alwaysShowActions && !(pinnedActions && pinnedActions.length > 0) && !lastLocked;
135
138
  const stretch = !!href && !selectionActive;
136
139
  const handleRowClick = (e) => {
137
140
  if (e.target.closest("a,button,input,label")) return;
@@ -158,7 +161,7 @@ function ListRow({ href, onActivate, items, goTo, openMenu, pinnedActions, selec
158
161
  showActions && /* @__PURE__ */ jsx(Table.Item, {
159
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!"),
160
163
  children: /* @__PURE__ */ jsxs("div", {
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"),
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"),
162
165
  children: [pinnedActions && pinnedActions.length > 0 && /* @__PURE__ */ jsx("span", {
163
166
  className: "hidden items-center gap-0.5 @[44rem]:flex",
164
167
  children: pinnedActions.map((action) => /* @__PURE__ */ jsx(PinnedActionButton, {
@@ -185,13 +188,19 @@ function ListRow({ href, onActivate, items, goTo, openMenu, pinnedActions, selec
185
188
  * column's width open; its content lays out across the row's visible width
186
189
  * from the first cell. Elsewhere it is one cell spanning `colSpan` columns.
187
190
  */
188
- function GroupRow({ label, count, trailing, children, leading = 0, colSpan, className }) {
191
+ function GroupRow({ label, detail, count, trailing, children, leading = 0, colSpan, className }) {
189
192
  const { show: showActions, columnClasses } = useContext(RowActionsContext);
193
+ const gutters = useRowGutters();
190
194
  const content = children ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [
191
195
  /* @__PURE__ */ jsx("span", {
192
196
  className: "flex min-w-0 items-center gap-2 text-(--theme-app)",
193
197
  children: label
194
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
+ }),
195
204
  count !== void 0 && /* @__PURE__ */ jsxs("span", {
196
205
  className: "shrink-0 text-(--theme-muted-text-color)",
197
206
  children: [/* @__PURE__ */ jsx("span", {
@@ -221,6 +230,8 @@ function GroupRow({ label, count, trailing, children, leading = 0, colSpan, clas
221
230
  return /* @__PURE__ */ jsx("tr", {
222
231
  className: row,
223
232
  "data-group-row": true,
233
+ ref: gutters.ref,
234
+ style: gutters.style,
224
235
  children: Array.from({ length: cells }, (_, i) => {
225
236
  const column = i - leading;
226
237
  const cls = column >= 0 && column < columnClasses.length ? columnClasses[column] : void 0;
@@ -230,7 +241,7 @@ function GroupRow({ label, count, trailing, children, leading = 0, colSpan, clas
230
241
  className: "w-0",
231
242
  children: /* @__PURE__ */ jsx("div", {
232
243
  className: line,
233
- style: { width: "calc(100cqw - 2rem)" },
244
+ style: { width: "calc(100cqw - var(--group-start, 1rem) - var(--group-end, 1rem))" },
234
245
  children: content
235
246
  })
236
247
  })
@@ -238,6 +249,41 @@ function GroupRow({ label, count, trailing, children, leading = 0, colSpan, clas
238
249
  })
239
250
  });
240
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
+ }
241
287
  function PinnedActionButton({ action, visible }) {
242
288
  warnIfInert(action, "ListPage pinned action");
243
289
  const runAction = useRunAction();
@@ -272,21 +318,57 @@ function columnSort(view, orderBy) {
272
318
  if (view.orderBy !== orderBy) return "none";
273
319
  return view.orderDir === "asc" ? "ascending" : "descending";
274
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
+ }
275
359
  /** Name column min-width is by role, not position, so a leading checkbox
276
360
  * doesn't shift it. A column with an `orderBy` the view offers sorts by it.
277
361
  * While the view's column headers are off, the header row collapses: its
278
362
  * cells still size the columns, but nobody sees or hears them, as with any
279
363
  * other display option that is off. */
280
364
  function ListColumns({ view }) {
365
+ const widths = shareWidths(view.visibleColumns, view.columnStacks);
281
366
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [view.visibleColumns.map((col, i) => {
282
367
  const orderBy = view.orderBys.some((o) => o.value === col.orderBy) ? col.orderBy : void 0;
283
368
  return /* @__PURE__ */ jsx(Table.Header, {
284
369
  caps: false,
285
- className: cx(col.headerClassName, view.columnClasses[i], !view.showColumnHeaders && "list-headers-off"),
286
- style: col.alwaysVisible || col.width !== void 0 ? {
287
- ...col.alwaysVisible && { minWidth: 200 },
288
- ...col.width !== void 0 && { width: col.width }
289
- } : 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]),
290
372
  sort: orderBy === void 0 ? void 0 : columnSort(view, orderBy),
291
373
  onSort: orderBy === void 0 ? void 0 : () => view.sortBy(orderBy),
292
374
  children: col.label
@@ -113,6 +113,7 @@ export declare function ListActionsHeader({ view, actionCount, className, }: {
113
113
  showActions: boolean;
114
114
  actionsWidth: number;
115
115
  alwaysShowActions?: boolean;
116
+ lastColumnLocked?: boolean | undefined;
116
117
  };
117
118
  actionCount?: number;
118
119
  className?: string;
@@ -215,7 +215,7 @@ function ListActionsHeader({ view, actionCount = 0, className }) {
215
215
  const { common } = useStrings();
216
216
  if (view && !view.showActions) return null;
217
217
  const width = view ? view.actionsWidth : actionsColumnWidth(actionCount);
218
- const floating = !!view && !view.alwaysShowActions && view.actionsWidth === actionsColumnWidth(0);
218
+ const floating = !!view && !view.alwaysShowActions && !view.lastColumnLocked && view.actionsWidth === actionsColumnWidth(0);
219
219
  return /* @__PURE__ */ jsx(Table.Header, {
220
220
  caps: false,
221
221
  style: {
@@ -1,4 +1,4 @@
1
- import type { ListView } from './listView.js';
1
+ import { type ListView } from './listView.js';
2
2
  import type { ListFilters } from './useListFilters.js';
3
3
  export declare function ListDisplayMenu<T>({ view, label }: {
4
4
  view: ListView<T>;
@@ -10,6 +10,7 @@ import { InfoTip } from "../components/InfoTip.js";
10
10
  import { Switch } from "../components/Switch.js";
11
11
  import { Select } from "../components/Select.js";
12
12
  import { AddFilterMenu } from "./ListFilterBar.js";
13
+ import { isLocked } from "./listView.js";
13
14
  import { Menu } from "@base-ui/react/menu";
14
15
  import { useState } from "react";
15
16
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -20,7 +21,7 @@ var pillClassName = "whitespace-nowrap rounded-full border border-(--theme-borde
20
21
  var SECTION_LABEL = "mb-2 text-[11px] font-medium uppercase tracking-wide text-(--theme-muted-text-color)";
21
22
  function ListDisplayMenu({ view, label }) {
22
23
  const { common, list: t } = useStrings();
23
- const toggleable = view.columns.filter((c) => !c.alwaysVisible);
24
+ const toggleable = view.columns.filter((c) => !isLocked(c));
24
25
  const hasGrouping = view.groupBys.length > 0;
25
26
  const orderDirLabel = view.orderDir === "asc" ? t.ascending : t.descending;
26
27
  return /* @__PURE__ */ jsxs(Popover, {
@@ -4,10 +4,10 @@ import { AddFilterMenu, FilterChip, ListFilterBar } from "./ListFilterBar.js";
4
4
  import { ListActionsHeader, ListCellText, ListError, ListSelectAllHeader, ListSkeleton, NAME_MIN_WIDTH_PX, NameCell, RowSelectCheckbox, actionsColumnWidth, listTableFixedProps, listTableProps } from "./ListTable.js";
5
5
  import { MoreButton, assembleRowMenu, rowContextMenuProps, useCopySubmenu, useRowMenuActive } from "./rowMenu.js";
6
6
  import { GroupRow, ListColumns, ListPage, ListRow, ListRowActionsProvider, useListNavigate } from "./ListPage.js";
7
+ import { useListFilters } from "./useListFilters.js";
8
+ import { DEFAULT_ORDER, FAVORITE_ORDER, useListView } from "./listView.js";
7
9
  import { ListDisplayMenu, ListFilterMenu } from "./ListViewControls.js";
8
10
  import { ListCount, ListSearchControl, useListSearch } from "./listSearch.js";
9
11
  import { ListToolbar } from "./ListToolbar.js";
10
- import { useListFilters } from "./useListFilters.js";
11
- import { DEFAULT_ORDER, FAVORITE_ORDER, useListView } from "./listView.js";
12
12
  import { HoverCardRow, UserHoverCard } from "./UserHoverCard.js";
13
13
  export { ActionToolbar, AddFilterMenu, DATE_PRESETS, DEFAULT_ORDER, FAVORITE_ORDER, FilterChip, GroupRow, HoverCardRow, ListActionsHeader, ListCellText, ListColumns, ListCount, ListDisplayMenu, ListError, ListFilterBar, ListFilterMenu, ListPage, ListRow, ListRowActionsProvider, ListSearchControl, ListSelectAllHeader, ListSkeleton, ListToolbar, MoreButton, NAME_MIN_WIDTH_PX, NameCell, RowSelectCheckbox, ToolbarButton, UserHoverCard, actionsColumnWidth, applyFilterList, assembleRowMenu, dateFilterRange, decodeFilters, encodeFilters, filterParamKeys, listTableFixedProps, listTableProps, matchFilter, rowContextMenuProps, sameFilters, useCopySubmenu, useListFilters, useListNavigate, useListSearch, useListView, useRowMenuActive, withFilterParams };
@@ -7,8 +7,13 @@ export interface ColumnDef {
7
7
  key: string;
8
8
  label: string;
9
9
  headerClassName?: string;
10
- /** Pinned on — never offered in the column picker (e.g. the Name column). */
10
+ /** Pinned on — never offered in the column picker — and the list's name
11
+ * column, which keeps a minimum width so it can't be crushed. */
11
12
  alwaysVisible?: boolean;
13
+ /** Always shown and never offered in the column picker, at its own width:
14
+ * for a column such as a delete button's, which `alwaysVisible` would widen
15
+ * to a name's minimum. */
16
+ locked?: boolean;
12
17
  /** Hidden until the user turns it on in the Display menu. */
13
18
  defaultHidden?: boolean;
14
19
  /** NOT a table column (no header) — appears in picker and isVisible, but excluded from visibleColumns. */
@@ -28,8 +33,9 @@ export interface ColumnDef {
28
33
  /** The column's width, such as `'12%'` or `128`, set on its header. In a
29
34
  * fixed-layout list (`listTableFixedProps`), where every column without one
30
35
  * takes an equal share, give the other columns a width each and leave the
31
- * name without one, so it takes what they leave. A percentage is of the
32
- * list's width, and a hidden column's no longer counts. */
36
+ * name without one, so it takes what they leave. A percentage is a share of
37
+ * the list: when narrower widths drop columns, the shares they held go to
38
+ * the columns left, each in proportion to its own, the name's among them. */
33
39
  width?: string | number;
34
40
  /** `end` sets the header and the cells against the column's far edge, for
35
41
  * figures, sizes and times that compare best lined up on their last digit. */
@@ -37,6 +43,8 @@ export interface ColumnDef {
37
43
  }
38
44
  /** Column responsive importance; `low` drops before `medium`. See {@link ColumnDef.priority}. */
39
45
  type ColumnPriority = 'low' | 'medium';
46
+ /** Whether a column is always shown and kept out of the column picker. */
47
+ export declare function isLocked(col: ColumnDef): boolean;
40
48
  interface GroupByOption {
41
49
  value: string;
42
50
  label: string;
@@ -119,6 +127,10 @@ export interface ListViewChrome {
119
127
  /** Per visible column, whether it stacks under the first on a narrow
120
128
  * container ({@link ColumnDef.stack}); `ListRow` reads entry `i`. */
121
129
  columnStacks: boolean[];
130
+ /** Whether the last column is a locked one, such as a delete button's: the
131
+ * row's `⋯` then keeps a lane of its own after it rather than floating over
132
+ * it on hover. */
133
+ lastColumnLocked?: boolean | undefined;
122
134
  /** The list's filters, which `ListPage` shows as a row of chips under its toolbar. */
123
135
  filterState?: ListFilters | undefined;
124
136
  }