@parallelworks/ui 0.60.0 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -74,6 +74,7 @@ Each need has one component. Reach for it before styling markup by hand.
74
74
  | Free text with suggestions, or tags | `Autocomplete`; `Combobox` with `multiple creatable` |
75
75
  | A range | `Slider` |
76
76
  | A page's breadcrumbs, title and commands | `PageHeader`, shown by the shell's `PageHeaderBar` (see [Page header](#page-header)) |
77
+ | A trail anywhere else, such as a folder's path in a panel | `Breadcrumbs`, which folds as the header's trail does |
77
78
  | An action in the page header | `PageHeaderAction`; `PageHeaderAddButton` for "+ Add" or "+ Create"; `PageHeaderMenu` for the "…" menu |
78
79
  | A titled part of a page or form | `Section` |
79
80
  | More detail under a line | `Disclosure` |
@@ -173,7 +174,12 @@ A page declares its header in one place, and the shell shows it:
173
174
  trail is a list in a `nav` landmark named by the strings' `breadcrumb`. A
174
175
  trail too wide for its bar folds its ancestors into a "…" menu of links
175
176
  (named by `showFullPath`), keeping the parent when it fits beside the page's
176
- name; the name truncates last, with the full name on hover.
177
+ name; the name truncates last, with the full name on hover. The bar draws it
178
+ with `Breadcrumbs`, which renders the same trail anywhere else: in a flex
179
+ row it takes the room the row's other items leave it, and elsewhere its
180
+ container's width. `current="location"` marks a place within the page, such
181
+ as the open folder, and `heading` makes the last crumb the page's `h1`, as
182
+ the bar does.
177
183
  - **`titleActions`** sit beside the title, for actions on the page's subject.
178
184
  **`actions`** sit at the end of the bar, for the page's commands: a detail
179
185
  page's Start, Stop or Terminal go here, not in a row of buttons below it.
@@ -479,6 +485,7 @@ where it opens elsewhere, and renders it through `AppLink`:
479
485
  | `Button`, `IconButton`, `Card` | `href` |
480
486
  | Menu rows, `RowAction`s (a list row's menu and pinned buttons), `ToolbarAction`s | `href` on the action, with `target: '_blank'` for a new tab |
481
487
  | `PageHeaderAction`, `PageHeaderAddButton`, `PageHeader` breadcrumbs | `href`; `external` opens a new tab |
488
+ | `Breadcrumbs` | `href` on the crumb; `onSelect` only for a place with no URL, as the explorer's trail without `pathHref` |
482
489
  | `Tabs` that are pages, `Pagination` | `href` on the item, `pageHref` |
483
490
  | A list row | `ListRow` `href` with a `NameCell` of the same `href`: the name's link covers the row |
484
491
  | A toast's action | `action: { label, href }` |
@@ -2,10 +2,10 @@ import { AddIcon, LockIcon, UsersIcon } from "../icons.js";
2
2
  import { toast } from "../components/Toast.js";
3
3
  import { EmptyState } from "../components/EmptyState.js";
4
4
  import { useQueryMatch } from "../components/search.js";
5
+ import { LoadingRegion, Skeleton } from "../components/Skeleton.js";
5
6
  import { Button } from "../components/Button.js";
6
7
  import { Callout } from "../components/Callout.js";
7
8
  import { Drawer } from "../components/Drawer.js";
8
- import { LoadingRegion, Skeleton } from "../components/Skeleton.js";
9
9
  import { SearchInput } from "../components/SearchInput.js";
10
10
  import { SegmentedControl } from "../components/SegmentedControl.js";
11
11
  import { buildModel, invert, key, principalsWithAccess, warnAboutMisuse } from "./model.js";
@@ -3,9 +3,9 @@ import { OrganizationIcon, RefreshIcon, UsersIcon } from "../icons.js";
3
3
  import { useQueryMatch } from "../components/search.js";
4
4
  import { categoryClasses } from "../components/category.js";
5
5
  import { Avatar } from "../components/Avatar.js";
6
+ import { Skeleton } from "../components/Skeleton.js";
6
7
  import { Callout } from "../components/Callout.js";
7
8
  import { Combobox } from "../components/Combobox.js";
8
- import { Skeleton } from "../components/Skeleton.js";
9
9
  import { useRelativeTime } from "../components/relativeTime.js";
10
10
  import { StatusBadge } from "../components/StatusBadge.js";
11
11
  import { covers, isOwner } from "./model.js";
@@ -1,9 +1,9 @@
1
1
  import { cx } from "../../cx.js";
2
2
  import { positionKeys } from "../../components/keys.js";
3
3
  import { usePersistedState } from "../../components/Provider.js";
4
+ import { LoadingRegion, Skeleton } from "../../components/Skeleton.js";
4
5
  import { useSplitter } from "../../components/splitter.js";
5
6
  import { Drawer } from "../../components/Drawer.js";
6
- import { LoadingRegion, Skeleton } from "../../components/Skeleton.js";
7
7
  import { SidebarGroupHeading, SidebarPanel, SidebarToggle } from "./sidebar.js";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
9
  import { Fragment as Fragment$1, useEffect, useRef } from "react";
@@ -0,0 +1,33 @@
1
+ /** One step of the trail to the current page. */
2
+ export interface Breadcrumb {
3
+ /** Empty while the name is still loading: the trail shows a placeholder. */
4
+ label: string;
5
+ href?: string | undefined;
6
+ }
7
+ export interface BreadcrumbsProps {
8
+ /** The trail, ending with where the reader is. A crumb with an empty label,
9
+ * such as a name still loading, shows a placeholder. */
10
+ crumbs: readonly Breadcrumb[];
11
+ /** Goes to a crumb that has no `href`: a place with no URL, such as a folder
12
+ * in a view that keeps its path out of the URL. A crumb with a URL takes
13
+ * `href` instead, so it opens in a new tab. Without either, a crumb is
14
+ * text. */
15
+ onSelect?: ((crumb: Breadcrumb, index: number) => void) | undefined;
16
+ /** What the last crumb is: the page itself, or a place within the page, such
17
+ * as the open folder. */
18
+ current?: 'page' | 'location' | undefined;
19
+ /** Makes the last crumb the page's `h1`, as `PageHeader` does. A page has
20
+ * one. */
21
+ heading?: boolean | undefined;
22
+ /** Names the landmark; the strings' `breadcrumb` by default. */
23
+ label?: string | undefined;
24
+ className?: string | undefined;
25
+ }
26
+ /**
27
+ * The trail to where the reader is, as a list in a `nav` landmark. A trail too
28
+ * wide for its room folds its ancestors into a "…" menu, keeping the parent
29
+ * when it fits beside the last crumb, which truncates last, with its full name
30
+ * on hover. In a flex row its room is what the row's other items leave it;
31
+ * elsewhere it fills its container's width.
32
+ */
33
+ export declare function Breadcrumbs({ crumbs, onSelect, current, heading, label, className, }: BreadcrumbsProps): import("react").JSX.Element;
@@ -0,0 +1,203 @@
1
+ import { cx } from "../cx.js";
2
+ import { buttonClasses } from "./buttonClasses.js";
3
+ import { ChevronRightIcon, MoreIcon } from "../icons.js";
4
+ import { keyedByContent } from "./keys.js";
5
+ import { Tooltip } from "./Tooltip.js";
6
+ import { useStrings } from "./Provider.js";
7
+ import { AppLink } from "./AppLink.js";
8
+ import { ActionMenu } from "./ActionMenu.js";
9
+ import { Skeleton } from "./Skeleton.js";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ import { useLayoutEffect, useState } from "react";
12
+ //#region src/components/Breadcrumbs.tsx
13
+ /**
14
+ * The trail to where the reader is, as a list in a `nav` landmark. A trail too
15
+ * wide for its room folds its ancestors into a "…" menu, keeping the parent
16
+ * when it fits beside the last crumb, which truncates last, with its full name
17
+ * on hover. In a flex row its room is what the row's other items leave it;
18
+ * elsewhere it fills its container's width.
19
+ */
20
+ function Breadcrumbs({ crumbs, onSelect, current = "page", heading = false, label, className }) {
21
+ const { common } = useStrings();
22
+ const [nav, setNav] = useState(null);
23
+ const [natural, setNatural] = useState(null);
24
+ const fit = useTrailFit(nav, natural);
25
+ const last = crumbs[crumbs.length - 1];
26
+ const ancestors = crumbs.slice(0, -1);
27
+ const folded = fit === "none" ? ancestors : fit === "parent" ? ancestors.slice(0, -1) : [];
28
+ const shown = ancestors.slice(folded.length);
29
+ const Last = heading ? "h1" : "span";
30
+ const menuItem = (crumb, index) => {
31
+ const name = crumb.label || common.loading;
32
+ if (crumb.href) return {
33
+ label: name,
34
+ href: crumb.href
35
+ };
36
+ if (onSelect) return {
37
+ label: name,
38
+ onClick: () => onSelect(crumb, index)
39
+ };
40
+ return {
41
+ label: name,
42
+ disabled: true,
43
+ onClick: () => {}
44
+ };
45
+ };
46
+ return /* @__PURE__ */ jsxs("nav", {
47
+ ref: setNav,
48
+ "aria-label": label ?? common.breadcrumb,
49
+ className: cx("relative min-w-0", className),
50
+ children: [/* @__PURE__ */ jsxs("ol", {
51
+ className: "m-0 flex min-w-0 list-none items-center gap-1.5 p-0 text-[13px] leading-none",
52
+ children: [
53
+ folded.length > 0 && /* @__PURE__ */ jsxs("li", {
54
+ className: "flex shrink-0 items-center gap-1.5",
55
+ children: [/* @__PURE__ */ jsx(ActionMenu, {
56
+ label: common.showFullPath,
57
+ align: "start",
58
+ items: folded.map(menuItem),
59
+ triggerClassName: buttonClasses({
60
+ variant: "ghost",
61
+ size: "xs",
62
+ iconOnly: true
63
+ }),
64
+ children: /* @__PURE__ */ jsx(MoreIcon, {})
65
+ }), /* @__PURE__ */ jsx(Separator, {})]
66
+ }),
67
+ keyedByContent(shown, (c) => c.href ?? c.label).map(({ key, item: crumb }, i) => {
68
+ const index = folded.length + i;
69
+ return /* @__PURE__ */ jsxs("li", {
70
+ className: cx("flex items-center gap-1.5", folded.length > 0 ? "min-w-0 max-w-36 shrink-0" : "min-w-0"),
71
+ children: [crumb.label === "" ? /* @__PURE__ */ jsx(CrumbPlaceholder, {}) : /* @__PURE__ */ jsx(Tooltip, {
72
+ content: crumb.label,
73
+ overflowOnly: true,
74
+ children: crumb.href ? /* @__PURE__ */ jsx(AppLink, {
75
+ href: crumb.href,
76
+ className: cx(crumbText, crumbLink, "max-w-48"),
77
+ children: crumb.label
78
+ }) : onSelect ? /* @__PURE__ */ jsx("button", {
79
+ type: "button",
80
+ onClick: () => onSelect(crumb, index),
81
+ className: cx(crumbText, crumbLink, "max-w-48 cursor-pointer text-start"),
82
+ children: crumb.label
83
+ }) : /* @__PURE__ */ jsx("span", {
84
+ className: cx(crumbText, "max-w-48"),
85
+ children: crumb.label
86
+ })
87
+ }), /* @__PURE__ */ jsx(Separator, {})]
88
+ }, key);
89
+ }),
90
+ last && /* @__PURE__ */ jsx("li", {
91
+ className: "flex min-w-0 items-center",
92
+ children: last.label === "" ? /* @__PURE__ */ jsx(Last, {
93
+ "aria-current": current,
94
+ className: "m-0",
95
+ children: /* @__PURE__ */ jsx(CrumbPlaceholder, {})
96
+ }) : /* @__PURE__ */ jsx(Tooltip, {
97
+ content: last.label,
98
+ overflowOnly: true,
99
+ children: /* @__PURE__ */ jsx(Last, {
100
+ "aria-current": current,
101
+ className: "m-0 block truncate text-[13px] font-medium leading-none text-(--theme-app)",
102
+ children: last.label
103
+ })
104
+ })
105
+ })
106
+ ]
107
+ }), /* @__PURE__ */ jsx("div", {
108
+ "aria-hidden": true,
109
+ className: "pointer-events-none invisible absolute inset-0 overflow-hidden",
110
+ children: /* @__PURE__ */ jsx("div", {
111
+ ref: setNatural,
112
+ className: "flex w-max items-center gap-1.5 whitespace-nowrap text-[13px] leading-none",
113
+ children: crumbs.map((crumb, i) => /* @__PURE__ */ jsxs("span", {
114
+ className: "flex items-center gap-1.5",
115
+ children: [/* @__PURE__ */ jsx("span", {
116
+ className: cx(i < crumbs.length - 1 && "max-w-48 truncate", i === crumbs.length - 1 && "font-medium"),
117
+ children: crumb.label || " ".repeat(6)
118
+ }), i < crumbs.length - 1 && /* @__PURE__ */ jsx(Separator, {})]
119
+ }, i))
120
+ })
121
+ })]
122
+ });
123
+ }
124
+ var crumbText = "block truncate text-(--theme-muted-text-color)";
125
+ var crumbLink = "rounded-sm transition-colors hover:text-(--theme-app) focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-(--theme-link)";
126
+ function Separator() {
127
+ return /* @__PURE__ */ jsx(ChevronRightIcon, { className: "size-3 shrink-0 text-(--theme-muted-text-color) opacity-60 rtl:rotate-180" });
128
+ }
129
+ function CrumbPlaceholder() {
130
+ const { common } = useStrings();
131
+ return /* @__PURE__ */ jsxs("span", {
132
+ className: "flex items-center",
133
+ children: [/* @__PURE__ */ jsx(Skeleton, {
134
+ width: "6rem",
135
+ height: "0.8125rem"
136
+ }), /* @__PURE__ */ jsx("span", {
137
+ className: "sr-only",
138
+ children: common.loading
139
+ })]
140
+ });
141
+ }
142
+ var FOLD_WIDTH = 48;
143
+ var PARENT_MAX_WIDTH = 144;
144
+ var LAST_MIN_WIDTH = 128;
145
+ var px = (value) => Number.parseFloat(value) || 0;
146
+ /** The width the trail may take. A flex row sizes the trail to its content, so
147
+ * its room is what the row leaves beside its other items; anywhere else the
148
+ * `nav` is a block that fills its container. */
149
+ function roomFor(nav, row) {
150
+ const style = getComputedStyle(row);
151
+ if (!style.display.endsWith("flex") || style.flexDirection.startsWith("column")) return nav.clientWidth;
152
+ const gap = px(style.columnGap);
153
+ let room = row.clientWidth - px(style.paddingLeft) - px(style.paddingRight);
154
+ for (const child of row.children) if (child !== nav) room -= child.getBoundingClientRect().width + gap;
155
+ return room;
156
+ }
157
+ /**
158
+ * Fits the trail to its room. It measures a hidden copy of the trail at its
159
+ * natural width, so the answer doesn't depend on how the trail is folded at the
160
+ * moment.
161
+ */
162
+ function useTrailFit(nav, natural) {
163
+ const [fit, setFit] = useState("all");
164
+ useLayoutEffect(() => {
165
+ const row = nav?.parentElement;
166
+ if (!nav || !natural || !row) return;
167
+ const measure = () => {
168
+ const room = roomFor(nav, row);
169
+ if (natural.scrollWidth <= room) {
170
+ setFit("all");
171
+ return;
172
+ }
173
+ const steps = [...natural.children];
174
+ const width = (el) => el?.getBoundingClientRect().width ?? 0;
175
+ const parent = Math.min(width(steps[steps.length - 2]), PARENT_MAX_WIDTH);
176
+ const last = Math.min(width(steps[steps.length - 1]), LAST_MIN_WIDTH);
177
+ setFit(FOLD_WIDTH + parent + last <= room ? "parent" : "none");
178
+ };
179
+ const resize = new ResizeObserver(measure);
180
+ const observe = () => {
181
+ for (const el of [
182
+ row,
183
+ nav,
184
+ natural,
185
+ ...row.children
186
+ ]) resize.observe(el);
187
+ };
188
+ const added = new MutationObserver(() => {
189
+ observe();
190
+ measure();
191
+ });
192
+ measure();
193
+ observe();
194
+ added.observe(row, { childList: true });
195
+ return () => {
196
+ resize.disconnect();
197
+ added.disconnect();
198
+ };
199
+ }, [nav, natural]);
200
+ return fit;
201
+ }
202
+ //#endregion
203
+ export { Breadcrumbs };
@@ -49,6 +49,7 @@ var labelSizes = {
49
49
  };
50
50
  var labelClassName = "flex items-center gap-1 font-medium";
51
51
  var actionsClassName = "ml-auto flex shrink-0 items-center gap-1";
52
+ var controlFirstExtrasClassName = "ms-1 inline-flex items-center gap-1 align-[-0.125em]";
52
53
  var errorTextClassName = "flex items-center gap-1.5 text-xs text-(--theme-error-text)";
53
54
  /** An error in the look a Field gives its own, for a control outside a Field, or for a part of
54
55
  * one rather than the whole of it, such as a row of a list editor. Give it an `id` and name it
@@ -188,7 +189,7 @@ function Field$1({ label, hint, help, actions, description, error, invalid, requ
188
189
  className: "w-52 shrink-0"
189
190
  }), /* @__PURE__ */ jsxs("div", {
190
191
  ref: help ? setControlBox : void 0,
191
- className: cx("flex w-fit max-w-full min-w-0 items-start gap-2", lined && labelSize.inlineLine, inRow && "row-start-2"),
192
+ className: cx("flex w-fit max-w-full min-w-0 items-start gap-2", lined ? labelSize.inlineLine : "py-0.5", inRow && "row-start-2"),
192
193
  children: [/* @__PURE__ */ jsx("div", {
193
194
  className: cx("flex h-lh shrink-0 items-center", labelSize.text),
194
195
  ...helpFocus,
@@ -197,10 +198,21 @@ function Field$1({ label, hint, help, actions, description, error, invalid, requ
197
198
  className: "flex min-w-0 flex-col gap-1",
198
199
  children: [/* @__PURE__ */ jsxs("div", {
199
200
  className: cx("font-medium", labelSize.text, disabled && "opacity-60"),
200
- children: [fieldLabel(disabled ? "cursor-not-allowed" : "cursor-pointer"), (help || hint || actions) && /* @__PURE__ */ jsx("span", {
201
- className: "ms-1 inline-flex items-center gap-1 align-[-0.125em]",
202
- children: labelExtras
203
- })]
201
+ children: [
202
+ fieldLabel(disabled ? "cursor-not-allowed" : "cursor-pointer"),
203
+ helpTip && /* @__PURE__ */ jsx("span", {
204
+ className: controlFirstExtrasClassName,
205
+ children: helpTip
206
+ }),
207
+ hint && /* @__PURE__ */ jsx("span", {
208
+ className: "ms-1",
209
+ children: hint
210
+ }),
211
+ actions && /* @__PURE__ */ jsx("span", {
212
+ className: controlFirstExtrasClassName,
213
+ children: actions
214
+ })
215
+ ]
204
216
  }), messages]
205
217
  })]
206
218
  })]
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
- import { type Breadcrumb } from './pageHeaderStore.js';
3
- export type { Breadcrumb } from './pageHeaderStore.js';
2
+ import { type Breadcrumb } from './Breadcrumbs.js';
3
+ export type { Breadcrumb } from './Breadcrumbs.js';
4
4
  /**
5
5
  * Holds the header the current page declares, for the `PageHeaderBar` the
6
6
  * shell renders. Wrap the shell, above both the bar and the routes.
@@ -1,12 +1,6 @@
1
1
  import { cx } from "../cx.js";
2
- import { buttonClasses } from "./buttonClasses.js";
3
- import { ChevronRightIcon, MoreIcon } from "../icons.js";
4
- import { keyedByContent } from "./keys.js";
5
- import { Tooltip } from "./Tooltip.js";
6
- import { useSlots, useStrings } from "./Provider.js";
7
- import { AppLink } from "./AppLink.js";
8
- import { ActionMenu } from "./ActionMenu.js";
9
- import { Skeleton } from "./Skeleton.js";
2
+ import { useSlots } from "./Provider.js";
3
+ import { Breadcrumbs } from "./Breadcrumbs.js";
10
4
  import { PageHeaderStore } from "./pageHeaderStore.js";
11
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
6
  import { createContext, use, useCallback, useLayoutEffect, useState, useSyncExternalStore } from "react";
@@ -146,7 +140,10 @@ function HeaderLayout({ breadcrumbs, titleActions, actions, toolbar, compact = f
146
140
  className: "flex min-w-0 flex-1 items-center gap-2",
147
141
  children: [
148
142
  navToggle?.(),
149
- hasTrail && /* @__PURE__ */ jsx(BreadcrumbTrail, { crumbs: breadcrumbs }),
143
+ hasTrail && /* @__PURE__ */ jsx(Breadcrumbs, {
144
+ crumbs: breadcrumbs,
145
+ heading: true
146
+ }),
150
147
  present(titleActions) && /* @__PURE__ */ jsx("div", {
151
148
  className: "flex shrink-0 items-center gap-0.5",
152
149
  children: titleActions
@@ -163,141 +160,5 @@ function HeaderLayout({ breadcrumbs, titleActions, actions, toolbar, compact = f
163
160
  })]
164
161
  });
165
162
  }
166
- var crumbText = "block truncate text-(--theme-muted-text-color)";
167
- var crumbLink = "rounded-sm transition-colors hover:text-(--theme-app) focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-(--theme-link)";
168
- function Separator() {
169
- return /* @__PURE__ */ jsx(ChevronRightIcon, { className: "size-3 shrink-0 text-(--theme-muted-text-color) opacity-60" });
170
- }
171
- var FOLD_WIDTH = 48;
172
- var PARENT_MAX_WIDTH = 144;
173
- var TITLE_MIN_WIDTH = 128;
174
- /**
175
- * Fits the trail to the room its row leaves it. It measures a hidden copy of
176
- * the trail at its natural width against the row's width less its other items,
177
- * so the answer doesn't depend on how the trail is folded at the moment.
178
- */
179
- function useTrailFit(nav, natural) {
180
- const [fit, setFit] = useState("all");
181
- useLayoutEffect(() => {
182
- const row = nav?.parentElement;
183
- if (!nav || !natural || !row) return;
184
- const measure = () => {
185
- const gap = Number.parseFloat(getComputedStyle(row).columnGap) || 0;
186
- let room = row.clientWidth;
187
- for (const child of row.children) if (child !== nav) room -= child.getBoundingClientRect().width + gap;
188
- if (natural.scrollWidth <= room) {
189
- setFit("all");
190
- return;
191
- }
192
- const steps = [...natural.children];
193
- const width = (el) => el?.getBoundingClientRect().width ?? 0;
194
- const parent = Math.min(width(steps[steps.length - 2]), PARENT_MAX_WIDTH);
195
- const title = Math.min(width(steps[steps.length - 1]), TITLE_MIN_WIDTH);
196
- setFit(FOLD_WIDTH + parent + title <= room ? "parent" : "none");
197
- };
198
- measure();
199
- const observer = new ResizeObserver(measure);
200
- observer.observe(row);
201
- observer.observe(natural);
202
- return () => observer.disconnect();
203
- }, [nav, natural]);
204
- return fit;
205
- }
206
- function BreadcrumbTrail({ crumbs }) {
207
- const { common } = useStrings();
208
- const [nav, setNav] = useState(null);
209
- const [natural, setNatural] = useState(null);
210
- const fit = useTrailFit(nav, natural);
211
- const current = crumbs[crumbs.length - 1];
212
- const ancestors = crumbs.slice(0, -1);
213
- const folded = fit === "none" ? ancestors : fit === "parent" ? ancestors.slice(0, -1) : [];
214
- const shown = ancestors.slice(folded.length);
215
- return /* @__PURE__ */ jsxs("nav", {
216
- ref: setNav,
217
- "aria-label": common.breadcrumb,
218
- className: "relative min-w-0",
219
- children: [/* @__PURE__ */ jsxs("ol", {
220
- className: "m-0 flex min-w-0 list-none items-center gap-1.5 p-0 text-[13px] leading-none",
221
- children: [
222
- folded.length > 0 && /* @__PURE__ */ jsxs("li", {
223
- className: "flex shrink-0 items-center gap-1.5",
224
- children: [/* @__PURE__ */ jsx(ActionMenu, {
225
- label: common.showFullPath,
226
- align: "start",
227
- items: folded.map((crumb) => crumb.href ? {
228
- label: crumb.label,
229
- href: crumb.href
230
- } : {
231
- label: crumb.label,
232
- disabled: true,
233
- onClick: () => {}
234
- }),
235
- triggerClassName: buttonClasses({
236
- variant: "ghost",
237
- size: "xs",
238
- iconOnly: true
239
- }),
240
- children: /* @__PURE__ */ jsx(MoreIcon, {})
241
- }), /* @__PURE__ */ jsx(Separator, {})]
242
- }),
243
- keyedByContent(shown, (c) => c.href ?? c.label).map(({ key, item: crumb }) => /* @__PURE__ */ jsxs("li", {
244
- className: cx("flex items-center gap-1.5", folded.length > 0 ? "min-w-0 max-w-36 shrink-0" : "min-w-0"),
245
- children: [crumb.label === "" ? /* @__PURE__ */ jsx(CrumbPlaceholder, {}) : /* @__PURE__ */ jsx(Tooltip, {
246
- content: crumb.label,
247
- overflowOnly: true,
248
- children: crumb.href ? /* @__PURE__ */ jsx(AppLink, {
249
- href: crumb.href,
250
- className: cx(crumbText, crumbLink, "max-w-48"),
251
- children: crumb.label
252
- }) : /* @__PURE__ */ jsx("span", {
253
- className: cx(crumbText, "max-w-48"),
254
- children: crumb.label
255
- })
256
- }), /* @__PURE__ */ jsx(Separator, {})]
257
- }, key)),
258
- current && /* @__PURE__ */ jsx("li", {
259
- className: "flex min-w-0 items-center",
260
- children: current.label === "" ? /* @__PURE__ */ jsx("h1", {
261
- "aria-current": "page",
262
- className: "m-0",
263
- children: /* @__PURE__ */ jsx(CrumbPlaceholder, {})
264
- }) : /* @__PURE__ */ jsx(Tooltip, {
265
- content: current.label,
266
- overflowOnly: true,
267
- children: /* @__PURE__ */ jsx("h1", {
268
- "aria-current": "page",
269
- className: "m-0 truncate text-[13px] font-medium leading-none text-(--theme-app)",
270
- children: current.label
271
- })
272
- })
273
- })
274
- ]
275
- }), /* @__PURE__ */ jsx("div", {
276
- ref: setNatural,
277
- "aria-hidden": true,
278
- className: "invisible absolute top-0 left-0 flex items-center gap-1.5 whitespace-nowrap text-[13px] leading-none",
279
- children: crumbs.map((crumb, i) => /* @__PURE__ */ jsxs("span", {
280
- className: "flex items-center gap-1.5",
281
- children: [/* @__PURE__ */ jsx("span", {
282
- className: cx(i < crumbs.length - 1 && "max-w-48 truncate", i === crumbs.length - 1 && "font-medium"),
283
- children: crumb.label || " ".repeat(6)
284
- }), i < crumbs.length - 1 && /* @__PURE__ */ jsx(Separator, {})]
285
- }, i))
286
- })]
287
- });
288
- }
289
- function CrumbPlaceholder() {
290
- const { common } = useStrings();
291
- return /* @__PURE__ */ jsxs("span", {
292
- className: "flex items-center",
293
- children: [/* @__PURE__ */ jsx(Skeleton, {
294
- width: "6rem",
295
- height: "0.8125rem"
296
- }), /* @__PURE__ */ jsx("span", {
297
- className: "sr-only",
298
- children: common.loading
299
- })]
300
- });
301
- }
302
163
  //#endregion
303
164
  export { PageHeader, PageHeaderBar, PageHeaderProvider };
@@ -641,7 +641,7 @@ export interface UIData {
641
641
  usePersistedState: PersistedStateHook;
642
642
  }
643
643
  import type { FieldComponent } from '../form/fieldRegistry.js';
644
- import type { Breadcrumb } from './pageHeaderStore.js';
644
+ import type { Breadcrumb } from './Breadcrumbs.js';
645
645
  /** An internal navigation link, which hosts bind to their router's Link. Its
646
646
  * `href` is a full in-app URL that may carry a search and a hash, such as
647
647
  * `/runs/42?tab=logs#step-3`, and the host routes all of it client-side on a
@@ -1,9 +1,4 @@
1
- /** One step of the trail to the current page. */
2
- export interface Breadcrumb {
3
- /** Empty while the name is still loading: the bar shows a placeholder. */
4
- label: string;
5
- href?: string | undefined;
6
- }
1
+ import type { Breadcrumb } from './Breadcrumbs.js';
7
2
  export type PageHeaderRegion = 'titleActions' | 'actions' | 'toolbar';
8
3
  /** The nodes a declaration portals its regions into. They are created with the
9
4
  * declaration, before any bar exists, so its content renders on the first pass
@@ -1,6 +1,6 @@
1
1
  import { cx } from "../cx.js";
2
2
  import { buttonClasses } from "../components/buttonClasses.js";
3
- import { AccessIcon, ChevronDownIcon, ChevronRightIcon, CollapseIcon, DownloadIcon, FileIcon, FileTextIcon, FolderIcon, FolderTreeIcon, LinkIcon, PreviewIcon, ShareIcon, TrashIcon, UploadIcon } from "../icons.js";
3
+ import { AccessIcon, ChevronDownIcon, CollapseIcon, DownloadIcon, FileIcon, FileTextIcon, FolderIcon, FolderTreeIcon, LinkIcon, PreviewIcon, ShareIcon, TrashIcon, UploadIcon } from "../icons.js";
4
4
  import { MenuItem, MenuPopup, MenuSeparator } from "../components/Menu.js";
5
5
  import { safeUrl } from "../safeUrl.js";
6
6
  import { Loader } from "../components/Loader.js";
@@ -8,8 +8,8 @@ import { toast } from "../components/Toast.js";
8
8
  import { keyedByContent } from "../components/keys.js";
9
9
  import { Tooltip } from "../components/Tooltip.js";
10
10
  import { useNavigation, usePersistedState, useSlots, useStrings } from "../components/Provider.js";
11
- import { AppLink } from "../components/AppLink.js";
12
11
  import { useCopy } from "../components/useCopy.js";
12
+ import { Breadcrumbs } from "../components/Breadcrumbs.js";
13
13
  import { Button } from "../components/Button.js";
14
14
  import { Callout } from "../components/Callout.js";
15
15
  import { InfoTip } from "../components/InfoTip.js";
@@ -68,7 +68,6 @@ function UriCard({ value }) {
68
68
  ]
69
69
  });
70
70
  }
71
- var breadcrumbClass = "max-w-[16rem] cursor-pointer truncate text-(--theme-muted-text-color) transition-colors hover:text-(--theme-app)";
72
71
  /** `href` resolved against the page, as the browser's "Copy link" gives it. */
73
72
  function absoluteUrl(href) {
74
73
  const url = safeUrl(href);
@@ -720,36 +719,17 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
720
719
  cur = treeMap[getParentPath(cur.path)];
721
720
  }
722
721
  if (chain.length === 0) return null;
723
- return /* @__PURE__ */ jsx("nav", {
724
- "aria-label": t.chrome.location,
725
- className: "flex min-w-0 flex-1 overflow-hidden text-[13px]",
726
- children: /* @__PURE__ */ jsx("ol", {
727
- className: "flex min-w-0 items-center gap-1",
728
- children: chain.map((node, i) => {
729
- const isLast = i === chain.length - 1;
730
- const label = node.displayName || node.name;
731
- return /* @__PURE__ */ jsxs("li", {
732
- className: "flex min-w-0 items-center gap-1",
733
- children: [i > 0 && /* @__PURE__ */ jsx(ChevronRightIcon, {
734
- "aria-hidden": "true",
735
- className: "h-3 w-3 shrink-0 text-(--theme-muted-text-color) rtl:rotate-180"
736
- }), isLast ? /* @__PURE__ */ jsx("span", {
737
- "aria-current": "location",
738
- className: "truncate font-semibold text-(--theme-app)",
739
- children: label
740
- }) : pathHref ? /* @__PURE__ */ jsx(AppLink, {
741
- href: pathHref(node.path),
742
- className: breadcrumbClass,
743
- children: label
744
- }) : /* @__PURE__ */ jsx("button", {
745
- type: "button",
746
- onClick: () => handleSelectTreeItem(node.path, node),
747
- className: breadcrumbClass,
748
- children: label
749
- })]
750
- }, node.path);
751
- })
752
- })
722
+ return /* @__PURE__ */ jsx(Breadcrumbs, {
723
+ label: t.chrome.location,
724
+ current: "location",
725
+ crumbs: chain.map((node) => ({
726
+ label: node.displayName || node.name,
727
+ href: pathHref?.(node.path)
728
+ })),
729
+ onSelect: (_, i) => {
730
+ const node = chain[i];
731
+ if (node) handleSelectTreeItem(node.path, node);
732
+ }
753
733
  });
754
734
  };
755
735
  const generateDownloadCommand = () => {
@@ -2,10 +2,10 @@ import { cx } from "../cx.js";
2
2
  import { CustomizeIcon, DiskIcon, FileIcon, FileTextIcon, FolderIcon, ImageIcon, PdfIcon, RefreshIcon, RetryIcon, ShareIcon, SpreadsheetIcon, UserIcon, VideoIcon, WarningIcon, ZipIcon } from "../icons.js";
3
3
  import { toast } from "../components/Toast.js";
4
4
  import { usePersistedState, useProvisionCorsRules, useSlots, useStrings } from "../components/Provider.js";
5
+ import { Skeleton } from "../components/Skeleton.js";
5
6
  import { Button } from "../components/Button.js";
6
7
  import { Callout } from "../components/Callout.js";
7
8
  import { ProgressBar } from "../components/Meter.js";
8
- import { Skeleton } from "../components/Skeleton.js";
9
9
  import { formatBytes } from "../format.js";
10
10
  import { useElapsed } from "../components/relativeTime.js";
11
11
  import { ListSkeleton } from "../list/ListTable.js";