@rebasepro/ui 0.17.3 → 0.18.1

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 (127) hide show
  1. package/README.md +4 -0
  2. package/dist/components/MultiSelect.d.ts +10 -0
  3. package/dist/index.es.js +2 -1
  4. package/dist/index.es.js.map +1 -1
  5. package/package.json +32 -23
  6. package/src/components/Alert.tsx +0 -118
  7. package/src/components/Autocomplete.tsx +0 -93
  8. package/src/components/Avatar.tsx +0 -73
  9. package/src/components/Badge.tsx +0 -44
  10. package/src/components/BooleanSwitch.tsx +0 -116
  11. package/src/components/BooleanSwitchWithLabel.tsx +0 -120
  12. package/src/components/Button.tsx +0 -110
  13. package/src/components/Card.tsx +0 -43
  14. package/src/components/CenteredView.tsx +0 -35
  15. package/src/components/Checkbox.tsx +0 -129
  16. package/src/components/Chip.tsx +0 -131
  17. package/src/components/CircularProgress.tsx +0 -53
  18. package/src/components/CircularProgressCenter.tsx +0 -27
  19. package/src/components/Collapse.tsx +0 -66
  20. package/src/components/ColorPicker.tsx +0 -140
  21. package/src/components/Container.tsx +0 -51
  22. package/src/components/DateTimeField.tsx +0 -363
  23. package/src/components/DebouncedTextField.tsx +0 -76
  24. package/src/components/Dialog.tsx +0 -159
  25. package/src/components/DialogActions.tsx +0 -29
  26. package/src/components/DialogContent.tsx +0 -27
  27. package/src/components/DialogTitle.tsx +0 -41
  28. package/src/components/ErrorBoundary.tsx +0 -236
  29. package/src/components/ExpandablePanel.tsx +0 -130
  30. package/src/components/FileUpload.tsx +0 -103
  31. package/src/components/FilterChip.tsx +0 -89
  32. package/src/components/IconButton.tsx +0 -101
  33. package/src/components/InfoLabel.tsx +0 -23
  34. package/src/components/InputLabel.tsx +0 -40
  35. package/src/components/Label.tsx +0 -32
  36. package/src/components/LoadingButton.tsx +0 -32
  37. package/src/components/Markdown.tsx +0 -55
  38. package/src/components/Menu.tsx +0 -114
  39. package/src/components/Menubar.tsx +0 -355
  40. package/src/components/MultiSelect.tsx +0 -433
  41. package/src/components/Paper.tsx +0 -23
  42. package/src/components/Popover.tsx +0 -162
  43. package/src/components/RadioGroup.tsx +0 -74
  44. package/src/components/ResizablePanels.tsx +0 -189
  45. package/src/components/SearchBar.tsx +0 -136
  46. package/src/components/Select.tsx +0 -326
  47. package/src/components/Separator.tsx +0 -28
  48. package/src/components/Sheet.tsx +0 -135
  49. package/src/components/Skeleton.tsx +0 -54
  50. package/src/components/Slider.tsx +0 -110
  51. package/src/components/Table.tsx +0 -178
  52. package/src/components/Tabs.tsx +0 -185
  53. package/src/components/TextField.tsx +0 -270
  54. package/src/components/TextareaAutosize.tsx +0 -368
  55. package/src/components/ToggleButtonGroup.tsx +0 -69
  56. package/src/components/Tooltip.tsx +0 -153
  57. package/src/components/Typography.tsx +0 -132
  58. package/src/components/VirtualTable/VirtualTable.performance.test.tsx +0 -90
  59. package/src/components/VirtualTable/VirtualTable.tsx +0 -700
  60. package/src/components/VirtualTable/VirtualTableCell.tsx +0 -59
  61. package/src/components/VirtualTable/VirtualTableHeader.tsx +0 -327
  62. package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +0 -250
  63. package/src/components/VirtualTable/VirtualTableProps.tsx +0 -339
  64. package/src/components/VirtualTable/VirtualTableRow.tsx +0 -51
  65. package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +0 -44
  66. package/src/components/VirtualTable/fields/VirtualTableInput.tsx +0 -89
  67. package/src/components/VirtualTable/fields/VirtualTableNumberInput.tsx +0 -71
  68. package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +0 -107
  69. package/src/components/VirtualTable/fields/VirtualTableSwitch.tsx +0 -28
  70. package/src/components/VirtualTable/index.tsx +0 -10
  71. package/src/components/VirtualTable/selection/SelectionContext.tsx +0 -36
  72. package/src/components/VirtualTable/selection/SelectionStore.ts +0 -54
  73. package/src/components/VirtualTable/types.tsx +0 -47
  74. package/src/components/common/SelectInputLabel.tsx +0 -23
  75. package/src/components/index.tsx +0 -62
  76. package/src/hooks/PortalContainerContext.tsx +0 -50
  77. package/src/hooks/index.ts +0 -7
  78. package/src/hooks/useDebounceCallback.tsx +0 -21
  79. package/src/hooks/useDebounceValue.tsx +0 -20
  80. package/src/hooks/useDebouncedCallback.ts +0 -39
  81. package/src/hooks/useInjectStyles.tsx +0 -31
  82. package/src/hooks/useOutsideAlerter.tsx +0 -46
  83. package/src/icons/GitHubIcon.tsx +0 -21
  84. package/src/icons/HandleIcon.tsx +0 -23
  85. package/src/icons/Icon.tsx +0 -33
  86. package/src/icons/LucideIconByName.tsx +0 -117
  87. package/src/icons/cool_icon_keys.ts +0 -12
  88. package/src/icons/icon_keys.ts +0 -1936
  89. package/src/icons/index.ts +0 -179
  90. package/src/index.css +0 -169
  91. package/src/index.ts +0 -47
  92. package/src/scripts/fixIconSizes.cjs +0 -92
  93. package/src/scripts/fixIconSizesToConst.cjs +0 -106
  94. package/src/scripts/generateIconKeys.ts +0 -41
  95. package/src/scripts/generateIcons.ts +0 -4
  96. package/src/scripts/saveIconFiles.ts +0 -89
  97. package/src/styles.ts +0 -141
  98. package/src/theme.css +0 -168
  99. package/src/util/chip_colors.ts +0 -242
  100. package/src/util/cls.ts +0 -8
  101. package/src/util/debounce.ts +0 -28
  102. package/src/util/hash.ts +0 -11
  103. package/src/util/index.ts +0 -4
  104. package/src/util/key_to_icon_component.ts +0 -11
  105. package/src/util/lazy_chunk.ts +0 -101
  106. package/src/views/CardView.tsx +0 -281
  107. package/src/views/CollectionView/CollectionCardView.tsx +0 -265
  108. package/src/views/CollectionView/CollectionKanbanView.tsx +0 -246
  109. package/src/views/CollectionView/CollectionListView.tsx +0 -240
  110. package/src/views/CollectionView/CollectionTableView.tsx +0 -229
  111. package/src/views/CollectionView/CollectionView.tsx +0 -450
  112. package/src/views/CollectionView/CollectionViewToolbar.tsx +0 -242
  113. package/src/views/CollectionView/CollectionViewTypes.ts +0 -181
  114. package/src/views/CollectionView/DefaultCellRenderer.tsx +0 -404
  115. package/src/views/CollectionView/index.ts +0 -39
  116. package/src/views/CollectionView/resolveTitleProperty.ts +0 -52
  117. package/src/views/CollectionView/utils.ts +0 -14
  118. package/src/views/Kanban/Board.tsx +0 -384
  119. package/src/views/Kanban/BoardColumn.tsx +0 -138
  120. package/src/views/Kanban/BoardColumnTitle.tsx +0 -47
  121. package/src/views/Kanban/BoardSortableList.tsx +0 -182
  122. package/src/views/Kanban/board_types.ts +0 -80
  123. package/src/views/Kanban/index.tsx +0 -5
  124. package/src/views/Kanban/placement.ts +0 -62
  125. package/src/views/KanbanView.tsx +0 -32
  126. package/src/views/ListView.tsx +0 -296
  127. package/src/views/index.ts +0 -5
package/src/util/hash.ts DELETED
@@ -1,11 +0,0 @@
1
- export function hashString(str: string): number {
2
- let hash = 0;
3
- let i;
4
- let chr;
5
- for (i = 0; i < str.length; i++) {
6
- chr = str.charCodeAt(i);
7
- hash = ((hash << 5) - hash) + chr;
8
- hash |= 0; // Convert to 32bit integer
9
- }
10
- return Math.abs(hash);
11
- }
package/src/util/index.ts DELETED
@@ -1,4 +0,0 @@
1
- export * from "./cls";
2
- export * from "./debounce";
3
- export * from "./chip_colors";
4
- export * from "./key_to_icon_component";
@@ -1,11 +0,0 @@
1
- export function keyToIconComponent(key: string) {
2
- const startsWithNumber = key.match(/^\d/);
3
-
4
- // convert key to came case
5
- const componentName = (startsWithNumber ? "_" : "") +
6
- key.split("_").map((word: string) => {
7
- return word.charAt(0).toUpperCase() + word.slice(1);
8
- }).join("") +
9
- "Icon";
10
- return componentName;
11
- }
@@ -1,101 +0,0 @@
1
- import React from "react";
2
-
3
- /**
4
- * Dynamic imports that survive a redeploy.
5
- *
6
- * A built SPA names its chunks by content hash and a deploy replaces the whole
7
- * `assets/` directory, so a tab opened before the deploy still holds the *old*
8
- * entry chunk. The first lazy route that tab opens asks for a hash that no
9
- * longer exists on the server, the import rejects, and the view is dead until
10
- * the user thinks to reload — which nothing on screen tells them to do. The
11
- * browser's own wording ("Failed to fetch dynamically imported module: …") reads
12
- * like a build bug, so this is usually reported as one.
13
- *
14
- * `loadChunk` retries once — that covers a transient network failure — and then
15
- * gives up with an error tagged as a chunk load failure, so `ErrorBoundary` can
16
- * offer a reload instead of printing the browser's message.
17
- */
18
-
19
- /**
20
- * Marker property set on the error thrown after a failed retry. Read by
21
- * `isChunkLoadError`, which is what the UI matches on.
22
- */
23
- const CHUNK_LOAD_ERROR_FLAG = "rebaseChunkLoadError";
24
-
25
- /**
26
- * Each engine words this differently, and the message is the only signal — none
27
- * of them use a distinguishable error type. The MIME variants matter as much as
28
- * the fetch ones: a server whose SPA fallback answers a missing `/assets/x.js`
29
- * with `index.html` returns 200 HTML, and the browser rejects it as a module.
30
- */
31
- const CHUNK_ERROR_PATTERNS = [
32
- "failed to fetch dynamically imported module",
33
- "error loading dynamically imported module",
34
- "importing a module script failed",
35
- "failed to load module script",
36
- "expected a javascript",
37
- "unable to preload css"
38
- ];
39
-
40
- /**
41
- * Does this error mean a chunk could not be loaded — rather than the chunk's
42
- * own code throwing once it did load?
43
- *
44
- * Errors raised anywhere are matched, not just ones `loadChunk` produced: React
45
- * `lazy()` calls that still import directly, and Vite's own CSS preloads, fail
46
- * the same way and deserve the same recovery.
47
- */
48
- export function isChunkLoadError(error: unknown): boolean {
49
- if (!error || typeof error !== "object") return false;
50
- if ((error as Record<string, unknown>)[CHUNK_LOAD_ERROR_FLAG] === true) return true;
51
- const message = (error as { message?: unknown }).message;
52
- if (typeof message !== "string") return false;
53
- const lower = message.toLowerCase();
54
- return CHUNK_ERROR_PATTERNS.some(pattern => lower.includes(pattern));
55
- }
56
-
57
- function chunkLoadError(cause: unknown): Error {
58
- const error = new Error(
59
- "This app was updated while the tab was open, so part of it could not be loaded. Reload to continue."
60
- );
61
- Object.assign(error, { [CHUNK_LOAD_ERROR_FLAG]: true,
62
- cause });
63
- return error;
64
- }
65
-
66
- /**
67
- * Run a dynamic import, retrying once before declaring the chunk unreachable.
68
- *
69
- * Errors that are not chunk load failures — the imported module throwing while
70
- * it evaluates, say — are re-thrown untouched and never retried: running a
71
- * module's side effects twice is worse than the original failure.
72
- */
73
- export async function loadChunk<T>(loader: () => Promise<T>): Promise<T> {
74
- try {
75
- return await loader();
76
- } catch (error) {
77
- if (!isChunkLoadError(error)) throw error;
78
- // A blip resolves inside a few hundred ms; a stale hash never will.
79
- await new Promise(resolve => setTimeout(resolve, 250));
80
- try {
81
- return await loader();
82
- } catch (retryError) {
83
- throw chunkLoadError(retryError);
84
- }
85
- }
86
- }
87
-
88
- /**
89
- * `React.lazy` with the retry above. A drop-in replacement — use it for every
90
- * lazy route or dialog, since any one of them can be the first chunk a stale
91
- * tab reaches for.
92
- *
93
- * Note that React caches the rejection: once a lazy component has failed, later
94
- * renders re-throw without calling the loader again. Resetting an error boundary
95
- * therefore cannot recover it, which is why the boundary offers a reload.
96
- */
97
- export function lazyChunk<T extends React.ComponentType<any>>(
98
- loader: () => Promise<{ default: T }>
99
- ): React.LazyExoticComponent<T> {
100
- return React.lazy(() => loadChunk(loader));
101
- }
@@ -1,281 +0,0 @@
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
- }
@@ -1,265 +0,0 @@
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
- import { resolveTitlePropertyKey } from "./resolveTitleProperty";
18
-
19
- import { getValueInPath } from "./utils";
20
-
21
- /**
22
- * Props for the {@link CollectionCardView} component.
23
- * @group Collection View
24
- */
25
- export type CollectionCardViewProps<T = Record<string, unknown>> = {
26
- dataController: CollectionDataController<T>;
27
- properties: Record<string, CollectionPropertyConfig>;
28
- propertiesOrder?: string[];
29
- idProperty?: string;
30
- titleProperty?: string;
31
- onRowClick?: (row: T) => void;
32
- cellRenderer?: CellRendererOverride<T>;
33
- size?: CollectionViewSize;
34
- selectionEnabled?: boolean;
35
- selectionController?: CollectionSelectionController<T>;
36
- highlightedItems?: T[];
37
- emptyComponent?: React.ReactNode;
38
- className?: string;
39
- };
40
-
41
- /**
42
- * Headless card grid view that wraps {@link CardView}.
43
- *
44
- * Renders each item as a card with an optional image, title,
45
- * preview properties, and selection support.
46
- *
47
- * @group Collection View
48
- */
49
- export function CollectionCardView<T extends Record<string, unknown> = Record<string, unknown>>({
50
- dataController,
51
- properties,
52
- propertiesOrder,
53
- idProperty = "id",
54
- titleProperty,
55
- onRowClick,
56
- cellRenderer,
57
- size = "m",
58
- selectionEnabled,
59
- selectionController,
60
- highlightedItems,
61
- emptyComponent,
62
- className,
63
- }: CollectionCardViewProps<T>) {
64
- const order = useMemo(
65
- () => propertiesOrder ?? Object.keys(properties),
66
- [propertiesOrder, properties]
67
- );
68
-
69
- // ── Resolve title property ───────────────────────────────────────
70
- const titlePropertyKey = useMemo(
71
- () => resolveTitlePropertyKey(order, properties, idProperty, titleProperty),
72
- [titleProperty, order, properties, idProperty]
73
- );
74
-
75
- // ── Resolve image property ───────────────────────────────────────
76
- const imageProperty = useMemo(() => {
77
- for (const key of order) {
78
- const prop = properties[key];
79
- if (prop && prop.type === "string" && prop.storage) {
80
- return key;
81
- }
82
- }
83
- return undefined;
84
- }, [order, properties]);
85
-
86
- // ── Preview properties (up to 3) ─────────────────────────────────
87
- const previewProperties = useMemo(() => {
88
- const result: { key: string; property: CollectionPropertyConfig }[] = [];
89
- for (const key of order) {
90
- if (key === titlePropertyKey || key === imageProperty) continue;
91
- const prop = properties[key];
92
- if (prop && !prop.hideFromCollection) {
93
- result.push({ key, property: prop });
94
- if (result.length >= 3) break;
95
- }
96
- }
97
- return result;
98
- }, [order, properties, titlePropertyKey, imageProperty]);
99
-
100
- // ── Selection IDs ────────────────────────────────────────────────
101
- const selectedIds = useMemo(() => {
102
- if (!selectionController?.selectedItems) return undefined;
103
- const ids = new Set<string | number>();
104
- for (const item of selectionController.selectedItems) {
105
- const id = getValueInPath(item as Record<string, unknown>, idProperty);
106
- if (id !== undefined && id !== null) {
107
- ids.add(id as string | number);
108
- }
109
- }
110
- return ids;
111
- }, [selectionController?.selectedItems, idProperty]);
112
-
113
- // ── Highlighted IDs ──────────────────────────────────────────────
114
- const highlightedIds = useMemo(() => {
115
- if (!highlightedItems) return undefined;
116
- const ids = new Set<string | number>();
117
- for (const item of highlightedItems) {
118
- const id = getValueInPath(item as Record<string, unknown>, idProperty);
119
- if (id !== undefined && id !== null) {
120
- ids.add(id as string | number);
121
- }
122
- }
123
- return ids;
124
- }, [highlightedItems, idProperty]);
125
-
126
- // ── Selection handler ────────────────────────────────────────────
127
- const handleSelectionChange = useCallback(
128
- (item: T, selected: boolean) => {
129
- if (!selectionController) return;
130
- selectionController.setSelectedItems((prev) => {
131
- if (selected) {
132
- return [...prev, item];
133
- }
134
- const itemId = getValueInPath(item as Record<string, unknown>, idProperty);
135
- return prev.filter((existing) => {
136
- const existingId = getValueInPath(existing as Record<string, unknown>, idProperty);
137
- return existingId !== itemId;
138
- });
139
- });
140
- },
141
- [selectionController, idProperty]
142
- );
143
-
144
- // ── Render card ──────────────────────────────────────────────────
145
- const renderCard = useCallback(
146
- (
147
- item: T,
148
- extra: {
149
- selected: boolean;
150
- highlighted: boolean;
151
- onSelectionChange: (selected: boolean) => void;
152
- onClick: (e: React.MouseEvent) => void;
153
- }
154
- ): React.ReactNode => {
155
- const titleValue = getValueInPath(item as Record<string, unknown>, titlePropertyKey);
156
- const imageValue = imageProperty
157
- ? getValueInPath(item as Record<string, unknown>, imageProperty)
158
- : undefined;
159
- const hasImage = typeof imageValue === "string" && imageValue.length > 0;
160
-
161
- return (
162
- <Card
163
- onClick={(e) => { if (e) extra.onClick(e); }}
164
- className={cls(
165
- "overflow-hidden relative",
166
- extra.selected && cardSelectedMixin,
167
- extra.highlighted && !extra.selected && "ring-1 ring-primary/50"
168
- )}
169
- >
170
- {/* Image */}
171
- {imageProperty && (
172
- hasImage ? (
173
- <img
174
- src={imageValue as string}
175
- alt=""
176
- className="w-full h-32 object-cover rounded-t-lg"
177
- />
178
- ) : (
179
- <div className={cls("h-24 rounded-t-lg", fieldBackgroundMixin)} />
180
- )
181
- )}
182
-
183
- {/* Selection checkbox */}
184
- {selectionEnabled && (
185
- <div
186
- className="absolute top-2 right-2"
187
- onClick={(e) => e.stopPropagation()}
188
- >
189
- <Checkbox
190
- checked={extra.selected}
191
- onCheckedChange={extra.onSelectionChange}
192
- size="small"
193
- />
194
- </div>
195
- )}
196
-
197
- {/* Title */}
198
- <Typography variant="subtitle2" noWrap className="px-3 pt-2 pb-1">
199
- {String(titleValue ?? "")}
200
- </Typography>
201
-
202
- {/* Preview properties */}
203
- {previewProperties.map(({ key, property }) => {
204
- const value = getValueInPath(item as Record<string, unknown>, key);
205
-
206
- if (cellRenderer) {
207
- const custom = cellRenderer({
208
- row: item,
209
- propertyKey: key,
210
- property,
211
- value,
212
- size: "small",
213
- });
214
- if (custom !== undefined) {
215
- return (
216
- <div key={key} className="px-3 py-0.5">
217
- {custom}
218
- </div>
219
- );
220
- }
221
- }
222
-
223
- return (
224
- <div key={key} className="px-3 py-0.5">
225
- <DefaultCellRenderer
226
- row={item}
227
- propertyKey={key}
228
- property={property}
229
- value={value}
230
- size="small"
231
- />
232
- </div>
233
- );
234
- })}
235
-
236
- <div className="pb-2" />
237
- </Card>
238
- );
239
- },
240
- [titlePropertyKey, imageProperty, previewProperties, cellRenderer, selectionEnabled]
241
- );
242
-
243
- return (
244
- <div className={cls("w-full h-full overflow-auto", className)}>
245
- <CardView<T>
246
- data={dataController.data}
247
- dataLoading={dataController.loading}
248
- noMoreToLoad={dataController.noMoreToLoad}
249
- dataLoadingError={dataController.error}
250
- itemCount={dataController.itemCount}
251
- setItemCount={dataController.setItemCount}
252
- pageSize={dataController.pageSize}
253
- paginationEnabled={dataController.paginationEnabled}
254
- onItemClick={onRowClick}
255
- selectedIds={selectedIds}
256
- highlightedIds={highlightedIds}
257
- selectionEnabled={!!selectionEnabled}
258
- onSelectionChange={handleSelectionChange}
259
- size={size}
260
- renderCard={renderCard}
261
- emptyComponent={emptyComponent}
262
- />
263
- </div>
264
- );
265
- }