@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,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree - Main tree component that renders nodes and manages UI states
|
|
3
|
+
*
|
|
4
|
+
* This component follows the observer pattern to react to TreeModel changes.
|
|
5
|
+
* It handles loading states, errors, and renders the tree structure.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
9
|
+
import { observer } from 'mobx-react-lite';
|
|
10
|
+
import { TreeModel } from '../models/TreeModel';
|
|
11
|
+
import { TreeNodeList } from './TreeNodeList';
|
|
12
|
+
import { TreeContextMenu } from './TreeContextMenu';
|
|
13
|
+
import type { TreeProvider } from '../providers/TreeProvider';
|
|
14
|
+
import type { TreeLoadOptions, TreeNodeData } from '../types/TreeTypes';
|
|
15
|
+
|
|
16
|
+
export interface TreeProps {
|
|
17
|
+
/** Provider that supplies tree data and operations */
|
|
18
|
+
provider: TreeProvider;
|
|
19
|
+
|
|
20
|
+
/** Optional pre-created TreeModel (if provided, component won't create its own) */
|
|
21
|
+
model?: TreeModel;
|
|
22
|
+
|
|
23
|
+
/** Optional loading options to pass to the provider */
|
|
24
|
+
loadOptions?: TreeLoadOptions;
|
|
25
|
+
|
|
26
|
+
/** Additional CSS classes to apply to the tree container */
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Tree Component - Reactive tree display with loading states
|
|
32
|
+
*
|
|
33
|
+
* This is the main tree component that:
|
|
34
|
+
* - Creates and manages TreeModel
|
|
35
|
+
* - Shows loading/error states
|
|
36
|
+
* - Renders tree nodes
|
|
37
|
+
* - Follows MobX observer pattern for reactivity
|
|
38
|
+
* - Handles keyboard navigation
|
|
39
|
+
*/
|
|
40
|
+
export const Tree = observer<TreeProps>(({ provider, model, loadOptions, className }) => {
|
|
41
|
+
const [internalModel] = useState(() => model || new TreeModel(provider));
|
|
42
|
+
const treeModel = model || internalModel;
|
|
43
|
+
const treeRef = useRef<HTMLDivElement>(null);
|
|
44
|
+
|
|
45
|
+
// Load data on mount and when loadOptions change (only if using internal model)
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!model) {
|
|
48
|
+
treeModel.loadNodes(loadOptions);
|
|
49
|
+
}
|
|
50
|
+
}, [treeModel, loadOptions, model]);
|
|
51
|
+
|
|
52
|
+
// Get flattened list of all visible nodes for navigation
|
|
53
|
+
const getFlattenedNodes = (): TreeNodeData[] => {
|
|
54
|
+
const result: TreeNodeData[] = [];
|
|
55
|
+
|
|
56
|
+
const processNodes = (nodes: TreeNodeData[]) => {
|
|
57
|
+
for (const node of nodes) {
|
|
58
|
+
result.push(node);
|
|
59
|
+
// Add children if node is expanded
|
|
60
|
+
if (treeModel.isNodeExpanded(node.id) && node.children && node.children.length > 0) {
|
|
61
|
+
processNodes(node.children);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
processNodes(treeModel.nodes);
|
|
67
|
+
return result;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Navigate to node by index in flattened list
|
|
71
|
+
const navigateToNode = (targetIndex: number) => {
|
|
72
|
+
const flatNodes = getFlattenedNodes();
|
|
73
|
+
if (targetIndex >= 0 && targetIndex < flatNodes.length) {
|
|
74
|
+
const targetNode = flatNodes[targetIndex];
|
|
75
|
+
if (targetNode) {
|
|
76
|
+
treeModel.focusedNode = targetNode.id;
|
|
77
|
+
// Also select the node (following common tree behavior)
|
|
78
|
+
treeModel.selectNode(targetNode);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Find current focused node index
|
|
84
|
+
const getCurrentNodeIndex = (): number => {
|
|
85
|
+
if (!treeModel.focusedNode) return -1;
|
|
86
|
+
const flatNodes = getFlattenedNodes();
|
|
87
|
+
return flatNodes.findIndex(node => node.id === treeModel.focusedNode);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Get parent node of current focused node
|
|
91
|
+
const getParentNode = (nodeId: string): TreeNodeData | null => {
|
|
92
|
+
const findParent = (nodes: TreeNodeData[], targetId: string, parentNode: TreeNodeData | null = null): TreeNodeData | null => {
|
|
93
|
+
for (const node of nodes) {
|
|
94
|
+
if (node.id === targetId) {
|
|
95
|
+
return parentNode;
|
|
96
|
+
}
|
|
97
|
+
if (node.children && node.children.length > 0) {
|
|
98
|
+
const parent = findParent(node.children, targetId, node);
|
|
99
|
+
if (parent) return parent;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return findParent(treeModel.nodes, nodeId);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Handle keyboard shortcuts
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
111
|
+
// Only handle if tree is focused or if we have a focused node
|
|
112
|
+
if (!treeRef.current?.contains(document.activeElement) && !treeModel.focusedNode) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const currentIndex = getCurrentNodeIndex();
|
|
117
|
+
const flatNodes = getFlattenedNodes();
|
|
118
|
+
const currentNode = treeModel.focusedNode ? treeModel.nodeMap.get(treeModel.focusedNode) : null;
|
|
119
|
+
|
|
120
|
+
switch (event.key) {
|
|
121
|
+
case 'ArrowDown':
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
if (currentIndex < flatNodes.length - 1) {
|
|
124
|
+
navigateToNode(currentIndex + 1);
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
|
|
128
|
+
case 'ArrowUp':
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
if (currentIndex > 0) {
|
|
131
|
+
navigateToNode(currentIndex - 1);
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
|
|
135
|
+
case 'ArrowRight':
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
if (currentNode) {
|
|
138
|
+
if (currentNode.hasChildren || (currentNode.children && currentNode.children.length > 0)) {
|
|
139
|
+
if (!treeModel.isNodeExpanded(currentNode.id)) {
|
|
140
|
+
// Expand the node
|
|
141
|
+
treeModel.expandNode(currentNode);
|
|
142
|
+
} else if (currentNode.children && currentNode.children.length > 0) {
|
|
143
|
+
// Move to first child
|
|
144
|
+
const firstChild = currentNode.children[0];
|
|
145
|
+
if (firstChild) {
|
|
146
|
+
treeModel.focusedNode = firstChild.id;
|
|
147
|
+
treeModel.selectNode(firstChild);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
|
|
154
|
+
case 'ArrowLeft':
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
if (currentNode) {
|
|
157
|
+
if (treeModel.isNodeExpanded(currentNode.id) && (currentNode.hasChildren || (currentNode.children && currentNode.children.length > 0))) {
|
|
158
|
+
// Collapse the node
|
|
159
|
+
treeModel.collapseNode(currentNode);
|
|
160
|
+
} else {
|
|
161
|
+
// Move to parent node
|
|
162
|
+
const parentNode = getParentNode(currentNode.id);
|
|
163
|
+
if (parentNode) {
|
|
164
|
+
treeModel.focusedNode = parentNode.id;
|
|
165
|
+
treeModel.selectNode(parentNode);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
170
|
+
|
|
171
|
+
case 'Home':
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
if (flatNodes.length > 0) {
|
|
174
|
+
navigateToNode(0);
|
|
175
|
+
}
|
|
176
|
+
break;
|
|
177
|
+
|
|
178
|
+
case 'End':
|
|
179
|
+
event.preventDefault();
|
|
180
|
+
if (flatNodes.length > 0) {
|
|
181
|
+
navigateToNode(flatNodes.length - 1);
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
|
|
185
|
+
case 'Enter':
|
|
186
|
+
case ' ':
|
|
187
|
+
event.preventDefault();
|
|
188
|
+
if (currentNode) {
|
|
189
|
+
if (currentNode.hasChildren || (currentNode.children && currentNode.children.length > 0)) {
|
|
190
|
+
treeModel.toggleExpansion(currentNode);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
|
|
195
|
+
case 'a':
|
|
196
|
+
// Ctrl+A or Cmd+A to select all (when multi-select is enabled)
|
|
197
|
+
if ((event.ctrlKey || event.metaKey) && treeModel.provider.isMultiSelectEnabled) {
|
|
198
|
+
event.preventDefault();
|
|
199
|
+
treeModel.selectAll();
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
|
|
203
|
+
case 'Escape':
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
treeModel.clearSelection();
|
|
206
|
+
treeModel.hideContextMenu();
|
|
207
|
+
break;
|
|
208
|
+
|
|
209
|
+
default:
|
|
210
|
+
// Let other keys pass through
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
216
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
217
|
+
}, [treeModel]);
|
|
218
|
+
|
|
219
|
+
// Auto-focus first node if no node is focused and we have nodes
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
if (!treeModel.focusedNode && treeModel.hasNodes && treeModel.nodes.length > 0) {
|
|
222
|
+
const firstNode = treeModel.nodes[0];
|
|
223
|
+
if (firstNode) {
|
|
224
|
+
treeModel.focusedNode = firstNode.id;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}, [treeModel.hasNodes, treeModel.nodes.length, treeModel.focusedNode]);
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<div
|
|
231
|
+
ref={treeRef}
|
|
232
|
+
className={`h-full overflow-hidden focus-within:outline-none ${className || ''}`}
|
|
233
|
+
tabIndex={0}
|
|
234
|
+
role="tree"
|
|
235
|
+
aria-label="File tree"
|
|
236
|
+
>
|
|
237
|
+
{treeModel.isLoading && (
|
|
238
|
+
<div className="p-4 text-center text-muted-foreground" role="status" aria-live="polite">
|
|
239
|
+
<div className="inline-flex items-center gap-2">
|
|
240
|
+
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
|
241
|
+
<span>Loading tree...</span>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
{!treeModel.isLoading && treeModel.errors.size > 0 && (
|
|
247
|
+
<div className="p-4 text-red-600 bg-red-50 border-l-4 border-red-400" role="alert">
|
|
248
|
+
{Array.from(treeModel.errors.entries()).map(([key, error]) => (
|
|
249
|
+
<div key={key} className="flex items-start gap-2">
|
|
250
|
+
<span className="font-semibold">Error:</span>
|
|
251
|
+
<span>{error.message}</span>
|
|
252
|
+
</div>
|
|
253
|
+
))}
|
|
254
|
+
</div>
|
|
255
|
+
)}
|
|
256
|
+
|
|
257
|
+
{!treeModel.isLoading && treeModel.errors.size === 0 && !treeModel.hasNodes && (
|
|
258
|
+
<div className="p-4 text-center text-muted-foreground" role="status">
|
|
259
|
+
<span>No items to display</span>
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
|
|
263
|
+
{!treeModel.isLoading && treeModel.hasNodes && (
|
|
264
|
+
<div className="h-full overflow-auto">
|
|
265
|
+
<TreeNodeList
|
|
266
|
+
treeModel={treeModel}
|
|
267
|
+
nodes={treeModel.nodes}
|
|
268
|
+
depth={0}
|
|
269
|
+
/>
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
|
|
273
|
+
{/* Context Menu */}
|
|
274
|
+
<TreeContextMenu
|
|
275
|
+
items={treeModel.contextMenuItems}
|
|
276
|
+
position={treeModel.contextMenuPosition}
|
|
277
|
+
visible={treeModel.contextMenuVisible}
|
|
278
|
+
onClose={() => treeModel.hideContextMenu()}
|
|
279
|
+
onItemClick={(menuItem) => treeModel.handleContextMenuAction(menuItem)}
|
|
280
|
+
/>
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TreeCheckbox - 3-state checkbox component for tree selection
|
|
3
|
+
*
|
|
4
|
+
* Supports three states:
|
|
5
|
+
* - unchecked (false): No children selected
|
|
6
|
+
* - checked (true): All children selected
|
|
7
|
+
* - indeterminate (mixed): Some children selected
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { observer } from 'mobx-react-lite';
|
|
12
|
+
import { Check, Minus } from 'lucide-react';
|
|
13
|
+
import { cn } from '../../lib/utils';
|
|
14
|
+
import { logger } from '../utils/logger';
|
|
15
|
+
|
|
16
|
+
export type CheckboxState = 'unchecked' | 'checked' | 'indeterminate';
|
|
17
|
+
|
|
18
|
+
export interface TreeCheckboxProps {
|
|
19
|
+
/** Current checkbox state */
|
|
20
|
+
state: CheckboxState;
|
|
21
|
+
|
|
22
|
+
/** Callback when checkbox is clicked */
|
|
23
|
+
onChange: (newState: CheckboxState) => void;
|
|
24
|
+
|
|
25
|
+
/** Whether the checkbox is disabled */
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
|
|
28
|
+
/** Additional CSS classes */
|
|
29
|
+
className?: string;
|
|
30
|
+
|
|
31
|
+
/** ARIA label for accessibility */
|
|
32
|
+
ariaLabel?: string;
|
|
33
|
+
|
|
34
|
+
/** Size of the checkbox */
|
|
35
|
+
size?: 'sm' | 'md' | 'lg';
|
|
36
|
+
|
|
37
|
+
/** Node ID for logging purposes */
|
|
38
|
+
nodeId?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* TreeCheckbox Component - 3-state checkbox for tree selection
|
|
43
|
+
*/
|
|
44
|
+
export const TreeCheckbox = observer<TreeCheckboxProps>(({
|
|
45
|
+
state,
|
|
46
|
+
onChange,
|
|
47
|
+
disabled = false,
|
|
48
|
+
className,
|
|
49
|
+
ariaLabel,
|
|
50
|
+
size = 'md',
|
|
51
|
+
nodeId = 'unknown'
|
|
52
|
+
}) => {
|
|
53
|
+
logger.rendering('TreeCheckbox', nodeId, {
|
|
54
|
+
state,
|
|
55
|
+
disabled,
|
|
56
|
+
size,
|
|
57
|
+
hasOnChange: !!onChange
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const handleClick = (event: React.MouseEvent) => {
|
|
61
|
+
event.stopPropagation(); // Prevent node selection
|
|
62
|
+
|
|
63
|
+
logger.interaction('TreeCheckbox click', nodeId, {
|
|
64
|
+
currentState: state,
|
|
65
|
+
disabled,
|
|
66
|
+
eventType: 'click'
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (disabled) {
|
|
70
|
+
logger.interaction('TreeCheckbox click ignored (disabled)', nodeId);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Toggle logic: unchecked -> checked -> unchecked
|
|
75
|
+
// Indeterminate state can only be set programmatically
|
|
76
|
+
const newState = state === 'checked' ? 'unchecked' : 'checked';
|
|
77
|
+
|
|
78
|
+
logger.interaction('TreeCheckbox state change', nodeId, {
|
|
79
|
+
from: state,
|
|
80
|
+
to: newState,
|
|
81
|
+
trigger: 'click'
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
onChange(newState);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
88
|
+
if (event.key === ' ' || event.key === 'Enter') {
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
|
|
91
|
+
logger.interaction('TreeCheckbox keydown', nodeId, {
|
|
92
|
+
key: event.key,
|
|
93
|
+
currentState: state
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
handleClick(event as any);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const sizeClasses = {
|
|
101
|
+
sm: 'w-3 h-3',
|
|
102
|
+
md: 'w-4 h-4',
|
|
103
|
+
lg: 'w-5 h-5'
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const iconSizeClasses = {
|
|
107
|
+
sm: 'w-2 h-2',
|
|
108
|
+
md: 'w-3 h-3',
|
|
109
|
+
lg: 'w-4 h-4'
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const computedClasses = cn(
|
|
113
|
+
'flex items-center justify-center rounded border cursor-pointer transition-all',
|
|
114
|
+
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-1',
|
|
115
|
+
sizeClasses[size],
|
|
116
|
+
// Base styles
|
|
117
|
+
'border-border bg-background',
|
|
118
|
+
// State-specific styles
|
|
119
|
+
state === 'checked' && 'bg-primary border-primary text-primary-foreground',
|
|
120
|
+
state === 'indeterminate' && 'bg-primary border-primary text-primary-foreground',
|
|
121
|
+
state === 'unchecked' && 'hover:border-primary/50',
|
|
122
|
+
// Disabled styles
|
|
123
|
+
disabled && 'opacity-50 cursor-not-allowed pointer-events-none',
|
|
124
|
+
className
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
logger.cssClasses(`TreeCheckbox-${nodeId}`, computedClasses, state === 'checked', false);
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div
|
|
131
|
+
role="checkbox"
|
|
132
|
+
aria-checked={state === 'indeterminate' ? 'mixed' : state === 'checked'}
|
|
133
|
+
aria-label={ariaLabel}
|
|
134
|
+
className={computedClasses}
|
|
135
|
+
onClick={handleClick}
|
|
136
|
+
onKeyDown={handleKeyDown}
|
|
137
|
+
tabIndex={disabled ? -1 : 0}
|
|
138
|
+
>
|
|
139
|
+
{state === 'checked' && (
|
|
140
|
+
<Check className={cn(iconSizeClasses[size], 'stroke-[3]')} />
|
|
141
|
+
)}
|
|
142
|
+
{state === 'indeterminate' && (
|
|
143
|
+
<Minus className={cn(iconSizeClasses[size], 'stroke-[3]')} />
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import type { TreeContextMenuItem } from '../types/TreeTypes';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Props for the TreeContextMenu component
|
|
7
|
+
*/
|
|
8
|
+
export interface TreeContextMenuProps {
|
|
9
|
+
/**
|
|
10
|
+
* Menu items to display
|
|
11
|
+
*/
|
|
12
|
+
items: TreeContextMenuItem[];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Position of the context menu
|
|
16
|
+
*/
|
|
17
|
+
position: { x: number; y: number };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether the menu is visible
|
|
21
|
+
*/
|
|
22
|
+
visible: boolean;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Callback when menu is closed
|
|
26
|
+
*/
|
|
27
|
+
onClose: () => void;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Callback when menu item is clicked
|
|
31
|
+
*/
|
|
32
|
+
onItemClick: (menuItem: TreeContextMenuItem) => void;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Additional CSS classes
|
|
36
|
+
*/
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* TreeContextMenu - Context menu component for tree nodes
|
|
42
|
+
*
|
|
43
|
+
* Features:
|
|
44
|
+
* - Renders context menu items with proper styling (no icons)
|
|
45
|
+
* - Handles click outside to close
|
|
46
|
+
* - Supports separators and disabled items
|
|
47
|
+
* - Positioned absolutely at cursor position
|
|
48
|
+
* - Keyboard navigation support
|
|
49
|
+
*/
|
|
50
|
+
export const TreeContextMenu = observer<TreeContextMenuProps>(({
|
|
51
|
+
items,
|
|
52
|
+
position,
|
|
53
|
+
visible,
|
|
54
|
+
onClose,
|
|
55
|
+
onItemClick,
|
|
56
|
+
className = ''
|
|
57
|
+
}) => {
|
|
58
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
59
|
+
|
|
60
|
+
// Handle click outside to close menu
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!visible) return;
|
|
63
|
+
|
|
64
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
65
|
+
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
|
66
|
+
onClose();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
71
|
+
if (event.key === 'Escape') {
|
|
72
|
+
onClose();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
77
|
+
document.addEventListener('keydown', handleEscape);
|
|
78
|
+
|
|
79
|
+
return () => {
|
|
80
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
81
|
+
document.removeEventListener('keydown', handleEscape);
|
|
82
|
+
};
|
|
83
|
+
}, [visible, onClose]);
|
|
84
|
+
|
|
85
|
+
const handleItemClick = (item: TreeContextMenuItem, event: React.MouseEvent) => {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
event.stopPropagation();
|
|
88
|
+
|
|
89
|
+
if (item.disabled) return;
|
|
90
|
+
|
|
91
|
+
onItemClick(item);
|
|
92
|
+
onClose();
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (!visible) return null;
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div
|
|
99
|
+
ref={menuRef}
|
|
100
|
+
className={`
|
|
101
|
+
bg-popover text-popover-foreground rounded-md border p-1 shadow-md min-w-[160px]
|
|
102
|
+
fixed z-[100]
|
|
103
|
+
${className}
|
|
104
|
+
`}
|
|
105
|
+
style={{
|
|
106
|
+
left: position.x,
|
|
107
|
+
top: position.y,
|
|
108
|
+
}}
|
|
109
|
+
role="menu"
|
|
110
|
+
aria-label="Context menu"
|
|
111
|
+
>
|
|
112
|
+
{items.map((item, index) => (
|
|
113
|
+
<React.Fragment key={item.id}>
|
|
114
|
+
<div
|
|
115
|
+
className={`
|
|
116
|
+
px-2 py-1.5 text-sm cursor-default hover:bg-accent hover:text-accent-foreground
|
|
117
|
+
flex items-center rounded-sm transition-colors duration-150
|
|
118
|
+
${item.disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
|
119
|
+
`}
|
|
120
|
+
onClick={(e) => handleItemClick(item, e)}
|
|
121
|
+
role="menuitem"
|
|
122
|
+
tabIndex={item.disabled ? -1 : 0}
|
|
123
|
+
aria-disabled={item.disabled}
|
|
124
|
+
>
|
|
125
|
+
<span className="flex-1">{item.label}</span>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{item.separator && index < items.length - 1 && (
|
|
129
|
+
<div className="h-px bg-border my-1" role="separator" />
|
|
130
|
+
)}
|
|
131
|
+
</React.Fragment>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
TreeContextMenu.displayName = 'TreeContextMenu';
|
|
138
|
+
|
|
139
|
+
export default TreeContextMenu;
|