@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.
@@ -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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/ui",
3
3
  "type": "module",
4
- "version": "0.13.0",
4
+ "version": "0.13.1-canary.g18cfeb7",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -57,7 +57,15 @@ const Menu = React.forwardRef<
57
57
  side={side}
58
58
  sideOffset={sideOffset}
59
59
  align={align}
60
- className={cls(paperMixin, focusedDisabled, "py-2 z-30", className)}>
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
- className={cls("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)}
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-40", className)}
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` : "100%",
33
- height: height ? `${height}px` : "12px"
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 are specified using the
316
- * strings `<`, `<=`, `==`, `>=`, `>`, `array-contains`, `in`, and `array-contains-any`.
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,
@@ -100,7 +100,9 @@ export {
100
100
  MoonIcon,
101
101
  MoreVerticalIcon,
102
102
  Music2Icon,
103
+ PanelLeftCloseIcon,
103
104
  PanelLeftIcon,
105
+ PanelLeftOpenIcon,
104
106
  PauseIcon,
105
107
  PenLineIcon,
106
108
  PencilIcon,
@@ -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
- onRowCreate={onRowCreate}
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">
@@ -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
- <div style={{ height: totalHeight + footerHeight, position: "relative" }}>
217
- <div style={{ position: "absolute", top: offsetY, left: 0, right: 0 }}>
218
- {visibleData.map((item, i) => {
219
- const actualIndex = startIndex + i;
220
- const isLast = actualIndex === data.length - 1;
221
- const id = getItemId(item);
222
- const selected = selectedIds?.has(id) ?? false;
223
- const highlighted = highlightedIds?.has(id) ?? false;
224
-
225
- const handleClick = (e: React.MouseEvent) => {
226
- if ((e.metaKey || e.ctrlKey) && selectionEnabled) {
227
- e.preventDefault();
228
- onSelectionChange?.(item, !selected);
229
- return;
230
- }
231
- onItemClick?.(item);
232
- };
233
-
234
- const handleSelectionChange = (val: boolean) => {
235
- onSelectionChange?.(item, val);
236
- };
237
-
238
- return (
239
- <React.Fragment key={id || actualIndex}>
240
- {renderRow({
241
- item,
242
- index: actualIndex,
243
- style: { height: estimatedRowHeight, overflow: "hidden" },
244
- className: cls(
245
- !isLast && "border-b",
246
- !isLast && borderMixinClass
247
- ),
248
- selected,
249
- highlighted,
250
- isLast,
251
- onClick: handleClick,
252
- onSelectionChange: handleSelectionChange
253
- })}
254
- </React.Fragment>
255
- );
256
- })}
257
- </div>
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
- </div>
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
  );