@rebasepro/ui 0.6.0 → 0.7.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.
Files changed (72) hide show
  1. package/dist/icons/index.d.ts +1 -1
  2. package/dist/index.css +13 -13
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.es.js +2119 -74
  5. package/dist/index.es.js.map +1 -1
  6. package/dist/src/index.css +13 -13
  7. package/dist/styles.d.ts +9 -9
  8. package/dist/views/CardView.d.ts +32 -0
  9. package/dist/views/CollectionView/CollectionCardView.d.ts +30 -0
  10. package/dist/views/CollectionView/CollectionKanbanView.d.ts +31 -0
  11. package/dist/views/CollectionView/CollectionListView.d.ts +30 -0
  12. package/dist/views/CollectionView/CollectionTableView.d.ts +37 -0
  13. package/dist/views/CollectionView/CollectionView.d.ts +121 -0
  14. package/dist/views/CollectionView/CollectionViewToolbar.d.ts +31 -0
  15. package/dist/views/CollectionView/CollectionViewTypes.d.ts +106 -0
  16. package/dist/views/CollectionView/DefaultCellRenderer.d.ts +9 -0
  17. package/dist/views/CollectionView/index.d.ts +24 -0
  18. package/dist/views/CollectionView/utils.d.ts +5 -0
  19. package/dist/views/Kanban/Board.d.ts +3 -0
  20. package/dist/views/Kanban/BoardColumn.d.ts +20 -0
  21. package/dist/views/Kanban/BoardColumnTitle.d.ts +8 -0
  22. package/dist/views/Kanban/BoardSortableList.d.ts +14 -0
  23. package/dist/views/Kanban/board_types.d.ts +61 -0
  24. package/dist/views/Kanban/index.d.ts +5 -0
  25. package/dist/views/KanbanView.d.ts +24 -0
  26. package/dist/views/ListView.d.ts +40 -0
  27. package/dist/views/TableView.d.ts +6 -0
  28. package/dist/views/index.d.ts +5 -0
  29. package/package.json +1 -1
  30. package/src/components/Alert.tsx +1 -1
  31. package/src/components/BooleanSwitchWithLabel.tsx +1 -1
  32. package/src/components/Button.tsx +5 -5
  33. package/src/components/Chip.tsx +39 -37
  34. package/src/components/DateTimeField.tsx +2 -2
  35. package/src/components/Dialog.tsx +1 -1
  36. package/src/components/ExpandablePanel.tsx +1 -1
  37. package/src/components/FileUpload.tsx +1 -1
  38. package/src/components/FilterChip.tsx +2 -2
  39. package/src/components/IconButton.tsx +1 -1
  40. package/src/components/MultiSelect.tsx +1 -1
  41. package/src/components/Select.tsx +2 -2
  42. package/src/components/Sheet.tsx +8 -4
  43. package/src/components/Table.tsx +2 -2
  44. package/src/components/TextField.tsx +3 -3
  45. package/src/components/VirtualTable/VirtualTableRow.tsx +1 -1
  46. package/src/icons/index.ts +3 -0
  47. package/src/index.css +13 -13
  48. package/src/index.ts +2 -0
  49. package/src/styles.ts +9 -9
  50. package/src/views/CardView.tsx +281 -0
  51. package/src/views/CollectionView/CollectionCardView.tsx +270 -0
  52. package/src/views/CollectionView/CollectionKanbanView.tsx +251 -0
  53. package/src/views/CollectionView/CollectionListView.tsx +245 -0
  54. package/src/views/CollectionView/CollectionTableView.tsx +229 -0
  55. package/src/views/CollectionView/CollectionView.tsx +388 -0
  56. package/src/views/CollectionView/CollectionViewToolbar.tsx +229 -0
  57. package/src/views/CollectionView/CollectionViewTypes.ts +137 -0
  58. package/src/views/CollectionView/DefaultCellRenderer.tsx +404 -0
  59. package/src/views/CollectionView/index.ts +44 -0
  60. package/src/views/CollectionView/utils.ts +14 -0
  61. package/src/views/Kanban/Board.tsx +421 -0
  62. package/src/views/Kanban/BoardColumn.tsx +135 -0
  63. package/src/views/Kanban/BoardColumnTitle.tsx +47 -0
  64. package/src/views/Kanban/BoardSortableList.tsx +170 -0
  65. package/src/views/Kanban/board_types.ts +70 -0
  66. package/src/views/Kanban/index.tsx +5 -0
  67. package/src/views/KanbanView.tsx +32 -0
  68. package/src/views/ListView.tsx +281 -0
  69. package/src/views/TableView.tsx +7 -0
  70. package/src/views/index.ts +5 -0
  71. package/dist/index.umd.js +0 -6958
  72. package/dist/index.umd.js.map +0 -1
package/src/styles.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  export const focusedDisabled = "focus-visible:ring-0 focus-visible:ring-offset-0";
2
2
  export const focusedInvisibleMixin = "focus:bg-opacity-70 focus:bg-surface-accent-100 focus:dark:bg-surface-900 focus:dark:bg-opacity-60 focus:bg-surface-accent-100/70 dark:focus:bg-surface-900/60";
3
- export const focusedClasses = "z-30 outline-hidden outline-none ring-2 ring-primary ring-opacity-75 ring-primary/75 ring-offset-0 ring-offset-transparent ";
4
- export const fieldBackgroundMixin = "bg-opacity-50 bg-surface-accent-200 bg-surface-accent-200/50 dark:bg-surface-900 dark:bg-opacity-100 dark:bg-surface-900/100";
5
- export const fieldBackgroundInvisibleMixin = "bg-opacity-0 bg-surface-accent-100 dark:bg-surface-900 dark:bg-opacity-0 bg-surface-accent-200/0 dark:bg-surface-900/0";
6
- export const fieldBackgroundDisabledMixin = "dark:bg-surface-900 bg-opacity-50 dark:bg-opacity-90 bg-surface-accent-200/50 dark:bg-surface-900/90";
7
- export const fieldBackgroundHoverMixin = "hover:bg-opacity-70 dark:hover:bg-surface-700 dark:hover:bg-opacity-40 hover:bg-surface-accent-200/70 hover:dark:bg-surface-700/40";
8
- export const defaultBorderMixin = "border-surface-200/60 dark:border-surface-700/60 ";
9
- export const paperMixin = "bg-white rounded-md dark:bg-surface-800 border border-surface-200/60 dark:border-surface-700/60";
10
- export const cardMixin = "bg-white dark:bg-surface-900 rounded-md border border-surface-200/60 dark:border-surface-700/60 m-1 -p-1";
11
- export const cardClickableMixin = "hover:bg-surface-accent-100 dark:hover:bg-surface-800 hover:ring-1 hover:ring-primary/50 hover:shadow-sm cursor-pointer hover:bg-primary/10 dark:hover:bg-primary/10 transition-all duration-150";
3
+ export const focusedClasses = "z-30 outline-hidden outline-none ring-2 ring-primary ring-opacity-50 ring-primary/50 ring-offset-0 ring-offset-transparent ";
4
+ export const fieldBackgroundMixin = "bg-surface-accent-200/50 dark:bg-black/30";
5
+ export const fieldBackgroundInvisibleMixin = "bg-surface-accent-200/0 dark:bg-black/0";
6
+ export const fieldBackgroundDisabledMixin = "bg-surface-accent-200/50 dark:bg-black/20";
7
+ export const fieldBackgroundHoverMixin = "hover:bg-surface-accent-200/70 hover:dark:bg-black/50";
8
+ export const defaultBorderMixin = "border-surface-200 dark:border-surface-700/60 ";
9
+ export const paperMixin = "bg-white rounded-lg dark:bg-surface-900 border border-surface-200 dark:border-surface-700";
10
+ export const cardMixin = "bg-white dark:bg-surface-900 rounded-lg border border-surface-200 dark:border-surface-700";
11
+ export const cardClickableMixin = "hover:bg-primary/5 dark:hover:bg-primary/5 cursor-pointer transition-colors duration-150";
12
12
  export const cardSelectedMixin = "bg-primary-bg/30 dark:bg-primary-bg/10 ring-1 ring-primary/75";
@@ -0,0 +1,281 @@
1
+ import React, { useCallback, useEffect, useRef } from "react";
2
+ import {
3
+ CircularProgress,
4
+ Typography
5
+ } from "../components";
6
+ import { cls } from "../util";
7
+
8
+ import { CollectionSize } from "./ListView";
9
+
10
+ export type CardViewProps<T> = {
11
+ data: T[];
12
+ dataLoading?: boolean;
13
+ noMoreToLoad?: boolean;
14
+ dataLoadingError?: Error;
15
+ itemCount?: number;
16
+ setItemCount?: (itemCount: number) => void;
17
+ pageSize?: number;
18
+ paginationEnabled?: boolean;
19
+
20
+ onItemClick?: (item: T) => void;
21
+ selectedIds?: Set<string | number>;
22
+ highlightedIds?: Set<string | number>;
23
+ selectionEnabled?: boolean;
24
+ onSelectionChange?: (item: T, selected: boolean) => void;
25
+
26
+ onScroll?: (props: {
27
+ scrollDirection: "forward" | "backward";
28
+ scrollOffset: number;
29
+ scrollUpdateWasRequested: boolean;
30
+ }) => void;
31
+ initialScroll?: number;
32
+
33
+ size?: CollectionSize;
34
+ renderCard: (
35
+ item: T,
36
+ extra: {
37
+ selected: boolean;
38
+ highlighted: boolean;
39
+ onSelectionChange: (selected: boolean) => void;
40
+ onClick: (e: React.MouseEvent) => void;
41
+ }
42
+ ) => React.ReactNode;
43
+ emptyComponent?: React.ReactNode;
44
+ };
45
+
46
+ function getGridColumnsClass(size: CollectionSize): string {
47
+ switch (size) {
48
+ case "xs":
49
+ return "grid-cols-4 sm:grid-cols-5 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10";
50
+ case "s":
51
+ return "grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-8";
52
+ case "m":
53
+ return "grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6";
54
+ case "l":
55
+ return "grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-4";
56
+ case "xl":
57
+ return "grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3";
58
+ default:
59
+ return "grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6";
60
+ }
61
+ }
62
+
63
+ function getScrollParent(element: HTMLElement | null): HTMLElement | null {
64
+ if (!element) return null;
65
+ let parent = element.parentElement;
66
+ while (parent) {
67
+ const overflowY = window.getComputedStyle(parent).overflowY;
68
+ if (overflowY === "auto" || overflowY === "scroll") {
69
+ return parent;
70
+ }
71
+ parent = parent.parentElement;
72
+ }
73
+ return document.documentElement;
74
+ }
75
+
76
+ export function CardView<T>({
77
+ data,
78
+ dataLoading = false,
79
+ noMoreToLoad = false,
80
+ dataLoadingError,
81
+ itemCount,
82
+ setItemCount,
83
+ pageSize = 50,
84
+ paginationEnabled = true,
85
+
86
+ onItemClick,
87
+ selectedIds,
88
+ highlightedIds,
89
+ selectionEnabled = true,
90
+ onSelectionChange,
91
+
92
+ onScroll,
93
+ initialScroll,
94
+ size = "m",
95
+ renderCard,
96
+ emptyComponent
97
+ }: CardViewProps<T>) {
98
+ const containerRef = useRef<HTMLDivElement>(null);
99
+ const hasRestoredScroll = useRef(false);
100
+ const isLoadingMore = useRef(false);
101
+
102
+ // Sync mutable ref with pagination settings to avoid resetting listeners
103
+ const paginationStateRef = useRef({ paginationEnabled, noMoreToLoad, itemCount, pageSize });
104
+ useEffect(() => {
105
+ paginationStateRef.current = { paginationEnabled, noMoreToLoad, itemCount, pageSize };
106
+ }, [paginationEnabled, noMoreToLoad, itemCount, pageSize]);
107
+
108
+ useEffect(() => {
109
+ if (!dataLoading) isLoadingMore.current = false;
110
+ }, [dataLoading]);
111
+
112
+ // Infinite scroll and resize observer
113
+ useEffect(() => {
114
+ const el = containerRef.current;
115
+ if (!el) return;
116
+ const scrollEl = getScrollParent(el);
117
+ if (!scrollEl) return;
118
+
119
+ let rafId: number | null = null;
120
+
121
+ const update = () => {
122
+ rafId = null;
123
+ const { paginationEnabled: pe, noMoreToLoad: nm, itemCount: ic, pageSize: ps } = paginationStateRef.current;
124
+ if (
125
+ pe &&
126
+ !nm &&
127
+ !isLoadingMore.current &&
128
+ scrollEl.scrollHeight - scrollEl.scrollTop - scrollEl.clientHeight < 400
129
+ ) {
130
+ isLoadingMore.current = true;
131
+ setItemCount?.((ic ?? ps) + ps);
132
+ }
133
+ };
134
+
135
+ const onScrollEvent = () => {
136
+ if (rafId === null) rafId = requestAnimationFrame(update);
137
+ };
138
+
139
+ scrollEl.addEventListener("scroll", onScrollEvent, { passive: true });
140
+ const ro = new ResizeObserver(() => update());
141
+ ro.observe(scrollEl);
142
+ update();
143
+
144
+ return () => {
145
+ scrollEl.removeEventListener("scroll", onScrollEvent);
146
+ ro.disconnect();
147
+ if (rafId !== null) cancelAnimationFrame(rafId);
148
+ };
149
+ }, [setItemCount]);
150
+
151
+ // Scroll restoration
152
+ useEffect(() => {
153
+ if (!containerRef.current || !initialScroll || hasRestoredScroll.current || data.length === 0) return;
154
+
155
+ const scrollEl = getScrollParent(containerRef.current);
156
+ if (!scrollEl) return;
157
+
158
+ let attempts = 0;
159
+ const maxAttempts = 5;
160
+
161
+ const tryRestore = () => {
162
+ if (scrollEl.scrollHeight >= initialScroll || attempts >= maxAttempts) {
163
+ scrollEl.scrollTop = initialScroll;
164
+ hasRestoredScroll.current = true;
165
+ } else {
166
+ attempts++;
167
+ requestAnimationFrame(tryRestore);
168
+ }
169
+ };
170
+
171
+ requestAnimationFrame(tryRestore);
172
+ }, [initialScroll, data.length]);
173
+
174
+ // Scroll tracking: call onScroll callback
175
+ const lastScrollOffset = useRef(0);
176
+ useEffect(() => {
177
+ const el = containerRef.current;
178
+ if (!el || !onScroll) return;
179
+ const scrollEl = getScrollParent(el);
180
+ if (!scrollEl) return;
181
+
182
+ const handleScroll = () => {
183
+ const currentOffset = scrollEl.scrollTop;
184
+ const direction = currentOffset > lastScrollOffset.current ? "forward" : "backward";
185
+ lastScrollOffset.current = currentOffset;
186
+ onScroll({
187
+ scrollDirection: direction,
188
+ scrollOffset: currentOffset,
189
+ scrollUpdateWasRequested: false
190
+ });
191
+ };
192
+
193
+ scrollEl.addEventListener("scroll", handleScroll, { passive: true });
194
+ return () => scrollEl.removeEventListener("scroll", handleScroll);
195
+ }, [onScroll]);
196
+
197
+ const getItemId = useCallback((item: T): string | number => {
198
+ if (item && typeof item === "object" && "id" in item) {
199
+ const id = (item as Record<"id", unknown>).id;
200
+ return typeof id === "number" ? id : String(id);
201
+ }
202
+ return String(item);
203
+ }, []);
204
+
205
+ const gridColumnsClass = getGridColumnsClass(size);
206
+
207
+ const isInitialLoading = dataLoading && data.length === 0 && !dataLoadingError;
208
+ const isEmpty = !dataLoading && data.length === 0 && !dataLoadingError;
209
+
210
+ return (
211
+ <div
212
+ ref={containerRef}
213
+ className="w-full p-4"
214
+ >
215
+ {dataLoadingError && data.length === 0 ? (
216
+ <div className="h-full flex items-center justify-center p-8">
217
+ <Typography className="text-red-500">
218
+ Error loading data: {dataLoadingError.message}
219
+ </Typography>
220
+ </div>
221
+ ) : isInitialLoading ? (
222
+ <div className="flex items-center justify-center py-12 px-8">
223
+ <CircularProgress size="small"/>
224
+ </div>
225
+ ) : isEmpty ? (
226
+ <div className="w-full flex items-center justify-center py-12 px-8">
227
+ {emptyComponent ?? (
228
+ <Typography variant="label" color="secondary">
229
+ No entries found
230
+ </Typography>
231
+ )}
232
+ </div>
233
+ ) : (
234
+ <div className="max-w-7xl mx-auto">
235
+ <div className={cls("grid gap-4", gridColumnsClass)}>
236
+ {data.map((item, index) => {
237
+ const id = getItemId(item);
238
+ const selected = selectedIds?.has(id) ?? false;
239
+ const highlighted = highlightedIds?.has(id) ?? false;
240
+
241
+ const handleClick = (e: React.MouseEvent) => {
242
+ if ((e.metaKey || e.ctrlKey) && selectionEnabled) {
243
+ e.preventDefault();
244
+ onSelectionChange?.(item, !selected);
245
+ return;
246
+ }
247
+ onItemClick?.(item);
248
+ };
249
+
250
+ const handleSelectionChange = (val: boolean) => {
251
+ onSelectionChange?.(item, val);
252
+ };
253
+
254
+ return (
255
+ <React.Fragment key={id || index}>
256
+ {renderCard(item, {
257
+ selected,
258
+ highlighted,
259
+ onSelectionChange: handleSelectionChange,
260
+ onClick: handleClick
261
+ })}
262
+ </React.Fragment>
263
+ );
264
+ })}
265
+ </div>
266
+
267
+ <div className="flex items-center justify-center py-8">
268
+ {dataLoading && (
269
+ <CircularProgress size="small"/>
270
+ )}
271
+ {!dataLoading && noMoreToLoad && data.length > 0 && (
272
+ <Typography variant="caption" color="secondary">
273
+ All {data.length} entries loaded
274
+ </Typography>
275
+ )}
276
+ </div>
277
+ </div>
278
+ )}
279
+ </div>
280
+ );
281
+ }
@@ -0,0 +1,270 @@
1
+ "use client";
2
+ import React, { useCallback, useMemo } from "react";
3
+ import { CardView } from "../CardView";
4
+ import { Card } from "../../components/Card";
5
+ import { Typography } from "../../components/Typography";
6
+ import { Checkbox } from "../../components/Checkbox";
7
+ import { DefaultCellRenderer } from "./DefaultCellRenderer";
8
+ import { cls } from "../../util";
9
+ import { cardSelectedMixin, fieldBackgroundMixin } from "../../styles";
10
+ import type {
11
+ CollectionPropertyConfig,
12
+ CollectionDataController,
13
+ CellRendererOverride,
14
+ CollectionViewSize,
15
+ CollectionSelectionController,
16
+ } from "./CollectionViewTypes";
17
+
18
+ import { getValueInPath } from "./utils";
19
+
20
+ /**
21
+ * Props for the {@link CollectionCardView} component.
22
+ * @group Collection View
23
+ */
24
+ export type CollectionCardViewProps<T = Record<string, unknown>> = {
25
+ dataController: CollectionDataController<T>;
26
+ properties: Record<string, CollectionPropertyConfig>;
27
+ propertiesOrder?: string[];
28
+ idProperty?: string;
29
+ titleProperty?: string;
30
+ onRowClick?: (row: T) => void;
31
+ cellRenderer?: CellRendererOverride<T>;
32
+ size?: CollectionViewSize;
33
+ selectionEnabled?: boolean;
34
+ selectionController?: CollectionSelectionController<T>;
35
+ highlightedItems?: T[];
36
+ emptyComponent?: React.ReactNode;
37
+ className?: string;
38
+ };
39
+
40
+ /**
41
+ * Headless card grid view that wraps {@link CardView}.
42
+ *
43
+ * Renders each item as a card with an optional image, title,
44
+ * preview properties, and selection support.
45
+ *
46
+ * @group Collection View
47
+ */
48
+ export function CollectionCardView<T extends Record<string, unknown> = Record<string, unknown>>({
49
+ dataController,
50
+ properties,
51
+ propertiesOrder,
52
+ idProperty = "id",
53
+ titleProperty,
54
+ onRowClick,
55
+ cellRenderer,
56
+ size = "m",
57
+ selectionEnabled,
58
+ selectionController,
59
+ highlightedItems,
60
+ emptyComponent,
61
+ className,
62
+ }: CollectionCardViewProps<T>) {
63
+ const order = useMemo(
64
+ () => propertiesOrder ?? Object.keys(properties),
65
+ [propertiesOrder, properties]
66
+ );
67
+
68
+ // ── Resolve title property ───────────────────────────────────────
69
+ const titlePropertyKey = useMemo(() => {
70
+ if (titleProperty) return titleProperty;
71
+ for (const key of order) {
72
+ const prop = properties[key];
73
+ if (prop && prop.type === "string" && !prop.hideFromCollection && !prop.storage) {
74
+ return key;
75
+ }
76
+ }
77
+ return idProperty;
78
+ }, [titleProperty, order, properties, idProperty]);
79
+
80
+ // ── Resolve image property ───────────────────────────────────────
81
+ const imageProperty = useMemo(() => {
82
+ for (const key of order) {
83
+ const prop = properties[key];
84
+ if (prop && prop.type === "string" && prop.storage) {
85
+ return key;
86
+ }
87
+ }
88
+ return undefined;
89
+ }, [order, properties]);
90
+
91
+ // ── Preview properties (up to 3) ─────────────────────────────────
92
+ const previewProperties = useMemo(() => {
93
+ const result: { key: string; property: CollectionPropertyConfig }[] = [];
94
+ for (const key of order) {
95
+ if (key === titlePropertyKey || key === imageProperty) continue;
96
+ const prop = properties[key];
97
+ if (prop && !prop.hideFromCollection) {
98
+ result.push({ key, property: prop });
99
+ if (result.length >= 3) break;
100
+ }
101
+ }
102
+ return result;
103
+ }, [order, properties, titlePropertyKey, imageProperty]);
104
+
105
+ // ── Selection IDs ────────────────────────────────────────────────
106
+ const selectedIds = useMemo(() => {
107
+ if (!selectionController?.selectedItems) return undefined;
108
+ const ids = new Set<string | number>();
109
+ for (const item of selectionController.selectedItems) {
110
+ const id = getValueInPath(item as Record<string, unknown>, idProperty);
111
+ if (id !== undefined && id !== null) {
112
+ ids.add(id as string | number);
113
+ }
114
+ }
115
+ return ids;
116
+ }, [selectionController?.selectedItems, idProperty]);
117
+
118
+ // ── Highlighted IDs ──────────────────────────────────────────────
119
+ const highlightedIds = useMemo(() => {
120
+ if (!highlightedItems) return undefined;
121
+ const ids = new Set<string | number>();
122
+ for (const item of highlightedItems) {
123
+ const id = getValueInPath(item as Record<string, unknown>, idProperty);
124
+ if (id !== undefined && id !== null) {
125
+ ids.add(id as string | number);
126
+ }
127
+ }
128
+ return ids;
129
+ }, [highlightedItems, idProperty]);
130
+
131
+ // ── Selection handler ────────────────────────────────────────────
132
+ const handleSelectionChange = useCallback(
133
+ (item: T, selected: boolean) => {
134
+ if (!selectionController) return;
135
+ selectionController.setSelectedItems((prev) => {
136
+ if (selected) {
137
+ return [...prev, item];
138
+ }
139
+ const itemId = getValueInPath(item as Record<string, unknown>, idProperty);
140
+ return prev.filter((existing) => {
141
+ const existingId = getValueInPath(existing as Record<string, unknown>, idProperty);
142
+ return existingId !== itemId;
143
+ });
144
+ });
145
+ },
146
+ [selectionController, idProperty]
147
+ );
148
+
149
+ // ── Render card ──────────────────────────────────────────────────
150
+ const renderCard = useCallback(
151
+ (
152
+ item: T,
153
+ extra: {
154
+ selected: boolean;
155
+ highlighted: boolean;
156
+ onSelectionChange: (selected: boolean) => void;
157
+ onClick: (e: React.MouseEvent) => void;
158
+ }
159
+ ): React.ReactNode => {
160
+ const titleValue = getValueInPath(item as Record<string, unknown>, titlePropertyKey);
161
+ const imageValue = imageProperty
162
+ ? getValueInPath(item as Record<string, unknown>, imageProperty)
163
+ : undefined;
164
+ const hasImage = typeof imageValue === "string" && imageValue.length > 0;
165
+
166
+ return (
167
+ <Card
168
+ onClick={(e) => { if (e) extra.onClick(e); }}
169
+ className={cls(
170
+ "overflow-hidden relative",
171
+ extra.selected && cardSelectedMixin,
172
+ extra.highlighted && !extra.selected && "ring-1 ring-primary/50"
173
+ )}
174
+ >
175
+ {/* Image */}
176
+ {imageProperty && (
177
+ hasImage ? (
178
+ <img
179
+ src={imageValue as string}
180
+ alt=""
181
+ className="w-full h-32 object-cover rounded-t-lg"
182
+ />
183
+ ) : (
184
+ <div className={cls("h-24 rounded-t-lg", fieldBackgroundMixin)} />
185
+ )
186
+ )}
187
+
188
+ {/* Selection checkbox */}
189
+ {selectionEnabled && (
190
+ <div
191
+ className="absolute top-2 right-2"
192
+ onClick={(e) => e.stopPropagation()}
193
+ >
194
+ <Checkbox
195
+ checked={extra.selected}
196
+ onCheckedChange={extra.onSelectionChange}
197
+ size="small"
198
+ />
199
+ </div>
200
+ )}
201
+
202
+ {/* Title */}
203
+ <Typography variant="subtitle2" noWrap className="px-3 pt-2 pb-1">
204
+ {String(titleValue ?? "")}
205
+ </Typography>
206
+
207
+ {/* Preview properties */}
208
+ {previewProperties.map(({ key, property }) => {
209
+ const value = getValueInPath(item as Record<string, unknown>, key);
210
+
211
+ if (cellRenderer) {
212
+ const custom = cellRenderer({
213
+ row: item,
214
+ propertyKey: key,
215
+ property,
216
+ value,
217
+ size: "small",
218
+ });
219
+ if (custom !== undefined) {
220
+ return (
221
+ <div key={key} className="px-3 py-0.5">
222
+ {custom}
223
+ </div>
224
+ );
225
+ }
226
+ }
227
+
228
+ return (
229
+ <div key={key} className="px-3 py-0.5">
230
+ <DefaultCellRenderer
231
+ row={item}
232
+ propertyKey={key}
233
+ property={property}
234
+ value={value}
235
+ size="small"
236
+ />
237
+ </div>
238
+ );
239
+ })}
240
+
241
+ <div className="pb-2" />
242
+ </Card>
243
+ );
244
+ },
245
+ [titlePropertyKey, imageProperty, previewProperties, cellRenderer, selectionEnabled]
246
+ );
247
+
248
+ return (
249
+ <div className={cls("w-full h-full overflow-auto", className)}>
250
+ <CardView<T>
251
+ data={dataController.data}
252
+ dataLoading={dataController.loading}
253
+ noMoreToLoad={dataController.noMoreToLoad}
254
+ dataLoadingError={dataController.error}
255
+ itemCount={dataController.itemCount}
256
+ setItemCount={dataController.setItemCount}
257
+ pageSize={dataController.pageSize}
258
+ paginationEnabled={dataController.paginationEnabled}
259
+ onItemClick={onRowClick}
260
+ selectedIds={selectedIds}
261
+ highlightedIds={highlightedIds}
262
+ selectionEnabled={!!selectionEnabled}
263
+ onSelectionChange={handleSelectionChange}
264
+ size={size}
265
+ renderCard={renderCard}
266
+ emptyComponent={emptyComponent}
267
+ />
268
+ </div>
269
+ );
270
+ }