@parallelworks/ui 0.92.0 → 0.94.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.
@@ -0,0 +1,369 @@
1
+ import { cx } from "../cx.js";
2
+ import { CloseIcon, SidebarCloseIcon, SidebarOpenIcon } from "../icons.js";
3
+ import { Tooltip } from "../components/Tooltip.js";
4
+ import { AppLink, isPlainClick } from "../components/AppLink.js";
5
+ import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
6
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
+ import { Dialog } from "@base-ui/react/dialog";
8
+ //#region src/app-frame/index.tsx
9
+ var defaultLabels = {
10
+ navigation: "Main navigation",
11
+ skipToContent: "Skip to content",
12
+ openNavigation: "Open navigation",
13
+ closeNavigation: "Close navigation",
14
+ expandSidebar: "Expand sidebar",
15
+ collapseSidebar: "Collapse sidebar",
16
+ resizeSidebar: "Resize sidebar"
17
+ };
18
+ var Context = createContext(null);
19
+ function useAppFrame() {
20
+ const context = useContext(Context);
21
+ if (!context) throw new Error("useAppFrame requires AppFrame");
22
+ return context;
23
+ }
24
+ function useFrameContext() {
25
+ const context = useContext(Context);
26
+ if (!context) throw new Error("App frame controls require AppFrame");
27
+ return context;
28
+ }
29
+ function useMobile(breakpoint) {
30
+ const media = useMemo(() => typeof window === "undefined" ? null : window.matchMedia(`(max-width: ${breakpoint - 1}px)`), [breakpoint]);
31
+ return useSyncExternalStore(useMemo(() => (changed) => {
32
+ media?.addEventListener("change", changed);
33
+ return () => media?.removeEventListener("change", changed);
34
+ }, [media]), () => media?.matches ?? false, () => false);
35
+ }
36
+ function slot(value, state) {
37
+ return typeof value === "function" ? value(state) : value;
38
+ }
39
+ /** An inset application surface beside a resizable icon rail or navigation column.
40
+ * Router, identity, persistence and permissions belong to the host. Import app-frame.css. */
41
+ function AppFrame({ children, brand, navigation, footer, header, banner, expanded: controlledExpanded, defaultExpanded = true, onExpandedChange, width: controlledWidth, defaultWidth = 240, onWidthChange, minWidth = 200, maxWidth = 400, railWidth = 64, resizable = true, mobileBreakpoint = 768, navigationKey, navigationResetKey = navigationKey, sidebarProps, fullBleed = false, labels: labelOverrides, mainId: suppliedMainId, mainClassName, className, style, ...props }) {
42
+ const id = useId();
43
+ const mainId = suppliedMainId ?? `${id}-main`;
44
+ const navigationId = `${id}-navigation`;
45
+ const [ownExpanded, setOwnExpanded] = useState(defaultExpanded);
46
+ const [ownWidth, setOwnWidth] = useState(defaultWidth);
47
+ const [dragWidth, setDragWidth] = useState(null);
48
+ const [mobileOpen, setMobileOpen] = useState(false);
49
+ const [resizing, setResizing] = useState(false);
50
+ const desktopExpanded = controlledExpanded ?? ownExpanded;
51
+ const minimum = Math.max(railWidth, minWidth);
52
+ const maximum = Math.max(minimum, maxWidth);
53
+ const clamp = (n) => Math.min(maximum, Math.max(minimum, Number.isFinite(n) ? n : defaultWidth));
54
+ const width = clamp(dragWidth ?? controlledWidth ?? ownWidth);
55
+ const isMobile = useMobile(mobileBreakpoint);
56
+ const labels = {
57
+ ...defaultLabels,
58
+ ...labelOverrides
59
+ };
60
+ const navRef = useRef(null);
61
+ const scrollRef = useRef(null);
62
+ const savedScrollTop = useRef(0);
63
+ const restoreScroll = useCallback((node) => {
64
+ scrollRef.current = node;
65
+ if (node) node.scrollTop = savedScrollTop.current;
66
+ }, []);
67
+ const portalRef = useRef(null);
68
+ const drag = useRef(null);
69
+ useEffect(() => {
70
+ if (!isMobile && resizable) return;
71
+ drag.current = null;
72
+ setDragWidth(null);
73
+ setResizing(false);
74
+ }, [isMobile, resizable]);
75
+ const setExpanded = (next) => {
76
+ setOwnExpanded(next);
77
+ onExpandedChange?.(next);
78
+ };
79
+ const setWidth = (next) => {
80
+ const value = clamp(next);
81
+ setOwnWidth(value);
82
+ onWidthChange?.(value);
83
+ };
84
+ const closeNavigation = () => setMobileOpen(false);
85
+ const state = {
86
+ expanded: isMobile || desktopExpanded,
87
+ isMobile,
88
+ mobileOpen,
89
+ toggle: () => isMobile ? setMobileOpen(!mobileOpen) : setExpanded(!desktopExpanded),
90
+ closeNavigation,
91
+ labels,
92
+ navigationId
93
+ };
94
+ const previousLocation = useRef({
95
+ navigationKey,
96
+ navigationResetKey,
97
+ isMobile
98
+ });
99
+ useEffect(() => {
100
+ if (previousLocation.current.navigationKey === navigationKey && previousLocation.current.navigationResetKey === navigationResetKey && previousLocation.current.isMobile === isMobile) return;
101
+ if (previousLocation.current.navigationKey !== navigationKey || previousLocation.current.isMobile !== isMobile) setMobileOpen(false);
102
+ if (previousLocation.current.navigationResetKey !== navigationResetKey || previousLocation.current.isMobile !== isMobile) {
103
+ savedScrollTop.current = 0;
104
+ scrollRef.current?.scrollTo({ top: 0 });
105
+ }
106
+ previousLocation.current = {
107
+ navigationKey,
108
+ navigationResetKey,
109
+ isMobile
110
+ };
111
+ }, [
112
+ navigationKey,
113
+ navigationResetKey,
114
+ isMobile
115
+ ]);
116
+ const navContent = /* @__PURE__ */ jsxs(Fragment$1, { children: [
117
+ brand && /* @__PURE__ */ jsx("div", {
118
+ className: "pw-frame-brand",
119
+ children: slot(brand, state)
120
+ }),
121
+ /* @__PURE__ */ jsx("div", {
122
+ className: "pw-frame-links",
123
+ ref: restoreScroll,
124
+ onScroll: (event) => {
125
+ savedScrollTop.current = event.currentTarget.scrollTop;
126
+ },
127
+ children: slot(navigation, state)
128
+ }),
129
+ footer && /* @__PURE__ */ jsx("div", {
130
+ className: "pw-frame-footer",
131
+ children: slot(footer, state)
132
+ })
133
+ ] });
134
+ const resize = (clientX, pointerId) => {
135
+ const current = drag.current;
136
+ if (!current || current.pointerId !== pointerId) return;
137
+ const next = (clientX - current.origin) * current.direction;
138
+ if (desktopExpanded && next < minimum - 80) setExpanded(false);
139
+ else if (!desktopExpanded && next >= minimum - 50) {
140
+ setExpanded(true);
141
+ current.width = clamp(next);
142
+ setDragWidth(current.width);
143
+ } else if (desktopExpanded) {
144
+ current.width = clamp(next);
145
+ setDragWidth(current.width);
146
+ }
147
+ };
148
+ const endResize = (event) => {
149
+ const ended = drag.current;
150
+ if (!ended || ended.pointerId !== event.pointerId) return;
151
+ drag.current = null;
152
+ setResizing(false);
153
+ setDragWidth(null);
154
+ if (desktopExpanded && ended.width !== ended.startWidth) setWidth(ended.width);
155
+ };
156
+ return /* @__PURE__ */ jsx(Context, {
157
+ value: state,
158
+ children: /* @__PURE__ */ jsxs("div", {
159
+ ...props,
160
+ className: cx("pw-app-frame", className),
161
+ "data-mobile": isMobile || void 0,
162
+ "data-full-bleed": fullBleed || void 0,
163
+ style,
164
+ children: [
165
+ /* @__PURE__ */ jsx("a", {
166
+ className: "pw-frame-skip",
167
+ href: `#${mainId}`,
168
+ onClick: (event) => {
169
+ event.preventDefault();
170
+ document.getElementById(mainId)?.focus();
171
+ },
172
+ children: labels.skipToContent
173
+ }),
174
+ banner && /* @__PURE__ */ jsx("div", {
175
+ className: "pw-frame-banner",
176
+ children: banner
177
+ }),
178
+ /* @__PURE__ */ jsxs("div", {
179
+ className: "pw-frame-body",
180
+ children: [
181
+ !isMobile && /* @__PURE__ */ jsxs("aside", {
182
+ ...sidebarProps,
183
+ ref: navRef,
184
+ id: navigationId,
185
+ className: cx("pw-frame-sidebar", sidebarProps?.className),
186
+ "data-sidebar-mode": desktopExpanded ? "expanded" : "rail",
187
+ "data-resizing": resizing || void 0,
188
+ style: {
189
+ ...sidebarProps?.style,
190
+ width: desktopExpanded ? width : railWidth
191
+ },
192
+ children: [navContent, resizable && /* @__PURE__ */ jsx("div", {
193
+ role: "separator",
194
+ tabIndex: 0,
195
+ "aria-orientation": "vertical",
196
+ "aria-label": labels.resizeSidebar,
197
+ "aria-controls": navigationId,
198
+ "aria-valuemin": railWidth,
199
+ "aria-valuemax": maximum,
200
+ "aria-valuenow": Math.round(desktopExpanded ? width : railWidth),
201
+ className: "pw-frame-resize",
202
+ onPointerDown: (event) => {
203
+ if (event.button !== 0 || event.isPrimary === false || drag.current) return;
204
+ event.preventDefault();
205
+ const rect = navRef.current?.getBoundingClientRect();
206
+ if (!rect) return;
207
+ const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
208
+ drag.current = {
209
+ origin: rtl ? rect.right : rect.left,
210
+ direction: rtl ? -1 : 1,
211
+ pointerId: event.pointerId,
212
+ startWidth: width,
213
+ width
214
+ };
215
+ event.currentTarget.setPointerCapture(event.pointerId);
216
+ setResizing(true);
217
+ },
218
+ onPointerMove: (event) => resize(event.clientX, event.pointerId),
219
+ onPointerUp: endResize,
220
+ onPointerCancel: endResize,
221
+ onLostPointerCapture: endResize,
222
+ onDoubleClick: () => {
223
+ setExpanded(true);
224
+ setWidth(defaultWidth);
225
+ },
226
+ onKeyDown: (event) => {
227
+ const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
228
+ const smaller = rtl ? "ArrowRight" : "ArrowLeft";
229
+ if (![
230
+ smaller,
231
+ rtl ? "ArrowLeft" : "ArrowRight",
232
+ "Home",
233
+ "End"
234
+ ].includes(event.key)) return;
235
+ event.preventDefault();
236
+ if (event.key === "Home") {
237
+ setExpanded(true);
238
+ setWidth(minimum);
239
+ } else if (event.key === "End") {
240
+ setExpanded(true);
241
+ setWidth(maximum);
242
+ } else if (event.key === smaller) {
243
+ if (width <= minimum) setExpanded(false);
244
+ else if (desktopExpanded) setWidth(width - 16);
245
+ } else if (!desktopExpanded) setExpanded(true);
246
+ else setWidth(width + 16);
247
+ }
248
+ })]
249
+ }),
250
+ /* @__PURE__ */ jsx("div", {
251
+ className: "pw-frame-surface-wrap",
252
+ children: /* @__PURE__ */ jsxs("div", {
253
+ className: "pw-frame-surface",
254
+ children: [header && /* @__PURE__ */ jsx("div", {
255
+ className: "pw-frame-header",
256
+ children: header
257
+ }), /* @__PURE__ */ jsx("main", {
258
+ id: mainId,
259
+ tabIndex: -1,
260
+ className: cx("pw-frame-main", mainClassName),
261
+ children
262
+ })]
263
+ })
264
+ }),
265
+ /* @__PURE__ */ jsx("div", { ref: portalRef })
266
+ ]
267
+ }),
268
+ isMobile && /* @__PURE__ */ jsx(Dialog.Root, {
269
+ open: mobileOpen,
270
+ onOpenChange: setMobileOpen,
271
+ children: /* @__PURE__ */ jsxs(Dialog.Portal, {
272
+ container: portalRef,
273
+ children: [/* @__PURE__ */ jsx(Dialog.Backdrop, { className: "pw-frame-backdrop" }), /* @__PURE__ */ jsxs(Dialog.Popup, {
274
+ ...sidebarProps,
275
+ id: navigationId,
276
+ className: cx("pw-frame-drawer", sidebarProps?.className),
277
+ "aria-label": labels.navigation,
278
+ "aria-modal": "true",
279
+ "data-sidebar-mode": "drawer",
280
+ style: {
281
+ ...sidebarProps?.style,
282
+ width
283
+ },
284
+ children: [/* @__PURE__ */ jsx("div", {
285
+ className: "pw-frame-mobile-close",
286
+ children: /* @__PURE__ */ jsx(Dialog.Close, {
287
+ className: "pw-frame-toggle",
288
+ "aria-label": labels.closeNavigation,
289
+ children: /* @__PURE__ */ jsx(CloseIcon, {})
290
+ })
291
+ }), navContent]
292
+ })]
293
+ })
294
+ })
295
+ ]
296
+ })
297
+ });
298
+ }
299
+ /** Put in the page header on mobile, and the sidebar footer on desktop. */
300
+ function AppFrameToggle({ className, ...props }) {
301
+ const { expanded, isMobile, mobileOpen, toggle, labels, navigationId } = useFrameContext();
302
+ const label = isMobile ? mobileOpen ? labels.closeNavigation : labels.openNavigation : expanded ? labels.collapseSidebar : labels.expandSidebar;
303
+ return /* @__PURE__ */ jsx(Tooltip, {
304
+ content: label,
305
+ side: isMobile ? "bottom" : "right",
306
+ children: /* @__PURE__ */ jsx("button", {
307
+ ...props,
308
+ type: "button",
309
+ className: cx("pw-frame-toggle", className),
310
+ "aria-label": label,
311
+ "aria-expanded": isMobile ? mobileOpen : expanded,
312
+ "aria-controls": navigationId,
313
+ onClick: (event) => {
314
+ props.onClick?.(event);
315
+ if (!event.defaultPrevented) toggle();
316
+ },
317
+ children: expanded && !isMobile ? /* @__PURE__ */ jsx(SidebarCloseIcon, {}) : /* @__PURE__ */ jsx(SidebarOpenIcon, {})
318
+ })
319
+ });
320
+ }
321
+ /** Router-aware navigation that keeps icon-only rail items named and discoverable. */
322
+ function AppFrameNavItem({ label, icon, selected, badge, disabled, onClick, className, ...target }) {
323
+ const { expanded, closeNavigation } = useFrameContext();
324
+ const props = {
325
+ className: cx("pw-frame-nav-item", className),
326
+ "aria-current": selected ? "page" : void 0,
327
+ onClick: (event) => {
328
+ if (disabled) {
329
+ event.preventDefault();
330
+ return;
331
+ }
332
+ onClick?.(event);
333
+ if (!event.defaultPrevented && isPlainClick(event)) closeNavigation();
334
+ },
335
+ children: /* @__PURE__ */ jsxs(Fragment$1, { children: [
336
+ /* @__PURE__ */ jsx("span", {
337
+ className: "pw-frame-nav-icon",
338
+ "aria-hidden": "true",
339
+ children: icon
340
+ }),
341
+ /* @__PURE__ */ jsx("span", {
342
+ className: expanded ? "pw-frame-nav-label" : "sr-only",
343
+ children: label
344
+ }),
345
+ expanded && badge != null && /* @__PURE__ */ jsx("span", {
346
+ className: "pw-frame-nav-badge",
347
+ "aria-hidden": "true",
348
+ children: badge
349
+ })
350
+ ] })
351
+ };
352
+ const item = target.href ? /* @__PURE__ */ jsx(AppLink, {
353
+ ...target,
354
+ ...props,
355
+ disabled
356
+ }) : /* @__PURE__ */ jsx("button", {
357
+ ...props,
358
+ type: "button",
359
+ disabled
360
+ });
361
+ return expanded ? item : /* @__PURE__ */ jsx(Tooltip, {
362
+ content: label,
363
+ side: "right",
364
+ delay: 0,
365
+ children: item
366
+ });
367
+ }
368
+ //#endregion
369
+ export { AppFrame, AppFrameNavItem, AppFrameToggle, useAppFrame };
@@ -151,47 +151,62 @@ function HeaderLayout({ breadcrumbs, heading, titleActions, actions, toolbar, fi
151
151
  const [row, setRow] = useState(null);
152
152
  const [lead, setLead] = useState(null);
153
153
  const [end, setEnd] = useState(null);
154
+ const [cover, setCover] = useState(null);
154
155
  useActionsFit(fit, row, lead, end);
156
+ useLayoutEffect(() => {
157
+ fit.setCoverNode(cover);
158
+ return () => fit.setCoverNode(null);
159
+ }, [fit, cover]);
155
160
  useSyncExternalStore(fit.subscribe, fit.getVersion, fit.getVersion);
156
161
  const moved = fit.menu();
157
162
  const hasTrail = breadcrumbs.length > 0;
163
+ const covered = fit.isCovered();
158
164
  return /* @__PURE__ */ jsxs("div", {
159
165
  className: cx("@container w-full shrink-0", !inPlace && "bg-(--theme-app-bg)", className),
160
166
  children: [/* @__PURE__ */ jsxs("div", {
161
167
  ref: setRow,
162
- className: cx("flex items-center justify-between gap-4 border-b border-(--theme-border) px-4", compact ? "h-8" : "h-12"),
163
- children: [/* @__PURE__ */ jsxs("div", {
164
- ref: setLead,
165
- className: "flex min-w-0 flex-1 items-center gap-2",
166
- children: [
167
- navToggle?.(),
168
- hasTrail && /* @__PURE__ */ jsx(Breadcrumbs, {
169
- crumbs: breadcrumbs,
170
- heading
171
- }),
172
- present(titleActions) && /* @__PURE__ */ jsx("div", {
173
- className: "flex shrink-0 items-center gap-0.5",
174
- children: titleActions
175
- }),
176
- hasTrail && breadcrumbPin?.({ crumbs: breadcrumbs })
177
- ]
178
- }), present(actions) && /* @__PURE__ */ jsxs("div", {
179
- ref: setEnd,
180
- className: "flex shrink-0 items-center gap-1",
181
- children: [/* @__PURE__ */ jsx(HeaderActionsFitContext, {
182
- value: fit,
183
- children: actions
184
- }), moved.length > 0 && /* @__PURE__ */ jsx(ActionMenu, {
185
- items: moved,
186
- label: t.moreActions,
187
- triggerClassName: buttonClasses({
188
- variant: "ghost",
189
- size: "xs",
190
- iconOnly: true
191
- }),
192
- children: /* @__PURE__ */ jsx(MoreIcon, {})
193
- })]
194
- })]
168
+ className: cx("relative flex items-center justify-between gap-4 border-b border-(--theme-border) px-4", compact ? "h-8" : "h-12"),
169
+ children: [
170
+ /* @__PURE__ */ jsxs("div", {
171
+ ref: setLead,
172
+ inert: covered,
173
+ className: cx("flex min-w-0 flex-1 items-center gap-2", covered && "invisible"),
174
+ children: [
175
+ navToggle?.(),
176
+ hasTrail && /* @__PURE__ */ jsx(Breadcrumbs, {
177
+ crumbs: breadcrumbs,
178
+ heading
179
+ }),
180
+ present(titleActions) && /* @__PURE__ */ jsx("div", {
181
+ className: "flex shrink-0 items-center gap-0.5",
182
+ children: titleActions
183
+ }),
184
+ hasTrail && breadcrumbPin?.({ crumbs: breadcrumbs })
185
+ ]
186
+ }),
187
+ present(actions) && /* @__PURE__ */ jsxs("div", {
188
+ ref: setEnd,
189
+ inert: covered,
190
+ className: cx("flex shrink-0 items-center gap-1", covered && "invisible"),
191
+ children: [/* @__PURE__ */ jsx(HeaderActionsFitContext, {
192
+ value: fit,
193
+ children: actions
194
+ }), moved.length > 0 && /* @__PURE__ */ jsx(ActionMenu, {
195
+ items: moved,
196
+ label: t.moreActions,
197
+ triggerClassName: buttonClasses({
198
+ variant: "ghost",
199
+ size: "xs",
200
+ iconOnly: true
201
+ }),
202
+ children: /* @__PURE__ */ jsx(MoreIcon, {})
203
+ })]
204
+ }),
205
+ /* @__PURE__ */ jsx("div", {
206
+ ref: setCover,
207
+ className: "pointer-events-none absolute inset-0 flex items-center px-4"
208
+ })
209
+ ]
195
210
  }), present(toolbar) && /* @__PURE__ */ jsx("div", {
196
211
  className: "px-4",
197
212
  children: toolbar
@@ -243,10 +258,13 @@ function useActionsFit(fit, row, lead, end) {
243
258
  }
244
259
  leadMin += Math.max(0, leadItems - 1) * LEAD_GAP;
245
260
  const room = rowWidth - Number.parseFloat(style.paddingLeft || "0") - Number.parseFloat(style.paddingRight || "0") - leadMin - ROW_GAP;
261
+ const shown = end.getBoundingClientRect().width;
262
+ const moved = items.filter((id) => fit.isOverflowed(id));
246
263
  fit.setOverflow(overflowingActions(items.map((id) => ({
247
264
  id,
248
265
  width: width(id)
249
- })), end.getBoundingClientRect().width, items.filter((id) => fit.isOverflowed(id)), room));
266
+ })), shown, moved, room));
267
+ fit.setSearchCovers(searchCovers(end, items, moved, width, shown, room));
250
268
  };
251
269
  measure();
252
270
  const resize = new ResizeObserver(measure);
@@ -270,5 +288,23 @@ function useActionsFit(fit, row, lead, end) {
270
288
  widths
271
289
  ]);
272
290
  }
291
+ var SEARCH_FIELD_WIDTH = 288;
292
+ /**
293
+ * Whether a search in the actions, opened in place, would leave the trail too little room even
294
+ * with every action in the "…" menu, so it takes the whole bar instead. It works from the
295
+ * actions' width as they are now, whether the search shows as its button or its field, so the
296
+ * answer is the same either way.
297
+ */
298
+ function searchCovers(end, items, moved, width, shown, room) {
299
+ const control = end.querySelector("[data-header-search]")?.firstElementChild;
300
+ if (!control) return false;
301
+ let least = shown - control.getBoundingClientRect().width + SEARCH_FIELD_WIDTH;
302
+ const still = items.filter((id) => !moved.includes(id));
303
+ if (still.length > 0) {
304
+ least -= still.reduce((sum, id) => sum + width(id) + 4, 0);
305
+ if (moved.length === 0) least += 28;
306
+ }
307
+ return least > room + .5;
308
+ }
273
309
  //#endregion
274
310
  export { PageHeader, PageHeaderBar, PageHeaderProvider };
@@ -653,6 +653,8 @@ export interface UIStrings {
653
653
  /** Where the page sits, for `Pagination` too narrow for page numbers: it shows "5 / 12". */
654
654
  pageOfTotal: (page: number, total: number) => string;
655
655
  removeSearchTerm: (term: string) => string;
656
+ /** The search button's name while a query it holds narrows the list, its field closed. */
657
+ searchActive: (label: string) => string;
656
658
  };
657
659
  /** The chip row of a list's active filters, its Filter menu and its pickers. Field and option
658
660
  * labels come from the host's fields, already translated. */
@@ -634,7 +634,8 @@ var DEFAULTS = {
634
634
  pagination: "Pagination",
635
635
  page: (page) => `Page ${page}`,
636
636
  pageOfTotal: (page, total) => `Page ${page} of ${total}`,
637
- removeSearchTerm: (term) => `Remove filter ${term}`
637
+ removeSearchTerm: (term) => `Remove filter ${term}`,
638
+ searchActive: (label) => `${label}, on`
638
639
  },
639
640
  filters: {
640
641
  addFilter: "Filter",
@@ -9,6 +9,9 @@ import type { MenuEntry } from './MenuEntries.js';
9
9
  export declare class HeaderActionsFit {
10
10
  private entries;
11
11
  private overflow;
12
+ private covers;
13
+ private cover;
14
+ private coverers;
12
15
  private version;
13
16
  private listeners;
14
17
  subscribe: (listener: () => void) => (() => void);
@@ -20,11 +23,23 @@ export declare class HeaderActionsFit {
20
23
  /** The moved actions' rows, in the bar's order. */
21
24
  menu(): MenuEntry[];
22
25
  setOverflow(ids: readonly string[]): void;
26
+ /** Whether an open search would take the whole bar: its field, opened in place, wouldn't fit
27
+ * beside the trail even with every action in the "…" menu. The bar measures it. */
28
+ searchCovers(): boolean;
29
+ setSearchCovers(covers: boolean): void;
30
+ /** Where a search that takes the bar renders its field: a layer over the bar's row. */
31
+ coverNode(): HTMLElement | null;
32
+ setCoverNode(node: HTMLElement | null): void;
33
+ /** Whether a search has the bar now, so the bar hides and disables what it covers. */
34
+ isCovered(): boolean;
35
+ setCovering(id: string, covering: boolean): void;
23
36
  private changed;
24
37
  }
25
38
  /** Set around the header's `actions`, so the actions there can move into its
26
39
  * menu; title actions sit beside the title and never do. */
27
40
  export declare const HeaderActionsFitContext: import("react").Context<HeaderActionsFit | null>;
41
+ export declare const ACTION_GAP = 4;
42
+ export declare const MENU_BUTTON_WIDTH = 24;
28
43
  /**
29
44
  * The trailing actions to move into the "…" menu so the rest fit in `room`
30
45
  * pixels, last first, or none when everything fits. `shown` is the actions'
@@ -10,6 +10,9 @@ import { createContext } from "react";
10
10
  var HeaderActionsFit = class {
11
11
  entries = /* @__PURE__ */ new Map();
12
12
  overflow = [];
13
+ covers = false;
14
+ cover = null;
15
+ coverers = /* @__PURE__ */ new Set();
13
16
  version = 0;
14
17
  listeners = /* @__PURE__ */ new Set();
15
18
  subscribe = (listener) => {
@@ -46,6 +49,37 @@ var HeaderActionsFit = class {
46
49
  this.overflow = ids;
47
50
  this.changed();
48
51
  }
52
+ /** Whether an open search would take the whole bar: its field, opened in place, wouldn't fit
53
+ * beside the trail even with every action in the "…" menu. The bar measures it. */
54
+ searchCovers() {
55
+ return this.covers;
56
+ }
57
+ setSearchCovers(covers) {
58
+ if (covers !== this.covers) {
59
+ this.covers = covers;
60
+ this.changed();
61
+ }
62
+ }
63
+ /** Where a search that takes the bar renders its field: a layer over the bar's row. */
64
+ coverNode() {
65
+ return this.cover;
66
+ }
67
+ setCoverNode(node) {
68
+ if (node !== this.cover) {
69
+ this.cover = node;
70
+ this.changed();
71
+ }
72
+ }
73
+ /** Whether a search has the bar now, so the bar hides and disables what it covers. */
74
+ isCovered() {
75
+ return this.coverers.size > 0;
76
+ }
77
+ setCovering(id, covering) {
78
+ if (covering === this.coverers.has(id)) return;
79
+ if (covering) this.coverers.add(id);
80
+ else this.coverers.delete(id);
81
+ this.changed();
82
+ }
49
83
  changed() {
50
84
  this.version++;
51
85
  for (const listener of this.listeners) listener();
@@ -54,7 +88,6 @@ var HeaderActionsFit = class {
54
88
  /** Set around the header's `actions`, so the actions there can move into its
55
89
  * menu; title actions sit beside the title and never do. */
56
90
  var HeaderActionsFitContext = createContext(null);
57
- var ACTION_GAP = 4;
58
91
  /**
59
92
  * The trailing actions to move into the "…" menu so the rest fit in `room`
60
93
  * pixels, last first, or none when everything fits. `shown` is the actions'
@@ -65,14 +98,14 @@ function overflowingActions(items, shown, moved, room) {
65
98
  const width = new Map(items.map((item) => [item.id, item.width]));
66
99
  let total = shown;
67
100
  if (moved.length > 0) {
68
- total += moved.reduce((sum, id) => sum + (width.get(id) ?? 0) + ACTION_GAP, 0);
101
+ total += moved.reduce((sum, id) => sum + (width.get(id) ?? 0) + 4, 0);
69
102
  total -= 28;
70
103
  }
71
104
  const next = [];
72
105
  for (let i = items.length - 1; i >= 0 && total > room + .5; i--) {
73
106
  const item = items[i];
74
107
  if (next.length === 0) total += 28;
75
- total -= item.width + ACTION_GAP;
108
+ total -= item.width + 4;
76
109
  next.unshift(item.id);
77
110
  }
78
111
  return next;
@@ -5,7 +5,7 @@ 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
7
  import { CellPlacementContext, STACKED_ITEM, Table, hasContent } from "../components/Table.js";
8
- import { ListCountText, ListFooterCountContext } from "./listSearch.js";
8
+ import { ListCountStatus, ListFooterCountContext } from "./listSearch.js";
9
9
  import { ListFilterBar } from "./ListFilterBar.js";
10
10
  import { ListActionsHeader, ListError, ListSkeleton, StretchRowLinkContext } from "./ListTable.js";
11
11
  import { ListToolbar, toolbarCount } from "./ListToolbar.js";
@@ -69,7 +69,8 @@ function ListPage({ bleed = false, toolbar, filterBar, view, loading = false, er
69
69
  nounPlural: count.nounPlural
70
70
  } : fromToolbar;
71
71
  const showsRows = !loading && !failed && !isEmpty && !noneMatch && (!counted || counted.matches > 0);
72
- const footerCount = showsRows ? counted : void 0;
72
+ const countable = !!count || fromToolbar !== void 0;
73
+ const spokenCount = loading || failed ? void 0 : counted;
73
74
  const shareLine = counted !== void 0 && !!toolbar && isValidElement(givenBar) && givenBar.type === ListFilterBar;
74
75
  const bar = shareLine ? cloneElement(givenBar, { end: toolbar }) : givenBar;
75
76
  let body = children;
@@ -102,18 +103,21 @@ function ListPage({ bleed = false, toolbar, filterBar, view, loading = false, er
102
103
  children: [/* @__PURE__ */ jsx(ListRowActionsProvider, {
103
104
  view,
104
105
  children: body
105
- }), (footerCount || showsRows && !!pager) && /* @__PURE__ */ jsx(ListFooter, {
106
- count: footerCount,
107
- pager
106
+ }), (countable || showsRows && !!pager) && /* @__PURE__ */ jsx(ListFooter, {
107
+ count: spokenCount,
108
+ countable,
109
+ pager: showsRows ? pager : null,
110
+ shown: showsRows && (counted !== void 0 || !!pager)
108
111
  })]
109
112
  })]
110
113
  });
111
114
  }
112
- /** The line under a list's last row: its count at the rows' start, its pager at their end. */
113
- function ListFooter({ count, pager }) {
115
+ /** The line under a list's last row: its count at the rows' start, its pager at their end. With no
116
+ * rows to show, only a screen reader finds it, so the count's status stays mounted. */
117
+ function ListFooter({ count, countable, pager, shown }) {
114
118
  return /* @__PURE__ */ jsxs("div", {
115
- className: "flex min-h-10 flex-wrap items-center gap-x-3 px-4 empty:hidden [&>:not([role=status])]:ml-auto [&>nav]:flex-1 [&>nav]:px-0 [&>nav>div:first-child]:justify-end",
116
- children: [count && /* @__PURE__ */ jsx(ListCountText, { ...count }), pager]
119
+ className: shown ? "flex min-h-10 flex-wrap items-center gap-x-3 px-4 empty:hidden [&>:not([role=status])]:ml-auto [&>nav]:flex-1 [&>nav]:px-0 [&>nav>div:first-child]:justify-end" : "sr-only",
120
+ children: [countable && /* @__PURE__ */ jsx(ListCountStatus, { count }), pager]
117
121
  });
118
122
  }
119
123
  var STACKED = { stacked: true };