@anymux/ui-kit 0.1.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.
- package/dist/ExplorerLayout-CSIJd7N4.js +105 -0
- package/dist/ExplorerLayout-CSIJd7N4.js.map +1 -0
- package/dist/FileBrowserContext-B6jixa2j.js +11 -0
- package/dist/FileBrowserContext-B6jixa2j.js.map +1 -0
- package/dist/calendar-DSlrbHoj.js +761 -0
- package/dist/calendar-DSlrbHoj.js.map +1 -0
- package/dist/calendar.d.ts +3 -0
- package/dist/calendar.js +3 -0
- package/dist/contacts-DQXTZzHc.js +539 -0
- package/dist/contacts-DQXTZzHc.js.map +1 -0
- package/dist/contacts.d.ts +3 -0
- package/dist/contacts.js +3 -0
- package/dist/file-browser-m5atC3kF.js +6755 -0
- package/dist/file-browser-m5atC3kF.js.map +1 -0
- package/dist/file-browser.d.ts +11 -0
- package/dist/file-browser.js +9 -0
- package/dist/git-B55e6LL-.js +561 -0
- package/dist/git-B55e6LL-.js.map +1 -0
- package/dist/git.d.ts +2 -0
- package/dist/git.js +3 -0
- package/dist/iconMap-V4B8P-Uh.js +206 -0
- package/dist/iconMap-V4B8P-Uh.js.map +1 -0
- package/dist/icons-CIsIOZXR.js +0 -0
- package/dist/icons.d.ts +2 -0
- package/dist/icons.js +4 -0
- package/dist/index-BNmNIWBL.d.ts +71 -0
- package/dist/index-BNmNIWBL.d.ts.map +1 -0
- package/dist/index-Bryv_GCG.d.ts +1481 -0
- package/dist/index-Bryv_GCG.d.ts.map +1 -0
- package/dist/index-CuQIjSXs.d.ts +134 -0
- package/dist/index-CuQIjSXs.d.ts.map +1 -0
- package/dist/index-DSu19mq0.d.ts +153 -0
- package/dist/index-DSu19mq0.d.ts.map +1 -0
- package/dist/index-DmsyeHFr.d.ts +149 -0
- package/dist/index-DmsyeHFr.d.ts.map +1 -0
- package/dist/index-DxnJ8FYM.d.ts +17 -0
- package/dist/index-DxnJ8FYM.d.ts.map +1 -0
- package/dist/index-DzfY1Tok.d.ts +32 -0
- package/dist/index-DzfY1Tok.d.ts.map +1 -0
- package/dist/index-Ml_SgiKa.d.ts +1847 -0
- package/dist/index-Ml_SgiKa.d.ts.map +1 -0
- package/dist/index-kHr9udZD.d.ts +1025 -0
- package/dist/index-kHr9udZD.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +15 -0
- package/dist/layout-Ca_4r8ka.js +89 -0
- package/dist/layout-Ca_4r8ka.js.map +1 -0
- package/dist/layout.d.ts +2 -0
- package/dist/layout.js +5 -0
- package/dist/list-CxfT6hix.js +6831 -0
- package/dist/list-CxfT6hix.js.map +1 -0
- package/dist/list.d.ts +2 -0
- package/dist/list.js +5 -0
- package/dist/media-DZ292aKK.js +557 -0
- package/dist/media-DZ292aKK.js.map +1 -0
- package/dist/media.d.ts +3 -0
- package/dist/media.js +3 -0
- package/dist/tree-Dd9Z0Aso.js +3351 -0
- package/dist/tree-Dd9Z0Aso.js.map +1 -0
- package/dist/tree.d.ts +2 -0
- package/dist/tree.js +6 -0
- package/dist/types-common-CB3kRek8.d.ts +26 -0
- package/dist/types-common-CB3kRek8.d.ts.map +1 -0
- package/dist/utils-B4fdKKsy.js +3 -0
- package/package.json +109 -0
- package/src/calendar/AgendaView.tsx +37 -0
- package/src/calendar/CalendarBrowser.tsx +90 -0
- package/src/calendar/CalendarModel.ts +142 -0
- package/src/calendar/CalendarSidebar.tsx +81 -0
- package/src/calendar/DayView.tsx +76 -0
- package/src/calendar/EventCard.tsx +51 -0
- package/src/calendar/MockCalendarProvider.ts +98 -0
- package/src/calendar/MonthView.tsx +77 -0
- package/src/calendar/WeekView.tsx +129 -0
- package/src/calendar/index.ts +18 -0
- package/src/calendar/types.ts +25 -0
- package/src/contacts/ContactAvatar.tsx +35 -0
- package/src/contacts/ContactBrowser.tsx +56 -0
- package/src/contacts/ContactCard.tsx +37 -0
- package/src/contacts/ContactDetail.tsx +63 -0
- package/src/contacts/ContactGroupSidebar.tsx +40 -0
- package/src/contacts/ContactList.tsx +32 -0
- package/src/contacts/ContactListModel.ts +120 -0
- package/src/contacts/MockContactProvider.ts +77 -0
- package/src/contacts/index.ts +17 -0
- package/src/contacts/types.ts +26 -0
- package/src/demos/CalendarBrowserDemo.tsx +15 -0
- package/src/demos/ContactBrowserDemo.tsx +15 -0
- package/src/demos/MediaBrowserDemo.tsx +15 -0
- package/src/file-browser/adapters/DocumentViewerAdapter.ts +371 -0
- package/src/file-browser/adapters/FileSystemBridge.ts +168 -0
- package/src/file-browser/adapters/GitBrowserAdapter.ts +546 -0
- package/src/file-browser/adapters/README.md +504 -0
- package/src/file-browser/adapters/index.ts +27 -0
- package/src/file-browser/adapters/types.ts +70 -0
- package/src/file-browser/architecture.md +645 -0
- package/src/file-browser/components/CreateItemDialog.tsx +71 -0
- package/src/file-browser/components/DeleteConfirmDialog.tsx +58 -0
- package/src/file-browser/components/FileBrowser.tsx +473 -0
- package/src/file-browser/components/FileBrowserContent.tsx +209 -0
- package/src/file-browser/components/FileBrowserHeader.tsx +151 -0
- package/src/file-browser/components/FileBrowserToolbar.tsx +145 -0
- package/src/file-browser/components/LeftPanel/LeftPanel.tsx +103 -0
- package/src/file-browser/components/LeftPanel/LeftPanelTabs.tsx +70 -0
- package/src/file-browser/components/LeftPanel/TreeNavigationView.tsx +256 -0
- package/src/file-browser/components/PreviewPane.tsx +146 -0
- package/src/file-browser/components/RightPanel/FilePreview.tsx +219 -0
- package/src/file-browser/components/RightPanel/RightPanel.tsx +186 -0
- package/src/file-browser/components/RightPanel/RightPanelToolbar.tsx +113 -0
- package/src/file-browser/components/UploadProgress.tsx +123 -0
- package/src/file-browser/components/ViewerHost.tsx +208 -0
- package/src/file-browser/components/mobile/MobileNavigation.tsx +227 -0
- package/src/file-browser/components/navigation/NavigationButtons.tsx +171 -0
- package/src/file-browser/components/shared/ErrorBoundary.tsx +116 -0
- package/src/file-browser/components/shared/FileBrowserItem.tsx +195 -0
- package/src/file-browser/components/shared/FileIcon.tsx +169 -0
- package/src/file-browser/components/toolbar/ViewModeToggle.tsx +200 -0
- package/src/file-browser/components/views/ListView/ListView.tsx +484 -0
- package/src/file-browser/components/views/ThumbnailView/ThumbnailView.tsx +323 -0
- package/src/file-browser/components/views/TreeView/TreeNode.tsx +186 -0
- package/src/file-browser/components/views/TreeView/TreeNodeList.tsx +191 -0
- package/src/file-browser/components/views/TreeView/TreeView.tsx +200 -0
- package/src/file-browser/components/views/TreemapView/TreemapView.tsx +339 -0
- package/src/file-browser/context/FileBrowserContext.tsx +13 -0
- package/src/file-browser/examples/BasicUsage.tsx +20 -0
- package/src/file-browser/index.ts +98 -0
- package/src/file-browser/models/FileBrowserModel.ts +623 -0
- package/src/file-browser/models/LeftPanelManagerModel.ts +105 -0
- package/src/file-browser/models/NavigationManagerModel.ts +312 -0
- package/src/file-browser/models/ResponsiveLayoutManagerModel.ts +437 -0
- package/src/file-browser/models/RightPanelManagerModel.ts +190 -0
- package/src/file-browser/models/SelectionManagerModel.ts +252 -0
- package/src/file-browser/models/ToolbarManagerModel.ts +144 -0
- package/src/file-browser/models/UploadModel.ts +147 -0
- package/src/file-browser/models/ViewModeManagerModel.ts +185 -0
- package/src/file-browser/models/ViewerHostModel.ts +44 -0
- package/src/file-browser/models/ui/ListViewUIModel.ts +265 -0
- package/src/file-browser/models/ui/PreviewUIModel.ts +297 -0
- package/src/file-browser/models/ui/ThumbnailViewUIModel.ts +254 -0
- package/src/file-browser/models/ui/TreeViewUIModel.ts +128 -0
- package/src/file-browser/models/ui/TreemapViewUIModel.ts +350 -0
- package/src/file-browser/providers/FileSystemListProvider.ts +552 -0
- package/src/file-browser/providers/FileSystemProvider.ts +401 -0
- package/src/file-browser/providers/FileSystemTreeProvider.ts +231 -0
- package/src/file-browser/providers/GitProvider.ts +337 -0
- package/src/file-browser/providers/GitRepositoryProvider.ts +376 -0
- package/src/file-browser/providers/IFileBrowserProvider.ts +56 -0
- package/src/file-browser/providers/MemoryProvider.ts +303 -0
- package/src/file-browser/providers/index.ts +4 -0
- package/src/file-browser/registry/ViewerRegistry.ts +551 -0
- package/src/file-browser/registry/types.ts +144 -0
- package/src/file-browser/scripts/performanceBenchmark.ts +553 -0
- package/src/file-browser/services/ThumbnailCacheService.ts +128 -0
- package/src/file-browser/tasks.md +537 -0
- package/src/file-browser/types/FileBrowserTypes.ts +126 -0
- package/src/file-browser/types/ProviderTypes.ts +155 -0
- package/src/file-browser/types/UITypes.ts +235 -0
- package/src/file-browser/types/ViewModeTypes.ts +150 -0
- package/src/file-browser/utils/gestures.ts +327 -0
- package/src/file-browser/utils/performance.ts +563 -0
- package/src/file-browser/viewers/ImageViewer.tsx +163 -0
- package/src/file-browser/viewers/ImageViewerModel.ts +79 -0
- package/src/file-browser/viewers/TextViewer.tsx +95 -0
- package/src/file-browser/viewers/UnsupportedFileViewer.tsx +57 -0
- package/src/file-browser/viewers/index.ts +61 -0
- package/src/git/BranchList.tsx +128 -0
- package/src/git/CommitGraph.tsx +239 -0
- package/src/git/CommitList.tsx +258 -0
- package/src/git/DiffViewer.tsx +219 -0
- package/src/git/index.ts +4 -0
- package/src/icons/iconMap.ts +146 -0
- package/src/icons/index.ts +9 -0
- package/src/index.ts +13 -0
- package/src/layout/README.md +307 -0
- package/src/layout/components/ExplorerLayout/ExplorerLayout.tsx +178 -0
- package/src/layout/examples/SimpleExample.tsx +60 -0
- package/src/layout/index.ts +6 -0
- package/src/lib/utils.ts +1 -0
- package/src/list/README.md +303 -0
- package/src/list/architecture.md +807 -0
- package/src/list/components/CalculatedGridView.tsx +252 -0
- package/src/list/components/DragPreview.tsx +102 -0
- package/src/list/components/ListContextMenu.tsx +274 -0
- package/src/list/components/ListItem.tsx +761 -0
- package/src/list/components/ListItems.tsx +919 -0
- package/src/list/components/MasonryView.tsx +241 -0
- package/src/list/components/SearchFilter.tsx +44 -0
- package/src/list/components/TreemapView.tsx +709 -0
- package/src/list/components/ViewSizeControls.tsx +205 -0
- package/src/list/components/ViewTypeSelector.tsx +312 -0
- package/src/list/components/VirtualizedDetailsView.tsx +231 -0
- package/src/list/components/VirtualizedGrid.tsx +164 -0
- package/src/list/components/VirtualizedList.tsx +154 -0
- package/src/list/components/VirtualizedMasonryView.tsx +344 -0
- package/src/list/components/shared/EmptyState.tsx +103 -0
- package/src/list/components/shared/ErrorBoundary.tsx +123 -0
- package/src/list/components/shared/ErrorDisplay.tsx +100 -0
- package/src/list/components/shared/ListLoader.tsx +146 -0
- package/src/list/components/shared/LoadingIndicator.tsx +80 -0
- package/src/list/index.ts +92 -0
- package/src/list/models/ListItemsModel.ts +1301 -0
- package/src/list/models/TreemapModel.ts +204 -0
- package/src/list/providers/ListItemsProvider.ts +313 -0
- package/src/list/providers/TestListProvider.ts +604 -0
- package/src/list/tasks.md +937 -0
- package/src/list/types/ListTypes.ts +178 -0
- package/src/list/utils/BenchmarkLogger.ts +243 -0
- package/src/list/utils/DragDropManager.ts +320 -0
- package/src/list/utils/GridLayoutCalculator.ts +290 -0
- package/src/list/utils/ListAccessibility.ts +367 -0
- package/src/list/utils/ListKeyboard.ts +414 -0
- package/src/list/utils/MasonryLayoutCalculator.ts +302 -0
- package/src/list/utils/MasonryLayoutEngine.ts +401 -0
- package/src/list/utils/__tests__/MasonryLayoutEngine.test.ts +157 -0
- package/src/list/utils/__tests__/VirtualizedMasonryView.test.tsx +251 -0
- package/src/media/AlbumSidebar.tsx +48 -0
- package/src/media/MediaBrowser.tsx +92 -0
- package/src/media/MediaBrowserModel.ts +138 -0
- package/src/media/MediaGrid.tsx +50 -0
- package/src/media/MediaList.tsx +49 -0
- package/src/media/MediaPreview.tsx +63 -0
- package/src/media/MediaTimeline.tsx +38 -0
- package/src/media/MockMediaProvider.ts +70 -0
- package/src/media/index.ts +18 -0
- package/src/media/types.ts +21 -0
- package/src/styles/variables.css +60 -0
- package/src/tree/DEVELOPMENT_SUMMARY.md +170 -0
- package/src/tree/__tests__/TreeModel.test.ts +16 -0
- package/src/tree/architecture.md +530 -0
- package/src/tree/components/Tree.tsx +283 -0
- package/src/tree/components/TreeCheckbox.tsx +147 -0
- package/src/tree/components/TreeContextMenu.tsx +139 -0
- package/src/tree/components/TreeNodeList.tsx +329 -0
- package/src/tree/components/TreeTable.tsx +382 -0
- package/src/tree/index.ts +58 -0
- package/src/tree/models/TreeModel.ts +839 -0
- package/src/tree/providers/SimpleTreeProvider.ts +463 -0
- package/src/tree/providers/TestTreeProvider.ts +946 -0
- package/src/tree/providers/TreeProvider.ts +308 -0
- package/src/tree/tasks.md +2046 -0
- package/src/tree/types/TreeTypes.ts +279 -0
- package/src/tree/utils/SelectionTheme.ts +150 -0
- package/src/tree/utils/logger.ts +203 -0
- package/src/types-common.ts +21 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import type { ListItemsModel } from '../models/ListItemsModel';
|
|
4
|
+
import type { ListItemData } from '../types/ListTypes';
|
|
5
|
+
import type { MasonryItemPosition } from '../utils/MasonryLayoutEngine';
|
|
6
|
+
|
|
7
|
+
// AICODE-NOTE: Virtualized masonry view that only renders visible items for performance with large datasets
|
|
8
|
+
// Uses viewport-based culling to handle thousands of items efficiently
|
|
9
|
+
|
|
10
|
+
interface VirtualizedMasonryViewProps {
|
|
11
|
+
model: ListItemsModel;
|
|
12
|
+
items: ListItemData[];
|
|
13
|
+
containerWidth: number;
|
|
14
|
+
containerHeight: number;
|
|
15
|
+
isHorizontal?: boolean; // true for horizontal masonry, false for vertical
|
|
16
|
+
overscanCount?: number; // Extra items to render outside viewport
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface MasonryItemProps {
|
|
21
|
+
item: ListItemData;
|
|
22
|
+
position: MasonryItemPosition;
|
|
23
|
+
model: ListItemsModel;
|
|
24
|
+
onClick: (item: ListItemData) => void;
|
|
25
|
+
onDoubleClick: (item: ListItemData) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const MasonryItem: React.FC<MasonryItemProps> = observer(({ item, position, model, onClick, onDoubleClick }) => {
|
|
29
|
+
const isSelected = model.isItemSelected(item.id);
|
|
30
|
+
const isFocused = model.focusedItem === item.id;
|
|
31
|
+
const thumbnailUrl = item.thumbnailUrl ? model.resolveThumbnailUrl(item) : undefined;
|
|
32
|
+
|
|
33
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
onClick(item);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const handleDoubleClick = (e: React.MouseEvent) => {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
onDoubleClick(item);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const debugStyles = model.debugVisualization ? {
|
|
44
|
+
border: '2px solid lightcyan',
|
|
45
|
+
boxSizing: 'border-box' as const
|
|
46
|
+
} : {};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
style={{
|
|
51
|
+
position: 'absolute',
|
|
52
|
+
left: position.x,
|
|
53
|
+
top: position.y,
|
|
54
|
+
width: position.width,
|
|
55
|
+
height: position.height,
|
|
56
|
+
cursor: 'pointer',
|
|
57
|
+
backgroundColor: isSelected ? 'rgba(0, 123, 255, 0.1)' : 'transparent',
|
|
58
|
+
borderRadius: model.debugVisualization ? '0px' : '2px',
|
|
59
|
+
overflow: 'hidden',
|
|
60
|
+
transition: 'background-color 0.15s ease, box-shadow 0.15s ease',
|
|
61
|
+
boxShadow: isSelected
|
|
62
|
+
? '0 0 0 2px rgba(0, 123, 255, 0.5)'
|
|
63
|
+
: 'none',
|
|
64
|
+
...debugStyles
|
|
65
|
+
}}
|
|
66
|
+
onClick={handleClick}
|
|
67
|
+
onDoubleClick={handleDoubleClick}
|
|
68
|
+
data-item-id={item.id}
|
|
69
|
+
>
|
|
70
|
+
{thumbnailUrl ? (
|
|
71
|
+
<>
|
|
72
|
+
<img
|
|
73
|
+
src={thumbnailUrl}
|
|
74
|
+
alt={item.name}
|
|
75
|
+
style={{
|
|
76
|
+
width: '100%',
|
|
77
|
+
height: '100%',
|
|
78
|
+
objectFit: 'cover',
|
|
79
|
+
display: 'block',
|
|
80
|
+
...(model.debugVisualization ? { border: '1px solid lightblue' } : {})
|
|
81
|
+
}}
|
|
82
|
+
loading="lazy"
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
{/* Overlay label at the bottom */}
|
|
86
|
+
<div
|
|
87
|
+
style={{
|
|
88
|
+
position: 'absolute',
|
|
89
|
+
bottom: 0,
|
|
90
|
+
left: 0,
|
|
91
|
+
right: 0,
|
|
92
|
+
background: 'linear-gradient(to top, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.3), transparent)',
|
|
93
|
+
color: 'white',
|
|
94
|
+
padding: '12px 8px 8px 8px',
|
|
95
|
+
fontSize: '12px',
|
|
96
|
+
fontWeight: '500',
|
|
97
|
+
textAlign: 'left',
|
|
98
|
+
lineHeight: '1.3',
|
|
99
|
+
maxHeight: '40px',
|
|
100
|
+
overflow: 'hidden',
|
|
101
|
+
textOverflow: 'ellipsis',
|
|
102
|
+
whiteSpace: 'nowrap',
|
|
103
|
+
pointerEvents: 'none',
|
|
104
|
+
...(model.debugVisualization ? { border: '1px solid yellow' } : {})
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
{item.name}
|
|
108
|
+
</div>
|
|
109
|
+
</>
|
|
110
|
+
) : (
|
|
111
|
+
// Fallback for items without thumbnails
|
|
112
|
+
<div
|
|
113
|
+
style={{
|
|
114
|
+
width: '100%',
|
|
115
|
+
height: '100%',
|
|
116
|
+
backgroundColor: '#f0f0f0',
|
|
117
|
+
display: 'flex',
|
|
118
|
+
flexDirection: 'column',
|
|
119
|
+
alignItems: 'center',
|
|
120
|
+
justifyContent: 'center',
|
|
121
|
+
fontSize: '24px',
|
|
122
|
+
color: '#888',
|
|
123
|
+
position: 'relative',
|
|
124
|
+
...(model.debugVisualization ? { border: '1px solid orange' } : {})
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
<div>📄</div>
|
|
128
|
+
{/* Label for non-image items */}
|
|
129
|
+
<div
|
|
130
|
+
style={{
|
|
131
|
+
position: 'absolute',
|
|
132
|
+
bottom: 0,
|
|
133
|
+
left: 0,
|
|
134
|
+
right: 0,
|
|
135
|
+
background: 'rgba(0, 0, 0, 0.6)',
|
|
136
|
+
color: 'white',
|
|
137
|
+
padding: '8px',
|
|
138
|
+
fontSize: '11px',
|
|
139
|
+
fontWeight: '500',
|
|
140
|
+
textAlign: 'center',
|
|
141
|
+
lineHeight: '1.3',
|
|
142
|
+
maxHeight: '32px',
|
|
143
|
+
overflow: 'hidden',
|
|
144
|
+
textOverflow: 'ellipsis',
|
|
145
|
+
whiteSpace: 'nowrap',
|
|
146
|
+
pointerEvents: 'none'
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
{item.name}
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
export const VirtualizedMasonryView: React.FC<VirtualizedMasonryViewProps> = observer(({
|
|
158
|
+
model,
|
|
159
|
+
items,
|
|
160
|
+
containerWidth,
|
|
161
|
+
containerHeight,
|
|
162
|
+
isHorizontal = false,
|
|
163
|
+
overscanCount = 10,
|
|
164
|
+
className = ''
|
|
165
|
+
}) => {
|
|
166
|
+
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
167
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
168
|
+
|
|
169
|
+
// Update container size in model via effect (not during render)
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (containerWidth > 0 && containerHeight > 0) {
|
|
172
|
+
model.updateContainerSize(containerWidth, containerHeight);
|
|
173
|
+
}
|
|
174
|
+
}, [model, containerWidth, containerHeight]);
|
|
175
|
+
|
|
176
|
+
// Calculate layout directly (MobX observer tracks dependencies)
|
|
177
|
+
let masonryLayout: { layout: MasonryItemPosition[]; totalHeight: number } = { layout: [], totalHeight: 0 };
|
|
178
|
+
|
|
179
|
+
if (items.length > 0 && containerWidth > 0 && model.containerSize.width > 0) {
|
|
180
|
+
const result = isHorizontal
|
|
181
|
+
? model.getHorizontalMasonryLayout()
|
|
182
|
+
: model.getVerticalMasonryLayout();
|
|
183
|
+
|
|
184
|
+
if (result) {
|
|
185
|
+
masonryLayout = result;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Calculate visible items for viewport culling
|
|
190
|
+
let visibleItems: MasonryItemPosition[] = [];
|
|
191
|
+
if (masonryLayout.layout.length > 0) {
|
|
192
|
+
const overscan = overscanCount * 50;
|
|
193
|
+
visibleItems = model.getVisibleMasonryItems(scrollTop, containerHeight, isHorizontal, overscan);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// AICODE-NOTE: Handle scroll events with throttling
|
|
197
|
+
const handleScroll = useCallback(() => {
|
|
198
|
+
const container = scrollContainerRef.current;
|
|
199
|
+
if (!container) return;
|
|
200
|
+
|
|
201
|
+
const newScrollTop = container.scrollTop;
|
|
202
|
+
setScrollTop(newScrollTop);
|
|
203
|
+
|
|
204
|
+
// Update model viewport range for potential dynamic loading
|
|
205
|
+
const startIndex = Math.floor(newScrollTop / 200); // Rough estimate
|
|
206
|
+
const endIndex = Math.min(items.length - 1, startIndex + Math.ceil(containerHeight / 200) + overscanCount);
|
|
207
|
+
model.updateViewportRange(startIndex, endIndex);
|
|
208
|
+
}, [containerHeight, items.length, overscanCount, model]);
|
|
209
|
+
|
|
210
|
+
// AICODE-NOTE: Set up scroll listener with throttling
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
const container = scrollContainerRef.current;
|
|
213
|
+
if (!container) return;
|
|
214
|
+
|
|
215
|
+
let ticking = false;
|
|
216
|
+
const throttledScroll = () => {
|
|
217
|
+
if (!ticking) {
|
|
218
|
+
requestAnimationFrame(() => {
|
|
219
|
+
handleScroll();
|
|
220
|
+
ticking = false;
|
|
221
|
+
});
|
|
222
|
+
ticking = true;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
container.addEventListener('scroll', throttledScroll);
|
|
227
|
+
|
|
228
|
+
// Calculate initial visible items
|
|
229
|
+
handleScroll();
|
|
230
|
+
|
|
231
|
+
return () => {
|
|
232
|
+
container.removeEventListener('scroll', throttledScroll);
|
|
233
|
+
};
|
|
234
|
+
}, [handleScroll]);
|
|
235
|
+
|
|
236
|
+
// Scroll to item when model requests it
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
const targetId = model.scrollToItemId;
|
|
239
|
+
if (!targetId || !scrollContainerRef.current) return;
|
|
240
|
+
const el = scrollContainerRef.current.querySelector(`[data-item-id="${CSS.escape(targetId)}"]`);
|
|
241
|
+
if (el) {
|
|
242
|
+
el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
243
|
+
} else {
|
|
244
|
+
// Item not rendered yet — find position from layout and scroll to it
|
|
245
|
+
const pos = masonryLayout.layout.find(p => p.id === targetId);
|
|
246
|
+
if (pos) {
|
|
247
|
+
scrollContainerRef.current.scrollTop = Math.max(0, pos.y - containerHeight / 2);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
model.clearScrollToItem();
|
|
251
|
+
}, [model.scrollToItemId, model, masonryLayout.layout, containerHeight]);
|
|
252
|
+
|
|
253
|
+
// AICODE-NOTE: Handle item interactions
|
|
254
|
+
const handleItemClick = useCallback((item: ListItemData) => {
|
|
255
|
+
model.selectItem(item);
|
|
256
|
+
}, [model]);
|
|
257
|
+
|
|
258
|
+
const handleItemDoubleClick = useCallback((item: ListItemData) => {
|
|
259
|
+
if (model.provider.onItemDoubleClick) {
|
|
260
|
+
model.provider.onItemDoubleClick(item);
|
|
261
|
+
}
|
|
262
|
+
}, [model]);
|
|
263
|
+
|
|
264
|
+
if (items.length === 0) {
|
|
265
|
+
return (
|
|
266
|
+
<div style={{
|
|
267
|
+
width: '100%',
|
|
268
|
+
height: '100%',
|
|
269
|
+
display: 'flex',
|
|
270
|
+
alignItems: 'center',
|
|
271
|
+
justifyContent: 'center',
|
|
272
|
+
color: '#6c757d',
|
|
273
|
+
fontSize: '16px'
|
|
274
|
+
}}>
|
|
275
|
+
No items to display
|
|
276
|
+
</div>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<div
|
|
282
|
+
ref={scrollContainerRef}
|
|
283
|
+
className={`overflow-y-auto overflow-x-hidden ${className}`}
|
|
284
|
+
style={{
|
|
285
|
+
width: containerWidth,
|
|
286
|
+
height: containerHeight
|
|
287
|
+
}}
|
|
288
|
+
>
|
|
289
|
+
{/* AICODE-NOTE: Container with total height for proper scrollbar */}
|
|
290
|
+
<div
|
|
291
|
+
style={{
|
|
292
|
+
position: 'relative',
|
|
293
|
+
width: containerWidth,
|
|
294
|
+
height: Math.max(masonryLayout.totalHeight, containerHeight),
|
|
295
|
+
overflow: 'hidden'
|
|
296
|
+
}}
|
|
297
|
+
>
|
|
298
|
+
{/* AICODE-NOTE: Render only visible items */}
|
|
299
|
+
{visibleItems.map((position) => {
|
|
300
|
+
const item = items.find(i => i.id === position.id);
|
|
301
|
+
if (!item) return null;
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<MasonryItem
|
|
305
|
+
key={item.id}
|
|
306
|
+
item={item}
|
|
307
|
+
position={position}
|
|
308
|
+
model={model}
|
|
309
|
+
onClick={handleItemClick}
|
|
310
|
+
onDoubleClick={handleItemDoubleClick}
|
|
311
|
+
/>
|
|
312
|
+
);
|
|
313
|
+
})}
|
|
314
|
+
|
|
315
|
+
{/* AICODE-NOTE: Debug info when debug visualization is enabled */}
|
|
316
|
+
{model.debugVisualization && (
|
|
317
|
+
<div
|
|
318
|
+
style={{
|
|
319
|
+
position: 'fixed',
|
|
320
|
+
top: 10,
|
|
321
|
+
right: 10,
|
|
322
|
+
background: 'rgba(0, 0, 0, 0.8)',
|
|
323
|
+
color: 'white',
|
|
324
|
+
padding: '8px 12px',
|
|
325
|
+
borderRadius: '4px',
|
|
326
|
+
fontSize: '12px',
|
|
327
|
+
fontFamily: 'monospace',
|
|
328
|
+
zIndex: 1000,
|
|
329
|
+
pointerEvents: 'none'
|
|
330
|
+
}}
|
|
331
|
+
>
|
|
332
|
+
<div>Total: {masonryLayout.layout.length}</div>
|
|
333
|
+
<div>Visible: {visibleItems.length}</div>
|
|
334
|
+
<div>Scroll: {Math.round(scrollTop)}px</div>
|
|
335
|
+
<div>Height: {Math.round(masonryLayout.totalHeight)}px</div>
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
MasonryItem.displayName = 'MasonryItem';
|
|
344
|
+
VirtualizedMasonryView.displayName = 'VirtualizedMasonryView';
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import { EmptyState as UIEmptyState } from '@anymux/ui/components/empty-state';
|
|
4
|
+
|
|
5
|
+
const SIZE_MAP = { small: 'sm', medium: 'md', large: 'lg' } as const;
|
|
6
|
+
|
|
7
|
+
export interface EmptyStateProps {
|
|
8
|
+
title?: string;
|
|
9
|
+
message?: string;
|
|
10
|
+
icon?: React.ReactNode;
|
|
11
|
+
actions?: React.ReactNode;
|
|
12
|
+
className?: string;
|
|
13
|
+
size?: 'small' | 'medium' | 'large';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const EmptyStateComponent: React.FC<EmptyStateProps> = ({
|
|
17
|
+
title = 'No items found',
|
|
18
|
+
message = 'There are no items to display.',
|
|
19
|
+
icon,
|
|
20
|
+
actions,
|
|
21
|
+
className = '',
|
|
22
|
+
size = 'medium',
|
|
23
|
+
}) => {
|
|
24
|
+
return (
|
|
25
|
+
<UIEmptyState
|
|
26
|
+
preset="generic"
|
|
27
|
+
title={title}
|
|
28
|
+
description={message}
|
|
29
|
+
icon={icon}
|
|
30
|
+
size={SIZE_MAP[size]}
|
|
31
|
+
className={className}
|
|
32
|
+
>
|
|
33
|
+
{actions && <div className="mt-6">{actions}</div>}
|
|
34
|
+
</UIEmptyState>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export interface NoItemsProps {
|
|
39
|
+
onRefresh?: () => void;
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const NoItemsComponent: React.FC<NoItemsProps> = ({ onRefresh, className = '' }) => {
|
|
44
|
+
return (
|
|
45
|
+
<UIEmptyState
|
|
46
|
+
preset="empty-folder"
|
|
47
|
+
title="No items found"
|
|
48
|
+
description="This folder appears to be empty."
|
|
49
|
+
{...(onRefresh ? { action: { label: 'Refresh', onClick: onRefresh } } : {})}
|
|
50
|
+
className={className}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export interface NoSearchResultsProps {
|
|
56
|
+
searchQuery?: string;
|
|
57
|
+
onClearSearch?: () => void;
|
|
58
|
+
className?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const NoSearchResultsComponent: React.FC<NoSearchResultsProps> = ({
|
|
62
|
+
searchQuery,
|
|
63
|
+
onClearSearch,
|
|
64
|
+
className = '',
|
|
65
|
+
}) => {
|
|
66
|
+
const message = searchQuery
|
|
67
|
+
? `No items match "${searchQuery}". Try adjusting your search terms.`
|
|
68
|
+
: 'No items match your search criteria.';
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<UIEmptyState
|
|
72
|
+
preset="no-results"
|
|
73
|
+
title="No search results"
|
|
74
|
+
description={message}
|
|
75
|
+
{...(onClearSearch ? { action: { label: 'Clear Search', onClick: onClearSearch } } : {})}
|
|
76
|
+
className={className}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export interface NoSelectionProps {
|
|
82
|
+
message?: string;
|
|
83
|
+
className?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const NoSelectionComponent: React.FC<NoSelectionProps> = ({
|
|
87
|
+
message = 'Select items to see details and available actions.',
|
|
88
|
+
className = '',
|
|
89
|
+
}) => {
|
|
90
|
+
return (
|
|
91
|
+
<UIEmptyState
|
|
92
|
+
preset="no-selection"
|
|
93
|
+
description={message}
|
|
94
|
+
size="sm"
|
|
95
|
+
className={className}
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const EmptyState = observer(EmptyStateComponent);
|
|
101
|
+
export const NoItems = observer(NoItemsComponent);
|
|
102
|
+
export const NoSearchResults = observer(NoSearchResultsComponent);
|
|
103
|
+
export const NoSelection = observer(NoSelectionComponent);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
// AICODE-NOTE: Error boundary for catching and displaying React errors
|
|
4
|
+
export interface ErrorBoundaryProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
fallback?: (error: Error, errorInfo: ErrorInfo) => ReactNode;
|
|
7
|
+
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ErrorBoundaryState {
|
|
12
|
+
hasError: boolean;
|
|
13
|
+
error: Error | null;
|
|
14
|
+
errorInfo: ErrorInfo | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ListErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
18
|
+
constructor(props: ErrorBoundaryProps) {
|
|
19
|
+
super(props);
|
|
20
|
+
this.state = {
|
|
21
|
+
hasError: false,
|
|
22
|
+
error: null,
|
|
23
|
+
errorInfo: null
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
|
|
28
|
+
// AICODE-NOTE: Update state to show error UI
|
|
29
|
+
return {
|
|
30
|
+
hasError: true,
|
|
31
|
+
error
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
36
|
+
// AICODE-NOTE: Log error and update state
|
|
37
|
+
this.setState({
|
|
38
|
+
error,
|
|
39
|
+
errorInfo
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// AICODE-NOTE: Call optional error handler
|
|
43
|
+
if (this.props.onError) {
|
|
44
|
+
this.props.onError(error, errorInfo);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// AICODE-NOTE: Log to console for debugging
|
|
48
|
+
console.error('ListItemsComponent Error:', error, errorInfo);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
handleRetry = () => {
|
|
52
|
+
// AICODE-NOTE: Reset error state to retry rendering
|
|
53
|
+
this.setState({
|
|
54
|
+
hasError: false,
|
|
55
|
+
error: null,
|
|
56
|
+
errorInfo: null
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
render() {
|
|
61
|
+
if (this.state.hasError) {
|
|
62
|
+
// AICODE-NOTE: Use custom fallback if provided
|
|
63
|
+
if (this.props.fallback && this.state.error && this.state.errorInfo) {
|
|
64
|
+
return this.props.fallback(this.state.error, this.state.errorInfo);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// AICODE-NOTE: Default error UI
|
|
68
|
+
return (
|
|
69
|
+
<div className={`flex items-center justify-center p-8 ${this.props.className || ''}`}>
|
|
70
|
+
<div className="text-center space-y-4 max-w-md">
|
|
71
|
+
<div className="text-destructive">
|
|
72
|
+
<svg
|
|
73
|
+
className="w-12 h-12 mx-auto mb-4"
|
|
74
|
+
fill="none"
|
|
75
|
+
stroke="currentColor"
|
|
76
|
+
viewBox="0 0 24 24"
|
|
77
|
+
>
|
|
78
|
+
<path
|
|
79
|
+
strokeLinecap="round"
|
|
80
|
+
strokeLinejoin="round"
|
|
81
|
+
strokeWidth={2}
|
|
82
|
+
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"
|
|
83
|
+
/>
|
|
84
|
+
</svg>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div>
|
|
88
|
+
<h3 className="text-lg font-semibold text-foreground mb-2">
|
|
89
|
+
Something went wrong
|
|
90
|
+
</h3>
|
|
91
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
92
|
+
An error occurred while displaying the list items.
|
|
93
|
+
</p>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div className="space-y-2">
|
|
97
|
+
<button
|
|
98
|
+
onClick={this.handleRetry}
|
|
99
|
+
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors"
|
|
100
|
+
>
|
|
101
|
+
Try Again
|
|
102
|
+
</button>
|
|
103
|
+
|
|
104
|
+
{typeof window !== 'undefined' && this.state.error && (
|
|
105
|
+
<details className="text-left">
|
|
106
|
+
<summary className="text-xs text-muted-foreground cursor-pointer hover:text-foreground">
|
|
107
|
+
Show Error Details
|
|
108
|
+
</summary>
|
|
109
|
+
<pre className="text-xs text-destructive mt-2 p-2 bg-muted rounded overflow-auto max-h-32">
|
|
110
|
+
{this.state.error.toString()}
|
|
111
|
+
{this.state.errorInfo?.componentStack}
|
|
112
|
+
</pre>
|
|
113
|
+
</details>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return this.props.children;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import { ErrorDisplay as UIErrorDisplay } from '@anymux/ui/components/error-display';
|
|
4
|
+
|
|
5
|
+
const sizeMap = { small: 'sm', medium: 'md', large: 'lg' } as const;
|
|
6
|
+
|
|
7
|
+
export interface ErrorDisplayProps {
|
|
8
|
+
error: Error | string;
|
|
9
|
+
title?: string;
|
|
10
|
+
message?: string;
|
|
11
|
+
onRetry?: () => void;
|
|
12
|
+
onDismiss?: () => void;
|
|
13
|
+
className?: string;
|
|
14
|
+
size?: 'small' | 'medium' | 'large';
|
|
15
|
+
variant?: 'default' | 'destructive' | 'warning';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ErrorDisplayComponent: React.FC<ErrorDisplayProps> = ({
|
|
19
|
+
error,
|
|
20
|
+
title = 'Error',
|
|
21
|
+
message,
|
|
22
|
+
onRetry,
|
|
23
|
+
onDismiss,
|
|
24
|
+
className = '',
|
|
25
|
+
size = 'medium',
|
|
26
|
+
variant = 'destructive'
|
|
27
|
+
}) => {
|
|
28
|
+
const mappedSize = sizeMap[size];
|
|
29
|
+
const mappedVariant = variant === 'default' ? undefined : variant;
|
|
30
|
+
|
|
31
|
+
// If message override is provided, create a modified error preserving stack
|
|
32
|
+
let mappedError: Error | string = error;
|
|
33
|
+
if (message) {
|
|
34
|
+
if (typeof error === 'object') {
|
|
35
|
+
mappedError = Object.assign(new Error(message), { stack: error.stack });
|
|
36
|
+
} else {
|
|
37
|
+
mappedError = message;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<UIErrorDisplay
|
|
43
|
+
error={mappedError}
|
|
44
|
+
title={title}
|
|
45
|
+
size={mappedSize}
|
|
46
|
+
variant={mappedVariant}
|
|
47
|
+
showDetails={typeof error === 'object' && !!error.stack}
|
|
48
|
+
className={className}
|
|
49
|
+
{...(onRetry != null ? { onRetry } : {})}
|
|
50
|
+
{...(onDismiss != null ? { onDismiss } : {})}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export interface NetworkErrorProps {
|
|
56
|
+
onRetry?: () => void;
|
|
57
|
+
className?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const NetworkErrorComponent: React.FC<NetworkErrorProps> = ({
|
|
61
|
+
onRetry,
|
|
62
|
+
className = ''
|
|
63
|
+
}) => {
|
|
64
|
+
return (
|
|
65
|
+
<ErrorDisplayComponent
|
|
66
|
+
error="Network request failed"
|
|
67
|
+
title="Connection Error"
|
|
68
|
+
message="Unable to load items. Please check your internet connection and try again."
|
|
69
|
+
variant="warning"
|
|
70
|
+
className={className}
|
|
71
|
+
{...(onRetry != null ? { onRetry } : {})}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export interface LoadErrorProps {
|
|
77
|
+
error: Error | string;
|
|
78
|
+
onRetry?: () => void;
|
|
79
|
+
className?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const LoadErrorComponent: React.FC<LoadErrorProps> = ({
|
|
83
|
+
error,
|
|
84
|
+
onRetry,
|
|
85
|
+
className = ''
|
|
86
|
+
}) => {
|
|
87
|
+
return (
|
|
88
|
+
<ErrorDisplayComponent
|
|
89
|
+
error={error}
|
|
90
|
+
title="Failed to Load Items"
|
|
91
|
+
variant="destructive"
|
|
92
|
+
className={className}
|
|
93
|
+
{...(onRetry != null ? { onRetry } : {})}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const ErrorDisplay = observer(ErrorDisplayComponent);
|
|
99
|
+
export const NetworkError = observer(NetworkErrorComponent);
|
|
100
|
+
export const LoadError = observer(LoadErrorComponent);
|