@parallelworks/ui 0.91.0 → 0.93.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 +74 -1
- package/dist/app-frame/app-frame.css +281 -0
- package/dist/app-frame/index.d.ts +73 -0
- package/dist/app-frame/index.js +369 -0
- package/dist/components/Table.d.ts +7 -0
- package/dist/components/Table.js +10 -3
- package/dist/list/ListPage.js +57 -15
- package/dist/list/index.d.ts +1 -1
- package/dist/list/listView.d.ts +10 -0
- package/dist/list/listView.js +2 -0
- package/dist/styles.css +1 -1
- package/package.json +8 -3
|
@@ -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 };
|
|
@@ -12,10 +12,17 @@ export interface CellPlacement {
|
|
|
12
12
|
className?: string | undefined;
|
|
13
13
|
after?: React.ReactNode;
|
|
14
14
|
stacked?: boolean | undefined;
|
|
15
|
+
/** A stacked value joined to the one before it on its line, after a dot. */
|
|
16
|
+
joined?: boolean | undefined;
|
|
15
17
|
}
|
|
16
18
|
/** A stacked column's value on the line under a row's name: it flows inline with the others and
|
|
17
19
|
* wraps between them, never inside a value, which truncates if it is wider than the line. */
|
|
18
20
|
export declare const STACKED_ITEM = "flex min-w-0 max-w-full items-center whitespace-nowrap [&>*]:min-w-0 [&>*]:truncate";
|
|
21
|
+
/** A joined stacked value's dot, part of the value so a value that renders nothing leaves none:
|
|
22
|
+
* 6px either side of it, where the line keeps 12px between values. */
|
|
23
|
+
export declare const STACKED_JOINED = "-ml-1.5 before:pr-1.5 before:content-['\u00B7'] before:text-(--theme-muted-text-color)";
|
|
24
|
+
/** Whether a cell's content shows anything: an empty string, like a missing value, doesn't. */
|
|
25
|
+
export declare function hasContent(children: React.ReactNode): boolean;
|
|
19
26
|
/** Set by a list row around each of its cells; a `Table.Item` takes what it asks. */
|
|
20
27
|
export declare const CellPlacementContext: React.Context<CellPlacement | null>;
|
|
21
28
|
interface ItemProps extends React.TdHTMLAttributes<HTMLTableCellElement> {
|
package/dist/components/Table.js
CHANGED
|
@@ -26,13 +26,20 @@ var cellX = "px-3";
|
|
|
26
26
|
/** A stacked column's value on the line under a row's name: it flows inline with the others and
|
|
27
27
|
* wraps between them, never inside a value, which truncates if it is wider than the line. */
|
|
28
28
|
var STACKED_ITEM = "flex min-w-0 max-w-full items-center whitespace-nowrap [&>*]:min-w-0 [&>*]:truncate";
|
|
29
|
+
/** A joined stacked value's dot, part of the value so a value that renders nothing leaves none:
|
|
30
|
+
* 6px either side of it, where the line keeps 12px between values. */
|
|
31
|
+
var STACKED_JOINED = "-ml-1.5 before:pr-1.5 before:content-['·'] before:text-(--theme-muted-text-color)";
|
|
32
|
+
/** Whether a cell's content shows anything: an empty string, like a missing value, doesn't. */
|
|
33
|
+
function hasContent(children) {
|
|
34
|
+
return React.Children.toArray(children).some((child) => child !== "");
|
|
35
|
+
}
|
|
29
36
|
/** Set by a list row around each of its cells; a `Table.Item` takes what it asks. */
|
|
30
37
|
var CellPlacementContext = createContext(null);
|
|
31
38
|
function Item({ wrap = false, children, className, ...tdProps }) {
|
|
32
39
|
const density = useContext(DensityContext);
|
|
33
40
|
const placed = useContext(CellPlacementContext);
|
|
34
|
-
if (placed?.stacked) return
|
|
35
|
-
className: STACKED_ITEM,
|
|
41
|
+
if (placed?.stacked) return hasContent(children) ? /* @__PURE__ */ jsx("div", {
|
|
42
|
+
className: cx(STACKED_ITEM, placed.joined && "-ml-1.5 before:pr-1.5 before:content-['·'] before:text-(--theme-muted-text-color)"),
|
|
36
43
|
"data-stacked": true,
|
|
37
44
|
children
|
|
38
45
|
}) : null;
|
|
@@ -121,4 +128,4 @@ function Table({ density = "comfortable", columns, caps = true, flush = false, c
|
|
|
121
128
|
Table.Header = Header;
|
|
122
129
|
Table.Item = Item;
|
|
123
130
|
//#endregion
|
|
124
|
-
export { CellPlacementContext, STACKED_ITEM, Table };
|
|
131
|
+
export { CellPlacementContext, STACKED_ITEM, STACKED_JOINED, Table, hasContent };
|
package/dist/list/ListPage.js
CHANGED
|
@@ -4,7 +4,7 @@ import { Tooltip } from "../components/Tooltip.js";
|
|
|
4
4
|
import { useNavigation } from "../components/Provider.js";
|
|
5
5
|
import { AppLink, isPlainClick } from "../components/AppLink.js";
|
|
6
6
|
import { isMenuDivider, isMenuGroup, isMenuSubmenu, useRunAction, warnIfInert } from "../components/MenuEntries.js";
|
|
7
|
-
import { CellPlacementContext, STACKED_ITEM, Table } from "../components/Table.js";
|
|
7
|
+
import { CellPlacementContext, STACKED_ITEM, Table, hasContent } from "../components/Table.js";
|
|
8
8
|
import { ListCountText, ListFooterCountContext } from "./listSearch.js";
|
|
9
9
|
import { ListFilterBar } from "./ListFilterBar.js";
|
|
10
10
|
import { ListActionsHeader, ListError, ListSkeleton, StretchRowLinkContext } from "./ListTable.js";
|
|
@@ -19,20 +19,24 @@ var RowActionsContext = createContext({
|
|
|
19
19
|
always: false,
|
|
20
20
|
columnClasses: [],
|
|
21
21
|
columnStacks: [],
|
|
22
|
+
columnStackLines: [],
|
|
22
23
|
reservesLane: false
|
|
23
24
|
});
|
|
25
|
+
var NO_STACK_LINES = [];
|
|
24
26
|
function useRowActions(view) {
|
|
25
27
|
return useMemo(() => ({
|
|
26
28
|
show: view?.showActions ?? true,
|
|
27
29
|
always: view?.alwaysShowActions ?? false,
|
|
28
30
|
columnClasses: view?.columnClasses ?? [],
|
|
29
31
|
columnStacks: view?.columnStacks ?? [],
|
|
32
|
+
columnStackLines: view?.columnStackLines ?? NO_STACK_LINES,
|
|
30
33
|
reservesLane: view?.reservesActionLane ?? view?.lastColumnLocked ?? false
|
|
31
34
|
}), [
|
|
32
35
|
view?.showActions,
|
|
33
36
|
view?.alwaysShowActions,
|
|
34
37
|
view?.columnClasses,
|
|
35
38
|
view?.columnStacks,
|
|
39
|
+
view?.columnStackLines,
|
|
36
40
|
view?.lastColumnLocked,
|
|
37
41
|
view?.reservesActionLane
|
|
38
42
|
]);
|
|
@@ -113,32 +117,67 @@ function ListFooter({ count, pager }) {
|
|
|
113
117
|
});
|
|
114
118
|
}
|
|
115
119
|
var STACKED = { stacked: true };
|
|
116
|
-
var
|
|
120
|
+
var STACKED_JOINED_PLACEMENT = {
|
|
121
|
+
stacked: true,
|
|
122
|
+
joined: true
|
|
123
|
+
};
|
|
124
|
+
var STACKED_LINE = "mt-1 flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1 text-xs text-(--theme-muted-text-color) empty:hidden @[36rem]:hidden";
|
|
125
|
+
var STACKED_LINES = "mt-1 flex min-w-0 flex-col gap-y-1 text-xs text-(--theme-muted-text-color) empty:hidden @[36rem]:hidden";
|
|
126
|
+
var STACKED_LINES_LINE = "flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1";
|
|
117
127
|
var isCellElement = (child) => child.type === Table.Item || child.type === "td";
|
|
118
128
|
function ListRow({ href, onActivate, items, goTo, openMenu, pinnedActions, selected, selectionActive, onRowSelect, leading, className, children }) {
|
|
119
129
|
const { active: menuActive, openMenu: openRowMenu } = useRowMenuActive(openMenu);
|
|
120
|
-
const { show: showActions, always: alwaysShowActions, columnClasses, columnStacks, reservesLane } = useContext(RowActionsContext);
|
|
130
|
+
const { show: showActions, always: alwaysShowActions, columnClasses, columnStacks, columnStackLines, reservesLane } = useContext(RowActionsContext);
|
|
121
131
|
const cells = Children.toArray(children);
|
|
122
132
|
const stacked = cells.flatMap((child, i) => {
|
|
123
133
|
if (i === 0 || !columnStacks[i] || !isValidElement(child)) return [];
|
|
134
|
+
const line = columnStackLines[i];
|
|
124
135
|
if (isCellElement(child)) {
|
|
125
136
|
const content = child.props.children;
|
|
126
|
-
return
|
|
137
|
+
return hasContent(content) ? [{
|
|
138
|
+
child,
|
|
139
|
+
line
|
|
140
|
+
}] : [];
|
|
127
141
|
}
|
|
128
|
-
return [
|
|
142
|
+
return [{
|
|
143
|
+
child,
|
|
144
|
+
line
|
|
145
|
+
}];
|
|
129
146
|
});
|
|
130
|
-
const
|
|
147
|
+
const item = (child, j, joined = false) => isValidElement(child) && child.type === "td" ? /* @__PURE__ */ jsx("div", {
|
|
148
|
+
className: cx(STACKED_ITEM, joined && "-ml-1.5 before:pr-1.5 before:content-['·'] before:text-(--theme-muted-text-color)"),
|
|
149
|
+
"data-stacked": true,
|
|
150
|
+
children: child.props.children
|
|
151
|
+
}, j) : /* @__PURE__ */ jsx(CellPlacementContext, {
|
|
152
|
+
value: joined ? STACKED_JOINED_PLACEMENT : STACKED,
|
|
153
|
+
children: child
|
|
154
|
+
}, j);
|
|
155
|
+
let under;
|
|
156
|
+
if (stacked.length > 0 && stacked.every(({ line }) => line === void 0)) under = /* @__PURE__ */ jsx("div", {
|
|
131
157
|
className: STACKED_LINE,
|
|
132
158
|
"data-stacked-line": true,
|
|
133
|
-
children: stacked.map((child, j) =>
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}, j)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
159
|
+
children: stacked.map(({ child }, j) => item(child, j))
|
|
160
|
+
});
|
|
161
|
+
else if (stacked.length > 0) {
|
|
162
|
+
const lines = [];
|
|
163
|
+
stacked.forEach(({ child, line }, j) => {
|
|
164
|
+
const current = lines.at(-1);
|
|
165
|
+
if (!current || line === "own") {
|
|
166
|
+
lines.push([item(child, j)]);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
current.push(item(child, j, line === "join"));
|
|
170
|
+
});
|
|
171
|
+
under = /* @__PURE__ */ jsx("div", {
|
|
172
|
+
className: STACKED_LINES,
|
|
173
|
+
"data-stacked-lines": true,
|
|
174
|
+
children: lines.map((values, k) => /* @__PURE__ */ jsx("div", {
|
|
175
|
+
className: STACKED_LINES_LINE,
|
|
176
|
+
"data-stacked-line": true,
|
|
177
|
+
children: values
|
|
178
|
+
}, k))
|
|
179
|
+
});
|
|
180
|
+
}
|
|
142
181
|
const styledCells = cells.map((child, i) => {
|
|
143
182
|
const cls = cx(columnClasses[i], i > 0 && stacked.length > 0 && !columnStacks[i] && STAYS_BESIDE_STACK);
|
|
144
183
|
const after = i === 0 ? under : void 0;
|
|
@@ -356,6 +395,9 @@ function shareWidths(columns, stacks) {
|
|
|
356
395
|
const total = shares.reduce((sum, share) => sum + (share ?? 0), 0);
|
|
357
396
|
const scale = (tier) => {
|
|
358
397
|
const kept = columns.reduce((sum, col, i) => sum + (shows(col, i, tier) ? shares[i] ?? 0 : 0), 0);
|
|
398
|
+
const drops = columns.some((col, i) => !shows(col, i, tier));
|
|
399
|
+
const unshared = columns.some((col, i) => shows(col, i, tier) && shares[i] === void 0);
|
|
400
|
+
if (drops && !unshared && kept > 0) return 100 / kept;
|
|
359
401
|
const left = 100 - (total - kept);
|
|
360
402
|
return left > 0 ? 100 / left : 1;
|
|
361
403
|
};
|
package/dist/list/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { actionsColumnWidth, ListActionsHeader, ListCellText, ListError, ListSel
|
|
|
6
6
|
export { ListToolbar, type ListToolbarProps } from './ListToolbar.js';
|
|
7
7
|
export { ListDisplayMenu, ListFilterMenu } from './ListViewControls.js';
|
|
8
8
|
export { ListCount, type ListSearch, ListSearchControl, useListSearch, } from './listSearch.js';
|
|
9
|
-
export { type ActionDef, type ColumnDef, DEFAULT_ORDER, FAVORITE_ORDER, type FilterFacet, type ListView, type ListViewChrome, type ListViewConfig, type OrderByOption, useListView, } from './listView.js';
|
|
9
|
+
export { type ActionDef, type ColumnDef, DEFAULT_ORDER, FAVORITE_ORDER, type FilterFacet, type ListView, type ListViewChrome, type ListViewConfig, type OrderByOption, type StackLine, useListView, } from './listView.js';
|
|
10
10
|
export { assembleRowMenu, type CopyFields, type CopySubmenu, MoreButton, type RowAction, rowContextMenuProps, useCopySubmenu, useRowMenuActive, } from './rowMenu.js';
|
|
11
11
|
export { HoverCardRow, UserHoverCard, type UserHoverCardProps, } from './UserHoverCard.js';
|
|
12
12
|
export { type FilterStore, type ListFilters, type ListFiltersConfig, useListFilters, } from './useListFilters.js';
|
package/dist/list/listView.d.ts
CHANGED
|
@@ -26,6 +26,11 @@ export interface ColumnDef {
|
|
|
26
26
|
* column there: for something short that compares best lined up, like a
|
|
27
27
|
* total. The first column never stacks; it is what the others stack under. */
|
|
28
28
|
stack?: boolean;
|
|
29
|
+
/** Where a stacked column's value sits on the lines under the name. By default the stacked
|
|
30
|
+
* values share a line, flowing and wrapping between them. `own` starts a line of its own, for
|
|
31
|
+
* a value read first, such as a status; `join` stays on the line before it after a "·", for
|
|
32
|
+
* one that belongs with that value, such as a job's nodes after its partition. */
|
|
33
|
+
stackLine?: StackLine | undefined;
|
|
29
34
|
/** The `value` of the {@link OrderByOption} this column sorts by. Its header
|
|
30
35
|
* then sorts the list: it shows, and sets, the same order as Display
|
|
31
36
|
* options' Ordering. */
|
|
@@ -43,6 +48,9 @@ export interface ColumnDef {
|
|
|
43
48
|
}
|
|
44
49
|
/** Column responsive importance; `low` drops before `medium`. See {@link ColumnDef.priority}. */
|
|
45
50
|
type ColumnPriority = 'low' | 'medium';
|
|
51
|
+
/** How a stacked column's value lines up with the others under the name; see
|
|
52
|
+
* {@link ColumnDef.stackLine}. */
|
|
53
|
+
export type StackLine = 'own' | 'join';
|
|
46
54
|
/** Whether a column is always shown and kept out of the column picker. */
|
|
47
55
|
export declare function isLocked(col: ColumnDef): boolean;
|
|
48
56
|
interface GroupByOption {
|
|
@@ -130,6 +138,8 @@ export interface ListViewChrome {
|
|
|
130
138
|
/** Per visible column, whether it stacks under the first on a narrow
|
|
131
139
|
* container ({@link ColumnDef.stack}); `ListRow` reads entry `i`. */
|
|
132
140
|
columnStacks: boolean[];
|
|
141
|
+
/** Per visible column, the line its stacked value takes ({@link ColumnDef.stackLine}). */
|
|
142
|
+
columnStackLines?: (StackLine | undefined)[] | undefined;
|
|
133
143
|
/** Whether the last column is a locked one, such as a delete button's: the
|
|
134
144
|
* row's `⋯` then keeps a lane of its own after it rather than floating over
|
|
135
145
|
* it on hover. */
|
package/dist/list/listView.js
CHANGED
|
@@ -94,6 +94,7 @@ function useListView({ storageKey, sharedFiltersKey, columns, groupBys = [], def
|
|
|
94
94
|
const visibleColumns = useMemo(() => columns.filter((c) => !c.inline && (isLocked(c) || !hiddenKeys.includes(c.key))), [columns, hiddenKeys]);
|
|
95
95
|
const columnClasses = useMemo(() => visibleColumns.map(columnClass), [visibleColumns]);
|
|
96
96
|
const columnStacks = useMemo(() => visibleColumns.map(stacks), [visibleColumns]);
|
|
97
|
+
const columnStackLines = useMemo(() => visibleColumns.map((c) => c.stackLine), [visibleColumns]);
|
|
97
98
|
const last = visibleColumns.at(-1);
|
|
98
99
|
const lastColumnLocked = visibleColumns.length > 1 && last !== void 0 && !last.alwaysVisible && isLocked(last);
|
|
99
100
|
const reservesActionLane = lastColumnLocked || visibleColumns.length > 1 && last?.align === "end";
|
|
@@ -191,6 +192,7 @@ function useListView({ storageKey, sharedFiltersKey, columns, groupBys = [], def
|
|
|
191
192
|
visibleColumns,
|
|
192
193
|
columnClasses,
|
|
193
194
|
columnStacks,
|
|
195
|
+
columnStackLines,
|
|
194
196
|
lastColumnLocked,
|
|
195
197
|
reservesActionLane,
|
|
196
198
|
isVisible,
|