@rebasepro/ui 0.6.1 → 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 (52) hide show
  1. package/dist/icons/index.d.ts +1 -1
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.es.js +2058 -16
  4. package/dist/index.es.js.map +1 -1
  5. package/dist/styles.d.ts +1 -1
  6. package/dist/views/CardView.d.ts +32 -0
  7. package/dist/views/CollectionView/CollectionCardView.d.ts +30 -0
  8. package/dist/views/CollectionView/CollectionKanbanView.d.ts +31 -0
  9. package/dist/views/CollectionView/CollectionListView.d.ts +30 -0
  10. package/dist/views/CollectionView/CollectionTableView.d.ts +37 -0
  11. package/dist/views/CollectionView/CollectionView.d.ts +121 -0
  12. package/dist/views/CollectionView/CollectionViewToolbar.d.ts +31 -0
  13. package/dist/views/CollectionView/CollectionViewTypes.d.ts +106 -0
  14. package/dist/views/CollectionView/DefaultCellRenderer.d.ts +9 -0
  15. package/dist/views/CollectionView/index.d.ts +24 -0
  16. package/dist/views/CollectionView/utils.d.ts +5 -0
  17. package/dist/views/Kanban/Board.d.ts +3 -0
  18. package/dist/views/Kanban/BoardColumn.d.ts +20 -0
  19. package/dist/views/Kanban/BoardColumnTitle.d.ts +8 -0
  20. package/dist/views/Kanban/BoardSortableList.d.ts +14 -0
  21. package/dist/views/Kanban/board_types.d.ts +61 -0
  22. package/dist/views/Kanban/index.d.ts +5 -0
  23. package/dist/views/KanbanView.d.ts +24 -0
  24. package/dist/views/ListView.d.ts +40 -0
  25. package/dist/views/TableView.d.ts +6 -0
  26. package/dist/views/index.d.ts +5 -0
  27. package/package.json +1 -1
  28. package/src/components/Sheet.tsx +8 -4
  29. package/src/icons/index.ts +3 -0
  30. package/src/index.ts +2 -0
  31. package/src/styles.ts +1 -1
  32. package/src/views/CardView.tsx +281 -0
  33. package/src/views/CollectionView/CollectionCardView.tsx +270 -0
  34. package/src/views/CollectionView/CollectionKanbanView.tsx +251 -0
  35. package/src/views/CollectionView/CollectionListView.tsx +245 -0
  36. package/src/views/CollectionView/CollectionTableView.tsx +229 -0
  37. package/src/views/CollectionView/CollectionView.tsx +388 -0
  38. package/src/views/CollectionView/CollectionViewToolbar.tsx +229 -0
  39. package/src/views/CollectionView/CollectionViewTypes.ts +137 -0
  40. package/src/views/CollectionView/DefaultCellRenderer.tsx +404 -0
  41. package/src/views/CollectionView/index.ts +44 -0
  42. package/src/views/CollectionView/utils.ts +14 -0
  43. package/src/views/Kanban/Board.tsx +421 -0
  44. package/src/views/Kanban/BoardColumn.tsx +135 -0
  45. package/src/views/Kanban/BoardColumnTitle.tsx +47 -0
  46. package/src/views/Kanban/BoardSortableList.tsx +170 -0
  47. package/src/views/Kanban/board_types.ts +70 -0
  48. package/src/views/Kanban/index.tsx +5 -0
  49. package/src/views/KanbanView.tsx +32 -0
  50. package/src/views/ListView.tsx +281 -0
  51. package/src/views/TableView.tsx +7 -0
  52. package/src/views/index.ts +5 -0
@@ -0,0 +1,229 @@
1
+ "use client";
2
+ import React, { useCallback, useMemo } from "react";
3
+ import { VirtualTable } from "../../components/VirtualTable/VirtualTable";
4
+ import type { VirtualTableColumn, CellRendererParams, OnVirtualTableColumnResizeParams } from "../../components/VirtualTable/VirtualTableProps";
5
+ import { DefaultCellRenderer } from "./DefaultCellRenderer";
6
+ import { cls } from "../../util";
7
+ import type {
8
+ CollectionPropertyConfig,
9
+ CollectionDataController,
10
+ CellRendererOverride,
11
+ CollectionViewSize,
12
+ CollectionSelectionController,
13
+ CellRendererProps,
14
+ } from "./CollectionViewTypes";
15
+
16
+ import { getValueInPath } from "./utils";
17
+
18
+ const DEFAULT_COLUMN_WIDTHS: Record<string, number> = {
19
+ string: 200,
20
+ number: 140,
21
+ date: 160,
22
+ boolean: 120,
23
+ array: 200,
24
+ map: 200,
25
+ reference: 200,
26
+ relation: 200,
27
+ geopoint: 180,
28
+ };
29
+ const FALLBACK_COLUMN_WIDTH = 200;
30
+
31
+ const SIZE_TO_ROW_HEIGHT: Record<CollectionViewSize, number> = {
32
+ xs: 36,
33
+ s: 44,
34
+ m: 52,
35
+ l: 64,
36
+ xl: 80,
37
+ };
38
+
39
+ const SORTABLE_TYPES = new Set(["string", "number", "date"]);
40
+
41
+ type RendererSize = "tiny" | "small" | "medium" | "large";
42
+
43
+ const VIEW_SIZE_TO_RENDERER_SIZE: Record<CollectionViewSize, RendererSize> = {
44
+ xs: "tiny",
45
+ s: "small",
46
+ m: "medium",
47
+ l: "large",
48
+ xl: "large",
49
+ };
50
+
51
+ /**
52
+ * Props for the {@link CollectionTableView} component.
53
+ * @group Collection View
54
+ */
55
+ export type CollectionTableViewProps<T = Record<string, unknown>> = {
56
+ dataController: CollectionDataController<T>;
57
+ properties: Record<string, CollectionPropertyConfig>;
58
+ propertiesOrder?: string[];
59
+ displayedColumnIds?: string[];
60
+ idProperty?: string;
61
+ titleProperty?: string;
62
+ onRowClick?: (row: T) => void;
63
+ cellRenderer?: CellRendererOverride<T>;
64
+ size?: CollectionViewSize;
65
+ selectionEnabled?: boolean;
66
+ selectionController?: CollectionSelectionController<T>;
67
+ highlightedItems?: T[];
68
+ emptyComponent?: React.ReactNode;
69
+ hoverRow?: boolean;
70
+ className?: string;
71
+ onColumnResize?: (params: { key: string; width: number }) => void;
72
+ onColumnsOrderChange?: (keys: string[]) => void;
73
+ };
74
+
75
+ /**
76
+ * Headless table view that wraps {@link VirtualTable}.
77
+ *
78
+ * Converts property configurations into virtual table columns and wires up
79
+ * sorting, pagination, and cell rendering without coupling to any data layer.
80
+ *
81
+ * @group Collection View
82
+ */
83
+ export function CollectionTableView<T extends Record<string, unknown> = Record<string, unknown>>({
84
+ dataController,
85
+ properties,
86
+ propertiesOrder,
87
+ displayedColumnIds,
88
+ onRowClick,
89
+ cellRenderer,
90
+ size = "m",
91
+ emptyComponent,
92
+ hoverRow = true,
93
+ className,
94
+ onColumnResize,
95
+ onColumnsOrderChange,
96
+ }: CollectionTableViewProps<T>) {
97
+ // ── Build columns ────────────────────────────────────────────────
98
+ const columns = useMemo<VirtualTableColumn[]>(() => {
99
+ const order = propertiesOrder ?? Object.keys(properties);
100
+ const filteredKeys = displayedColumnIds
101
+ ? order.filter((key) => displayedColumnIds.includes(key))
102
+ : order.filter((key) => {
103
+ const prop = properties[key];
104
+ return prop && !prop.hideFromCollection;
105
+ });
106
+
107
+ return filteredKeys
108
+ .map((key): VirtualTableColumn | null => {
109
+ const prop = properties[key];
110
+ if (!prop) return null;
111
+ return {
112
+ key,
113
+ width: prop.columnWidth || DEFAULT_COLUMN_WIDTHS[prop.type] || FALLBACK_COLUMN_WIDTH,
114
+ title: prop.name,
115
+ sortable: SORTABLE_TYPES.has(prop.type),
116
+ resizable: true,
117
+ };
118
+ })
119
+ .filter((col): col is VirtualTableColumn => col !== null);
120
+ }, [properties, propertiesOrder, displayedColumnIds]);
121
+
122
+ // ── Row height ───────────────────────────────────────────────────
123
+ const rowHeight = SIZE_TO_ROW_HEIGHT[size];
124
+ const rendererSize = VIEW_SIZE_TO_RENDERER_SIZE[size];
125
+
126
+ // ── Cell renderer ────────────────────────────────────────────────
127
+ const tableCellRenderer = useCallback(
128
+ (params: CellRendererParams<Record<string, unknown>>): React.ReactNode => {
129
+ if (!params.rowData) return null;
130
+
131
+ const propertyConfig = properties[params.column.key];
132
+ if (!propertyConfig) return null;
133
+
134
+ const value = getValueInPath(
135
+ params.rowData,
136
+ params.column.key
137
+ );
138
+
139
+ const cellProps: CellRendererProps<T> = {
140
+ row: params.rowData as T,
141
+ propertyKey: params.column.key,
142
+ property: propertyConfig,
143
+ value,
144
+ size: rendererSize,
145
+ width: params.width,
146
+ };
147
+
148
+ // Try custom renderer first
149
+ if (cellRenderer) {
150
+ const custom = cellRenderer(cellProps);
151
+ if (custom !== undefined) {
152
+ return (
153
+ <div className="flex items-center h-full px-1 overflow-hidden">
154
+ {custom}
155
+ </div>
156
+ );
157
+ }
158
+ }
159
+
160
+ return (
161
+ <div className="flex items-center h-full px-1 overflow-hidden">
162
+ <DefaultCellRenderer {...cellProps} />
163
+ </div>
164
+ );
165
+ },
166
+ [properties, cellRenderer, rendererSize]
167
+ );
168
+
169
+ // ── Row click ────────────────────────────────────────────────────
170
+ const handleRowClick = useCallback(
171
+ ({ rowData }: { rowData: Record<string, unknown> }) => {
172
+ onRowClick?.(rowData as T);
173
+ },
174
+ [onRowClick]
175
+ );
176
+
177
+ // ── Pagination ──────────────────────────────────────────────────
178
+ const handleEndReached = useCallback(() => {
179
+ if (dataController.paginationEnabled && !dataController.noMoreToLoad) {
180
+ const currentCount = dataController.itemCount ?? dataController.pageSize ?? 50;
181
+ const pageSize = dataController.pageSize ?? 50;
182
+ dataController.setItemCount?.(currentCount + pageSize);
183
+ }
184
+ }, [
185
+ dataController.paginationEnabled,
186
+ dataController.noMoreToLoad,
187
+ dataController.itemCount,
188
+ dataController.pageSize,
189
+ dataController.setItemCount,
190
+ ]);
191
+
192
+ // ── Column resize ───────────────────────────────────────────────
193
+ const handleColumnResize = useCallback(
194
+ (params: OnVirtualTableColumnResizeParams) => {
195
+ onColumnResize?.({ key: params.key, width: params.width });
196
+ },
197
+ [onColumnResize]
198
+ );
199
+
200
+ // ── Column reorder ──────────────────────────────────────────────
201
+ const handleColumnsOrderChange = useCallback(
202
+ (newColumns: VirtualTableColumn[]) => {
203
+ onColumnsOrderChange?.(newColumns.map((col) => col.key));
204
+ },
205
+ [onColumnsOrderChange]
206
+ );
207
+
208
+ return (
209
+ <VirtualTable
210
+ data={dataController.data}
211
+ columns={columns}
212
+ cellRenderer={tableCellRenderer}
213
+ rowHeight={rowHeight}
214
+ sortBy={dataController.sortBy}
215
+ onSortByUpdate={dataController.setSortBy}
216
+ onRowClick={handleRowClick}
217
+ onEndReached={handleEndReached}
218
+ onColumnResize={handleColumnResize}
219
+ onColumnsOrderChange={handleColumnsOrderChange}
220
+ loading={dataController.loading}
221
+ emptyComponent={emptyComponent}
222
+ error={dataController.error}
223
+ hoverRow={hoverRow}
224
+ className={cls("h-full", className)}
225
+ onScroll={dataController.onScroll}
226
+ initialScroll={dataController.initialScroll}
227
+ />
228
+ );
229
+ }
@@ -0,0 +1,388 @@
1
+ import React, { useCallback, useMemo, useState } from "react";
2
+ import { cls } from "../../util";
3
+ import { Typography } from "../../components/Typography";
4
+ import { CollectionViewToolbar } from "./CollectionViewToolbar";
5
+ import { CollectionTableView } from "./CollectionTableView";
6
+ import { CollectionCardView } from "./CollectionCardView";
7
+ import { CollectionListView } from "./CollectionListView";
8
+ import { CollectionKanbanView } from "./CollectionKanbanView";
9
+ import type {
10
+ CollectionViewMode,
11
+ CollectionViewSize,
12
+ CollectionPropertyConfig,
13
+ CollectionDataController,
14
+ CellRendererOverride,
15
+ CollectionSelectionController,
16
+ KanbanPropertyOption
17
+ } from "./CollectionViewTypes";
18
+
19
+ /**
20
+ * Props for the headless {@link CollectionView} component.
21
+ *
22
+ * This component is data-agnostic — it renders collection data based on
23
+ * property configurations and callbacks, with ZERO coupling to any entity
24
+ * or data layer.
25
+ *
26
+ * @group Collection View
27
+ */
28
+ export interface CollectionViewProps<T = Record<string, unknown>> {
29
+ // ── Data ──────────────────────────────────────────────────────────
30
+
31
+ /**
32
+ * Data controller providing rows, loading state, pagination,
33
+ * filter/sort/search state and setters.
34
+ */
35
+ dataController: CollectionDataController<T>;
36
+
37
+ /**
38
+ * Property definitions keyed by property key.
39
+ * Drives column rendering, cell formatting, and filter UI.
40
+ */
41
+ properties: Record<string, CollectionPropertyConfig>;
42
+
43
+ /**
44
+ * Display order of properties. If omitted, uses Object.keys(properties).
45
+ */
46
+ propertiesOrder?: string[];
47
+
48
+ /**
49
+ * Subset of property keys to actually display as columns.
50
+ * If omitted, all non-hidden properties are shown.
51
+ */
52
+ displayedColumnIds?: string[];
53
+
54
+ // ── Identity ──────────────────────────────────────────────────────
55
+
56
+ /** Property key used as row identifier. Default: "id" */
57
+ idProperty?: string;
58
+
59
+ /** Property key used as the display title in card/list views */
60
+ titleProperty?: string;
61
+
62
+ // ── View configuration ────────────────────────────────────────────
63
+
64
+ /** Title displayed above the collection. `false` to hide. */
65
+ title?: string | false;
66
+
67
+ /** Controlled view mode. If omitted, internal state is used. */
68
+ viewMode?: CollectionViewMode;
69
+
70
+ /** Default view mode when uncontrolled. Default: "list" */
71
+ defaultViewMode?: CollectionViewMode;
72
+
73
+ /** Which view modes are available in the toggle */
74
+ enabledViews?: CollectionViewMode[];
75
+
76
+ /** Callback when view mode changes */
77
+ onViewModeChange?: (mode: CollectionViewMode) => void;
78
+
79
+ // ── Size ──────────────────────────────────────────────────────────
80
+
81
+ /** Controlled size. If omitted, internal state is used. */
82
+ size?: CollectionViewSize;
83
+
84
+ /** Default size. Default: "m" */
85
+ defaultSize?: CollectionViewSize;
86
+
87
+ /** Callback when size changes */
88
+ onSizeChange?: (size: CollectionViewSize) => void;
89
+
90
+ // ── Kanban ─────────────────────────────────────────────────────────
91
+
92
+ /** Property key for kanban column grouping */
93
+ kanbanProperty?: string;
94
+
95
+ /** Available kanban property options (shown in view toggle) */
96
+ kanbanPropertyOptions?: KanbanPropertyOption[];
97
+
98
+ /** Callback when kanban column property changes */
99
+ onKanbanPropertyChange?: (property: string) => void;
100
+
101
+ // ── Interactions ──────────────────────────────────────────────────
102
+
103
+ /** Called when a row is clicked */
104
+ onRowClick?: (row: T) => void;
105
+
106
+ /** Called when the "create" button is clicked */
107
+ onRowCreate?: (defaults?: Record<string, unknown>) => void;
108
+
109
+ /** Whether creation is allowed (shows the + button) */
110
+ canCreate?: boolean;
111
+
112
+ // ── Selection ─────────────────────────────────────────────────────
113
+
114
+ /** Enable row selection checkboxes */
115
+ selectionEnabled?: boolean;
116
+
117
+ /** Selection controller */
118
+ selectionController?: CollectionSelectionController<T>;
119
+
120
+ /** Highlighted items (e.g. recently clicked) */
121
+ highlightedItems?: T[];
122
+
123
+ /** Callback for bulk delete */
124
+ onMultipleDelete?: (rows: T[]) => void;
125
+
126
+ // ── Custom rendering ──────────────────────────────────────────────
127
+
128
+ /**
129
+ * Custom cell renderer override. Return undefined to use default.
130
+ * This is how the connected wrapper injects PropertyPreview.
131
+ */
132
+ cellRenderer?: CellRendererOverride<T>;
133
+
134
+ /** Custom empty state component */
135
+ emptyComponent?: React.ReactNode;
136
+
137
+ // ── Toolbar ───────────────────────────────────────────────────────
138
+
139
+ /** Actions rendered at the start (left) of the toolbar */
140
+ toolbarActionsStart?: React.ReactNode;
141
+
142
+ /** Actions rendered at the end (right) of the toolbar */
143
+ toolbarActionsEnd?: React.ReactNode;
144
+
145
+ /** Hide the built-in toolbar entirely */
146
+ hideToolbar?: boolean;
147
+
148
+ // ── Table-specific ────────────────────────────────────────────────
149
+
150
+ /** Enable row hover highlight in table view */
151
+ hoverRow?: boolean;
152
+
153
+ /** Callback when table columns are resized */
154
+ onColumnResize?: (params: { key: string; width: number }) => void;
155
+
156
+ /** Callback when table columns are reordered */
157
+ onColumnsOrderChange?: (keys: string[]) => void;
158
+
159
+ // ── Style ─────────────────────────────────────────────────────────
160
+
161
+ /** CSS class for the outer container */
162
+ className?: string;
163
+ }
164
+
165
+ const DEFAULT_ENABLED_VIEWS: CollectionViewMode[] = ["list", "table", "cards", "kanban"];
166
+
167
+ /**
168
+ * A data-agnostic collection view component that orchestrates table, card,
169
+ * list, and kanban views — driven entirely by props.
170
+ *
171
+ * Zero imports from any entity or data layer. This component lives in
172
+ * `@rebasepro/ui` and can be used with any data source.
173
+ *
174
+ * @example
175
+ * ```tsx
176
+ * import { CollectionView } from "@rebasepro/ui";
177
+ *
178
+ * <CollectionView
179
+ * dataController={{ data: myRows, loading: false, noMoreToLoad: true }}
180
+ * properties={{
181
+ * name: { type: "string", name: "Name" },
182
+ * email: { type: "string", name: "Email" },
183
+ * }}
184
+ * title="Contacts"
185
+ * onRowClick={(row) => openDetail(row.id)}
186
+ * />
187
+ * ```
188
+ *
189
+ * @group Collection View
190
+ */
191
+ export function CollectionView<T extends Record<string, unknown> = Record<string, unknown>>({
192
+ dataController,
193
+ properties,
194
+ propertiesOrder,
195
+ displayedColumnIds,
196
+ idProperty = "id",
197
+ titleProperty,
198
+ title,
199
+ viewMode: viewModeProp,
200
+ defaultViewMode = "list",
201
+ enabledViews = DEFAULT_ENABLED_VIEWS,
202
+ onViewModeChange: onViewModeChangeProp,
203
+ size: sizeProp,
204
+ defaultSize = "m",
205
+ onSizeChange: onSizeChangeProp,
206
+ kanbanProperty: kanbanPropertyProp,
207
+ kanbanPropertyOptions,
208
+ onKanbanPropertyChange: onKanbanPropertyChangeProp,
209
+ onRowClick,
210
+ onRowCreate,
211
+ canCreate = true,
212
+ selectionEnabled,
213
+ selectionController,
214
+ highlightedItems,
215
+ onMultipleDelete,
216
+ cellRenderer,
217
+ emptyComponent,
218
+ toolbarActionsStart,
219
+ toolbarActionsEnd,
220
+ hideToolbar,
221
+ hoverRow = true,
222
+ onColumnResize,
223
+ onColumnsOrderChange,
224
+ className
225
+ }: CollectionViewProps<T>) {
226
+
227
+ // ── View mode state (uncontrolled with controlled override) ───────
228
+
229
+ const [internalViewMode, setInternalViewMode] = useState<CollectionViewMode>(defaultViewMode);
230
+ const viewMode = viewModeProp ?? internalViewMode;
231
+
232
+ const onViewModeChange = useCallback((mode: CollectionViewMode) => {
233
+ setInternalViewMode(mode);
234
+ onViewModeChangeProp?.(mode);
235
+ }, [onViewModeChangeProp]);
236
+
237
+ // ── Size state (uncontrolled with controlled override) ────────────
238
+
239
+ const [internalSize, setInternalSize] = useState<CollectionViewSize>(defaultSize);
240
+ const size = sizeProp ?? internalSize;
241
+
242
+ const onSizeChange = useCallback((s: CollectionViewSize) => {
243
+ setInternalSize(s);
244
+ onSizeChangeProp?.(s);
245
+ }, [onSizeChangeProp]);
246
+
247
+ // ── Kanban property state ─────────────────────────────────────────
248
+
249
+ const [internalKanbanProperty, setInternalKanbanProperty] = useState<string | undefined>(kanbanPropertyProp);
250
+ const kanbanProperty = kanbanPropertyProp ?? internalKanbanProperty;
251
+
252
+ const onKanbanPropertyChange = useCallback((property: string) => {
253
+ setInternalKanbanProperty(property);
254
+ onKanbanPropertyChangeProp?.(property);
255
+ }, [onKanbanPropertyChangeProp]);
256
+
257
+ // Auto-detect kanban property if not provided
258
+ const resolvedKanbanProperty = useMemo(() => {
259
+ if (kanbanProperty) return kanbanProperty;
260
+ // Find first enum property
261
+ const order = propertiesOrder ?? Object.keys(properties);
262
+ for (const key of order) {
263
+ const prop = properties[key];
264
+ if (prop && prop.type === "string" && prop.enum) {
265
+ return key;
266
+ }
267
+ }
268
+ return undefined;
269
+ }, [kanbanProperty, properties, propertiesOrder]);
270
+
271
+ // Build kanban property options from properties if not provided
272
+ const resolvedKanbanPropertyOptions = useMemo((): KanbanPropertyOption[] => {
273
+ if (kanbanPropertyOptions) return kanbanPropertyOptions;
274
+ const options: KanbanPropertyOption[] = [];
275
+ const order = propertiesOrder ?? Object.keys(properties);
276
+ for (const key of order) {
277
+ const prop = properties[key];
278
+ if (prop && prop.type === "string" && prop.enum) {
279
+ options.push({ key, label: prop.name });
280
+ }
281
+ }
282
+ return options;
283
+ }, [kanbanPropertyOptions, properties, propertiesOrder]);
284
+
285
+ // ── Render ────────────────────────────────────────────────────────
286
+
287
+ const sharedProps = {
288
+ dataController,
289
+ properties,
290
+ propertiesOrder,
291
+ idProperty,
292
+ titleProperty,
293
+ onRowClick,
294
+ cellRenderer,
295
+ selectionEnabled,
296
+ selectionController,
297
+ highlightedItems,
298
+ emptyComponent,
299
+ };
300
+
301
+ function renderView() {
302
+ switch (viewMode) {
303
+ case "table":
304
+ return (
305
+ <CollectionTableView<T>
306
+ {...sharedProps}
307
+ displayedColumnIds={displayedColumnIds}
308
+ size={size}
309
+ hoverRow={hoverRow}
310
+ onColumnResize={onColumnResize}
311
+ onColumnsOrderChange={onColumnsOrderChange}
312
+ />
313
+ );
314
+ case "cards":
315
+ return (
316
+ <CollectionCardView<T>
317
+ {...sharedProps}
318
+ size={size}
319
+ />
320
+ );
321
+ case "kanban":
322
+ return resolvedKanbanProperty ? (
323
+ <CollectionKanbanView<T>
324
+ {...sharedProps}
325
+ kanbanProperty={resolvedKanbanProperty}
326
+ onRowCreate={onRowCreate}
327
+ />
328
+ ) : (
329
+ <div className="flex items-center justify-center p-8">
330
+ <Typography variant="body2" color="secondary">
331
+ No enum property available for kanban grouping.
332
+ </Typography>
333
+ </div>
334
+ );
335
+ case "list":
336
+ default:
337
+ return (
338
+ <CollectionListView<T>
339
+ {...sharedProps}
340
+ size={size}
341
+ />
342
+ );
343
+ }
344
+ }
345
+
346
+ return (
347
+ <div
348
+ className={cls(
349
+ "overflow-hidden h-full w-full rounded-md flex flex-col",
350
+ "dark:bg-surface-800",
351
+ className
352
+ )}
353
+ >
354
+ {/* Error banner */}
355
+ {dataController.error && dataController.data.length > 0 && (
356
+ <div className="flex items-center gap-4 px-4 py-2 bg-red-50 dark:bg-red-900/20 border-b border-red-200 dark:border-red-800 flex-shrink-0">
357
+ <Typography variant="body2" className="text-red-700 dark:text-red-300 flex-1">
358
+ <strong>Warning:</strong> {dataController.error.message || "Failed to update data."}
359
+ </Typography>
360
+ </div>
361
+ )}
362
+
363
+ {/* Toolbar */}
364
+ {!hideToolbar && (
365
+ <CollectionViewToolbar
366
+ viewMode={viewMode}
367
+ onViewModeChange={enabledViews.length > 1 ? onViewModeChange : undefined}
368
+ enabledViews={enabledViews}
369
+ size={size}
370
+ onSizeChange={onSizeChange}
371
+ searchString={dataController.searchString}
372
+ onSearchChange={dataController.setSearchString}
373
+ loading={dataController.loading}
374
+ actionsStart={toolbarActionsStart}
375
+ actionsEnd={toolbarActionsEnd}
376
+ kanbanPropertyOptions={viewMode === "kanban" ? resolvedKanbanPropertyOptions : undefined}
377
+ selectedKanbanProperty={resolvedKanbanProperty}
378
+ onKanbanPropertyChange={onKanbanPropertyChange}
379
+ />
380
+ )}
381
+
382
+ {/* View */}
383
+ <div className="flex-1 overflow-hidden">
384
+ {renderView()}
385
+ </div>
386
+ </div>
387
+ );
388
+ }