@rebasepro/ui 0.13.0 → 0.13.1-canary.g18cfeb7
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/dist/components/VirtualTable/VirtualTableProps.d.ts +17 -2
- package/dist/icons/index.d.ts +1 -1
- package/dist/index.es.js +19 -13
- package/dist/index.es.js.map +1 -1
- package/dist/views/ListView.d.ts +7 -1
- package/package.json +1 -1
- package/src/components/Menu.tsx +9 -1
- package/src/components/Menubar.tsx +5 -1
- package/src/components/Popover.tsx +4 -1
- package/src/components/Skeleton.tsx +9 -2
- package/src/components/VirtualTable/VirtualTableProps.tsx +17 -2
- package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +7 -0
- package/src/components/VirtualTable/fields/VirtualTableInput.tsx +7 -0
- package/src/icons/index.ts +2 -0
- package/src/views/CollectionView/CollectionView.tsx +7 -1
- package/src/views/ListView.tsx +77 -62
package/dist/views/ListView.d.ts
CHANGED
|
@@ -25,6 +25,12 @@ export type ListViewProps<T> = {
|
|
|
25
25
|
emptyComponent?: React.ReactNode;
|
|
26
26
|
size?: CollectionSize;
|
|
27
27
|
selectedEntityId?: string | number;
|
|
28
|
+
/**
|
|
29
|
+
* Rendered above the rows, inside the list's own frame, and only when there
|
|
30
|
+
* are rows to head. A column header over an empty state or a spinner labels
|
|
31
|
+
* nothing.
|
|
32
|
+
*/
|
|
33
|
+
header?: React.ReactNode;
|
|
28
34
|
renderRow: (params: {
|
|
29
35
|
item: T;
|
|
30
36
|
index: number;
|
|
@@ -37,4 +43,4 @@ export type ListViewProps<T> = {
|
|
|
37
43
|
onSelectionChange: (selected: boolean) => void;
|
|
38
44
|
}) => React.ReactNode;
|
|
39
45
|
};
|
|
40
|
-
export declare function ListView<T>({ data, dataLoading, noMoreToLoad, dataLoadingError, itemCount, setItemCount, pageSize, paginationEnabled, onItemClick, selectedIds, highlightedIds, selectionEnabled, onSelectionChange, emptyComponent, size, selectedEntityId, renderRow }: ListViewProps<T>): React.JSX.Element;
|
|
46
|
+
export declare function ListView<T>({ data, dataLoading, noMoreToLoad, dataLoadingError, itemCount, setItemCount, pageSize, paginationEnabled, onItemClick, selectedIds, highlightedIds, selectionEnabled, onSelectionChange, emptyComponent, size, selectedEntityId, header, renderRow }: ListViewProps<T>): React.JSX.Element;
|
package/package.json
CHANGED
package/src/components/Menu.tsx
CHANGED
|
@@ -57,7 +57,15 @@ const Menu = React.forwardRef<
|
|
|
57
57
|
side={side}
|
|
58
58
|
sideOffset={sideOffset}
|
|
59
59
|
align={align}
|
|
60
|
-
|
|
60
|
+
// `z-50` is the floating layer — the tier `Dialog`, `Sheet`,
|
|
61
|
+
// `Select` and `Tooltip` all sit on. Radix copies this value
|
|
62
|
+
// onto the popper wrapper it portals next to them, so at any
|
|
63
|
+
// lower value the menu opens *behind* the dialog or sheet it
|
|
64
|
+
// was opened from: visible in the DOM, painted under the
|
|
65
|
+
// overlay, and unclickable. Peers on this tier stack by mount
|
|
66
|
+
// order, which is what puts a menu above the dialog holding
|
|
67
|
+
// its trigger, and a dialog that menu opens above them both.
|
|
68
|
+
className={cls(paperMixin, focusedDisabled, "py-2 z-50", className)}>
|
|
61
69
|
{children}
|
|
62
70
|
</DropdownMenu.Content>
|
|
63
71
|
</DropdownMenu.Portal>
|
|
@@ -109,7 +109,11 @@ export function MenubarContent({
|
|
|
109
109
|
return (
|
|
110
110
|
<MenubarPrimitive.Content
|
|
111
111
|
onSelect={onSelect}
|
|
112
|
-
|
|
112
|
+
// `z-50`, the floating layer — see the note in `Menu`. This carried
|
|
113
|
+
// no z-index at all, so the wrapper Radix portals out to the body
|
|
114
|
+
// got `auto` and the menu opened underneath any dialog or panel it
|
|
115
|
+
// was hosted in.
|
|
116
|
+
className={cls("z-50 min-w-[220px] bg-white dark:bg-surface-900 rounded-md p-[6px] shadow-[0px_10px_38px_-10px_rgba(22,_23,_24,_0.35),_0px_10px_20px_-15px_rgba(22,_23,_24,_0.2)] [animation-duration:_400ms] [animation-timing-function:_cubic-bezier(0.16,_1,_0.3,_1)] will-change-[transform,opacity]", className)}
|
|
113
117
|
align={align ?? "start"}
|
|
114
118
|
sideOffset={sideOffset ?? 5}
|
|
115
119
|
alignOffset={alignOffset ?? -3}
|
|
@@ -73,8 +73,11 @@ export function Popover({
|
|
|
73
73
|
|
|
74
74
|
<PopoverPrimitive.Portal container={finalContainer}>
|
|
75
75
|
<PopoverPrimitive.Content
|
|
76
|
+
// `z-50`, the floating layer — see the note in `Menu`. Below it
|
|
77
|
+
// a popover opened from inside a dialog or a side panel is
|
|
78
|
+
// painted under the thing that owns its trigger.
|
|
76
79
|
className={cls(paperMixin,
|
|
77
|
-
"PopoverContent z-
|
|
80
|
+
"PopoverContent z-50", className)}
|
|
78
81
|
side={side}
|
|
79
82
|
sideOffset={sideOffset}
|
|
80
83
|
align={align}
|
|
@@ -29,14 +29,21 @@ export function Skeleton({
|
|
|
29
29
|
|
|
30
30
|
return <span
|
|
31
31
|
style={{
|
|
32
|
-
width: width ? `${width}px
|
|
33
|
-
height: height ? `${height}px`
|
|
32
|
+
width: width === undefined ? undefined : `${width}px`,
|
|
33
|
+
height: height === undefined ? undefined : `${height}px`
|
|
34
34
|
}}
|
|
35
35
|
className={
|
|
36
36
|
cls(
|
|
37
37
|
"block relative overflow-hidden",
|
|
38
38
|
"bg-surface-accent-50 dark:bg-surface-accent-800 rounded-md",
|
|
39
39
|
"max-w-full max-h-full",
|
|
40
|
+
// The defaults are classes, not inline styles, so that a caller can
|
|
41
|
+
// override them: an inline style beats every utility, which is why
|
|
42
|
+
// `<Skeleton className="w-full h-full"/>` inside a 40px thumbnail
|
|
43
|
+
// box used to render a 12px bar. The `width`/`height` props still
|
|
44
|
+
// win over any class, since those do land inline.
|
|
45
|
+
width === undefined ? "w-full" : undefined,
|
|
46
|
+
height === undefined ? "h-3" : undefined,
|
|
40
47
|
className)
|
|
41
48
|
}>
|
|
42
49
|
<span
|
|
@@ -275,6 +275,16 @@ export type OnVirtualTableColumnResizeParams = {
|
|
|
275
275
|
};
|
|
276
276
|
|
|
277
277
|
/**
|
|
278
|
+
* The filter operators this table can express.
|
|
279
|
+
*
|
|
280
|
+
* A deliberate copy of `WhereFilterOp` in `@rebasepro/types`, not an import:
|
|
281
|
+
* `@rebasepro/ui` has no dependency on the core types and is meant to stay
|
|
282
|
+
* usable on its own. The cost is that two published packages export the same
|
|
283
|
+
* name, so the two must be kept identical by hand —
|
|
284
|
+
* `packages/types/test/filter-operators-duplication.test.ts` fails if they
|
|
285
|
+
* diverge, because a table that cannot express an operator the query layer
|
|
286
|
+
* supports is a silent gap: the filter simply is not offered.
|
|
287
|
+
*
|
|
278
288
|
* @see Table
|
|
279
289
|
* @group Components
|
|
280
290
|
*/
|
|
@@ -312,8 +322,13 @@ export type VirtualTableSort = "asc" | "desc" | undefined;
|
|
|
312
322
|
export type VirtualTableFilterValues<Key extends string> = FilterValues<Key>;
|
|
313
323
|
|
|
314
324
|
/**
|
|
315
|
-
* Filter conditions in a `Query.where()` clause
|
|
316
|
-
*
|
|
325
|
+
* Filter conditions in a `Query.where()` clause, named by {@link WhereFilterOp}.
|
|
326
|
+
*
|
|
327
|
+
* The list is not repeated here: this comment used to name eight operators when
|
|
328
|
+
* the type had sixteen, having been written before the SQL pattern and null
|
|
329
|
+
* checks (`like`, `ilike`, `not-like`, `not-ilike`, `is-null`, `is-not-null`)
|
|
330
|
+
* were added. A prose copy of a union drifts from it silently.
|
|
331
|
+
*
|
|
317
332
|
* @see Table
|
|
318
333
|
* @group Models
|
|
319
334
|
*/
|
|
@@ -22,6 +22,13 @@ export function VirtualTableDateField(props: {
|
|
|
22
22
|
<DateTimeField
|
|
23
23
|
value={internalValue ?? undefined}
|
|
24
24
|
onChange={(dateValue) => updateValue(dateValue ?? null)}
|
|
25
|
+
// Same omission as `VirtualTableInput`: destructured and forwarded
|
|
26
|
+
// nowhere, so a disabled date cell was still editable. `error` went
|
|
27
|
+
// the same way — it drives `DateTimeField`'s invalid styling, and a
|
|
28
|
+
// prop a caller passes should reach something rather than sit in a
|
|
29
|
+
// destructure.
|
|
30
|
+
disabled={disabled}
|
|
31
|
+
error={Boolean(error)}
|
|
25
32
|
invisible={true}
|
|
26
33
|
// Without this the field falls back to DateTimeField's own default of
|
|
27
34
|
// `size="large"` (min-h-64px), which overflows a table cell and makes
|
|
@@ -50,6 +50,13 @@ export function VirtualTableInput(props: {
|
|
|
50
50
|
<TextareaAutosize
|
|
51
51
|
className={focusedDisabled}
|
|
52
52
|
ref={ref}
|
|
53
|
+
// Destructured above and passed to nothing, so a property carrying
|
|
54
|
+
// `admin: { disabled: true }` stayed typeable in the table and the
|
|
55
|
+
// debounced write fired on blur. `readOnly` as well as `disabled`:
|
|
56
|
+
// a disabled textarea is skipped by keyboard navigation, and a cell
|
|
57
|
+
// you cannot tab through is worse than one you cannot edit.
|
|
58
|
+
disabled={disabled}
|
|
59
|
+
readOnly={disabled}
|
|
53
60
|
style={{
|
|
54
61
|
padding: 0,
|
|
55
62
|
margin: 0,
|
package/src/icons/index.ts
CHANGED
|
@@ -323,7 +323,13 @@ export function CollectionView<T extends Record<string, unknown> = Record<string
|
|
|
323
323
|
<CollectionKanbanView<T>
|
|
324
324
|
{...sharedProps}
|
|
325
325
|
kanbanProperty={resolvedKanbanProperty}
|
|
326
|
-
|
|
326
|
+
// `canCreate` is documented as "whether creation is
|
|
327
|
+
// allowed (shows the + button)" and was destructured
|
|
328
|
+
// and read nowhere, so the button showed regardless.
|
|
329
|
+
// The kanban view draws its add affordance exactly when
|
|
330
|
+
// it has a callback, so withholding the callback is
|
|
331
|
+
// what the prop means.
|
|
332
|
+
onRowCreate={canCreate ? onRowCreate : undefined}
|
|
327
333
|
/>
|
|
328
334
|
) : (
|
|
329
335
|
<div className="flex items-center justify-center p-8">
|
package/src/views/ListView.tsx
CHANGED
|
@@ -36,6 +36,13 @@ export type ListViewProps<T> = {
|
|
|
36
36
|
size?: CollectionSize;
|
|
37
37
|
selectedEntityId?: string | number;
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Rendered above the rows, inside the list's own frame, and only when there
|
|
41
|
+
* are rows to head. A column header over an empty state or a spinner labels
|
|
42
|
+
* nothing.
|
|
43
|
+
*/
|
|
44
|
+
header?: React.ReactNode;
|
|
45
|
+
|
|
39
46
|
renderRow: (params: {
|
|
40
47
|
item: T;
|
|
41
48
|
index: number;
|
|
@@ -97,9 +104,14 @@ export function ListView<T>({
|
|
|
97
104
|
|
|
98
105
|
size = "m",
|
|
99
106
|
selectedEntityId,
|
|
107
|
+
header,
|
|
100
108
|
renderRow
|
|
101
109
|
}: ListViewProps<T>) {
|
|
102
110
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
111
|
+
// The rows' own box, which a header shifts down from the container's top.
|
|
112
|
+
// Virtualization measures against it rather than against the container, so
|
|
113
|
+
// the window of rendered rows stays the one the viewport is actually over.
|
|
114
|
+
const rowsRef = useRef<HTMLDivElement>(null);
|
|
103
115
|
const isLoadingMore = useRef(false);
|
|
104
116
|
|
|
105
117
|
// Reset loading gate when loading stops
|
|
@@ -128,7 +140,7 @@ export function ListView<T>({
|
|
|
128
140
|
const update = () => {
|
|
129
141
|
rafId = null;
|
|
130
142
|
const scrollRect = scrollEl.getBoundingClientRect();
|
|
131
|
-
const listRect = el.getBoundingClientRect();
|
|
143
|
+
const listRect = (rowsRef.current ?? el).getBoundingClientRect();
|
|
132
144
|
|
|
133
145
|
const listTopRelative = listRect.top - scrollRect.top;
|
|
134
146
|
setEffectiveScrollTop(Math.max(0, -listTopRelative));
|
|
@@ -213,68 +225,71 @@ export function ListView<T>({
|
|
|
213
225
|
)}
|
|
214
226
|
</div>
|
|
215
227
|
) : (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
{dataLoading && (
|
|
260
|
-
<div
|
|
261
|
-
className="flex items-center justify-center py-3"
|
|
262
|
-
style={{ position: "absolute", top: totalHeight, left: 0, right: 0 }}
|
|
263
|
-
>
|
|
264
|
-
<CircularProgress size="small" />
|
|
265
|
-
</div>
|
|
266
|
-
)}
|
|
267
|
-
{!dataLoading && noMoreToLoad && data.length > 0 && (
|
|
268
|
-
<div
|
|
269
|
-
className="flex items-center justify-center py-2 dark:bg-surface-900"
|
|
270
|
-
style={{ position: "absolute", top: totalHeight, left: 0, right: 0 }}
|
|
271
|
-
>
|
|
272
|
-
<Typography variant="caption" color="secondary">
|
|
273
|
-
All {data.length} entries loaded
|
|
274
|
-
</Typography>
|
|
228
|
+
<>
|
|
229
|
+
{header}
|
|
230
|
+
<div ref={rowsRef} style={{ height: totalHeight + footerHeight, position: "relative" }}>
|
|
231
|
+
<div style={{ position: "absolute", top: offsetY, left: 0, right: 0 }}>
|
|
232
|
+
{visibleData.map((item, i) => {
|
|
233
|
+
const actualIndex = startIndex + i;
|
|
234
|
+
const isLast = actualIndex === data.length - 1;
|
|
235
|
+
const id = getItemId(item);
|
|
236
|
+
const selected = selectedIds?.has(id) ?? false;
|
|
237
|
+
const highlighted = highlightedIds?.has(id) ?? false;
|
|
238
|
+
|
|
239
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
240
|
+
if ((e.metaKey || e.ctrlKey) && selectionEnabled) {
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
onSelectionChange?.(item, !selected);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
onItemClick?.(item);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const handleSelectionChange = (val: boolean) => {
|
|
249
|
+
onSelectionChange?.(item, val);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<React.Fragment key={id || actualIndex}>
|
|
254
|
+
{renderRow({
|
|
255
|
+
item,
|
|
256
|
+
index: actualIndex,
|
|
257
|
+
style: { height: estimatedRowHeight, overflow: "hidden" },
|
|
258
|
+
className: cls(
|
|
259
|
+
!isLast && "border-b",
|
|
260
|
+
!isLast && borderMixinClass
|
|
261
|
+
),
|
|
262
|
+
selected,
|
|
263
|
+
highlighted,
|
|
264
|
+
isLast,
|
|
265
|
+
onClick: handleClick,
|
|
266
|
+
onSelectionChange: handleSelectionChange
|
|
267
|
+
})}
|
|
268
|
+
</React.Fragment>
|
|
269
|
+
);
|
|
270
|
+
})}
|
|
275
271
|
</div>
|
|
276
|
-
|
|
277
|
-
|
|
272
|
+
|
|
273
|
+
{dataLoading && (
|
|
274
|
+
<div
|
|
275
|
+
className="flex items-center justify-center py-3"
|
|
276
|
+
style={{ position: "absolute", top: totalHeight, left: 0, right: 0 }}
|
|
277
|
+
>
|
|
278
|
+
<CircularProgress size="small" />
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
{!dataLoading && noMoreToLoad && data.length > 0 && (
|
|
282
|
+
<div
|
|
283
|
+
className="flex items-center justify-center py-2 dark:bg-surface-900"
|
|
284
|
+
style={{ position: "absolute", top: totalHeight, left: 0, right: 0 }}
|
|
285
|
+
>
|
|
286
|
+
<Typography variant="caption" color="secondary">
|
|
287
|
+
All {data.length} entries loaded
|
|
288
|
+
</Typography>
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
</div>
|
|
292
|
+
</>
|
|
278
293
|
)}
|
|
279
294
|
</div>
|
|
280
295
|
);
|