@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.
Files changed (244) hide show
  1. package/dist/ExplorerLayout-CSIJd7N4.js +105 -0
  2. package/dist/ExplorerLayout-CSIJd7N4.js.map +1 -0
  3. package/dist/FileBrowserContext-B6jixa2j.js +11 -0
  4. package/dist/FileBrowserContext-B6jixa2j.js.map +1 -0
  5. package/dist/calendar-DSlrbHoj.js +761 -0
  6. package/dist/calendar-DSlrbHoj.js.map +1 -0
  7. package/dist/calendar.d.ts +3 -0
  8. package/dist/calendar.js +3 -0
  9. package/dist/contacts-DQXTZzHc.js +539 -0
  10. package/dist/contacts-DQXTZzHc.js.map +1 -0
  11. package/dist/contacts.d.ts +3 -0
  12. package/dist/contacts.js +3 -0
  13. package/dist/file-browser-m5atC3kF.js +6755 -0
  14. package/dist/file-browser-m5atC3kF.js.map +1 -0
  15. package/dist/file-browser.d.ts +11 -0
  16. package/dist/file-browser.js +9 -0
  17. package/dist/git-B55e6LL-.js +561 -0
  18. package/dist/git-B55e6LL-.js.map +1 -0
  19. package/dist/git.d.ts +2 -0
  20. package/dist/git.js +3 -0
  21. package/dist/iconMap-V4B8P-Uh.js +206 -0
  22. package/dist/iconMap-V4B8P-Uh.js.map +1 -0
  23. package/dist/icons-CIsIOZXR.js +0 -0
  24. package/dist/icons.d.ts +2 -0
  25. package/dist/icons.js +4 -0
  26. package/dist/index-BNmNIWBL.d.ts +71 -0
  27. package/dist/index-BNmNIWBL.d.ts.map +1 -0
  28. package/dist/index-Bryv_GCG.d.ts +1481 -0
  29. package/dist/index-Bryv_GCG.d.ts.map +1 -0
  30. package/dist/index-CuQIjSXs.d.ts +134 -0
  31. package/dist/index-CuQIjSXs.d.ts.map +1 -0
  32. package/dist/index-DSu19mq0.d.ts +153 -0
  33. package/dist/index-DSu19mq0.d.ts.map +1 -0
  34. package/dist/index-DmsyeHFr.d.ts +149 -0
  35. package/dist/index-DmsyeHFr.d.ts.map +1 -0
  36. package/dist/index-DxnJ8FYM.d.ts +17 -0
  37. package/dist/index-DxnJ8FYM.d.ts.map +1 -0
  38. package/dist/index-DzfY1Tok.d.ts +32 -0
  39. package/dist/index-DzfY1Tok.d.ts.map +1 -0
  40. package/dist/index-Ml_SgiKa.d.ts +1847 -0
  41. package/dist/index-Ml_SgiKa.d.ts.map +1 -0
  42. package/dist/index-kHr9udZD.d.ts +1025 -0
  43. package/dist/index-kHr9udZD.d.ts.map +1 -0
  44. package/dist/index.d.ts +11 -0
  45. package/dist/index.js +15 -0
  46. package/dist/layout-Ca_4r8ka.js +89 -0
  47. package/dist/layout-Ca_4r8ka.js.map +1 -0
  48. package/dist/layout.d.ts +2 -0
  49. package/dist/layout.js +5 -0
  50. package/dist/list-CxfT6hix.js +6831 -0
  51. package/dist/list-CxfT6hix.js.map +1 -0
  52. package/dist/list.d.ts +2 -0
  53. package/dist/list.js +5 -0
  54. package/dist/media-DZ292aKK.js +557 -0
  55. package/dist/media-DZ292aKK.js.map +1 -0
  56. package/dist/media.d.ts +3 -0
  57. package/dist/media.js +3 -0
  58. package/dist/tree-Dd9Z0Aso.js +3351 -0
  59. package/dist/tree-Dd9Z0Aso.js.map +1 -0
  60. package/dist/tree.d.ts +2 -0
  61. package/dist/tree.js +6 -0
  62. package/dist/types-common-CB3kRek8.d.ts +26 -0
  63. package/dist/types-common-CB3kRek8.d.ts.map +1 -0
  64. package/dist/utils-B4fdKKsy.js +3 -0
  65. package/package.json +109 -0
  66. package/src/calendar/AgendaView.tsx +37 -0
  67. package/src/calendar/CalendarBrowser.tsx +90 -0
  68. package/src/calendar/CalendarModel.ts +142 -0
  69. package/src/calendar/CalendarSidebar.tsx +81 -0
  70. package/src/calendar/DayView.tsx +76 -0
  71. package/src/calendar/EventCard.tsx +51 -0
  72. package/src/calendar/MockCalendarProvider.ts +98 -0
  73. package/src/calendar/MonthView.tsx +77 -0
  74. package/src/calendar/WeekView.tsx +129 -0
  75. package/src/calendar/index.ts +18 -0
  76. package/src/calendar/types.ts +25 -0
  77. package/src/contacts/ContactAvatar.tsx +35 -0
  78. package/src/contacts/ContactBrowser.tsx +56 -0
  79. package/src/contacts/ContactCard.tsx +37 -0
  80. package/src/contacts/ContactDetail.tsx +63 -0
  81. package/src/contacts/ContactGroupSidebar.tsx +40 -0
  82. package/src/contacts/ContactList.tsx +32 -0
  83. package/src/contacts/ContactListModel.ts +120 -0
  84. package/src/contacts/MockContactProvider.ts +77 -0
  85. package/src/contacts/index.ts +17 -0
  86. package/src/contacts/types.ts +26 -0
  87. package/src/demos/CalendarBrowserDemo.tsx +15 -0
  88. package/src/demos/ContactBrowserDemo.tsx +15 -0
  89. package/src/demos/MediaBrowserDemo.tsx +15 -0
  90. package/src/file-browser/adapters/DocumentViewerAdapter.ts +371 -0
  91. package/src/file-browser/adapters/FileSystemBridge.ts +168 -0
  92. package/src/file-browser/adapters/GitBrowserAdapter.ts +546 -0
  93. package/src/file-browser/adapters/README.md +504 -0
  94. package/src/file-browser/adapters/index.ts +27 -0
  95. package/src/file-browser/adapters/types.ts +70 -0
  96. package/src/file-browser/architecture.md +645 -0
  97. package/src/file-browser/components/CreateItemDialog.tsx +71 -0
  98. package/src/file-browser/components/DeleteConfirmDialog.tsx +58 -0
  99. package/src/file-browser/components/FileBrowser.tsx +473 -0
  100. package/src/file-browser/components/FileBrowserContent.tsx +209 -0
  101. package/src/file-browser/components/FileBrowserHeader.tsx +151 -0
  102. package/src/file-browser/components/FileBrowserToolbar.tsx +145 -0
  103. package/src/file-browser/components/LeftPanel/LeftPanel.tsx +103 -0
  104. package/src/file-browser/components/LeftPanel/LeftPanelTabs.tsx +70 -0
  105. package/src/file-browser/components/LeftPanel/TreeNavigationView.tsx +256 -0
  106. package/src/file-browser/components/PreviewPane.tsx +146 -0
  107. package/src/file-browser/components/RightPanel/FilePreview.tsx +219 -0
  108. package/src/file-browser/components/RightPanel/RightPanel.tsx +186 -0
  109. package/src/file-browser/components/RightPanel/RightPanelToolbar.tsx +113 -0
  110. package/src/file-browser/components/UploadProgress.tsx +123 -0
  111. package/src/file-browser/components/ViewerHost.tsx +208 -0
  112. package/src/file-browser/components/mobile/MobileNavigation.tsx +227 -0
  113. package/src/file-browser/components/navigation/NavigationButtons.tsx +171 -0
  114. package/src/file-browser/components/shared/ErrorBoundary.tsx +116 -0
  115. package/src/file-browser/components/shared/FileBrowserItem.tsx +195 -0
  116. package/src/file-browser/components/shared/FileIcon.tsx +169 -0
  117. package/src/file-browser/components/toolbar/ViewModeToggle.tsx +200 -0
  118. package/src/file-browser/components/views/ListView/ListView.tsx +484 -0
  119. package/src/file-browser/components/views/ThumbnailView/ThumbnailView.tsx +323 -0
  120. package/src/file-browser/components/views/TreeView/TreeNode.tsx +186 -0
  121. package/src/file-browser/components/views/TreeView/TreeNodeList.tsx +191 -0
  122. package/src/file-browser/components/views/TreeView/TreeView.tsx +200 -0
  123. package/src/file-browser/components/views/TreemapView/TreemapView.tsx +339 -0
  124. package/src/file-browser/context/FileBrowserContext.tsx +13 -0
  125. package/src/file-browser/examples/BasicUsage.tsx +20 -0
  126. package/src/file-browser/index.ts +98 -0
  127. package/src/file-browser/models/FileBrowserModel.ts +623 -0
  128. package/src/file-browser/models/LeftPanelManagerModel.ts +105 -0
  129. package/src/file-browser/models/NavigationManagerModel.ts +312 -0
  130. package/src/file-browser/models/ResponsiveLayoutManagerModel.ts +437 -0
  131. package/src/file-browser/models/RightPanelManagerModel.ts +190 -0
  132. package/src/file-browser/models/SelectionManagerModel.ts +252 -0
  133. package/src/file-browser/models/ToolbarManagerModel.ts +144 -0
  134. package/src/file-browser/models/UploadModel.ts +147 -0
  135. package/src/file-browser/models/ViewModeManagerModel.ts +185 -0
  136. package/src/file-browser/models/ViewerHostModel.ts +44 -0
  137. package/src/file-browser/models/ui/ListViewUIModel.ts +265 -0
  138. package/src/file-browser/models/ui/PreviewUIModel.ts +297 -0
  139. package/src/file-browser/models/ui/ThumbnailViewUIModel.ts +254 -0
  140. package/src/file-browser/models/ui/TreeViewUIModel.ts +128 -0
  141. package/src/file-browser/models/ui/TreemapViewUIModel.ts +350 -0
  142. package/src/file-browser/providers/FileSystemListProvider.ts +552 -0
  143. package/src/file-browser/providers/FileSystemProvider.ts +401 -0
  144. package/src/file-browser/providers/FileSystemTreeProvider.ts +231 -0
  145. package/src/file-browser/providers/GitProvider.ts +337 -0
  146. package/src/file-browser/providers/GitRepositoryProvider.ts +376 -0
  147. package/src/file-browser/providers/IFileBrowserProvider.ts +56 -0
  148. package/src/file-browser/providers/MemoryProvider.ts +303 -0
  149. package/src/file-browser/providers/index.ts +4 -0
  150. package/src/file-browser/registry/ViewerRegistry.ts +551 -0
  151. package/src/file-browser/registry/types.ts +144 -0
  152. package/src/file-browser/scripts/performanceBenchmark.ts +553 -0
  153. package/src/file-browser/services/ThumbnailCacheService.ts +128 -0
  154. package/src/file-browser/tasks.md +537 -0
  155. package/src/file-browser/types/FileBrowserTypes.ts +126 -0
  156. package/src/file-browser/types/ProviderTypes.ts +155 -0
  157. package/src/file-browser/types/UITypes.ts +235 -0
  158. package/src/file-browser/types/ViewModeTypes.ts +150 -0
  159. package/src/file-browser/utils/gestures.ts +327 -0
  160. package/src/file-browser/utils/performance.ts +563 -0
  161. package/src/file-browser/viewers/ImageViewer.tsx +163 -0
  162. package/src/file-browser/viewers/ImageViewerModel.ts +79 -0
  163. package/src/file-browser/viewers/TextViewer.tsx +95 -0
  164. package/src/file-browser/viewers/UnsupportedFileViewer.tsx +57 -0
  165. package/src/file-browser/viewers/index.ts +61 -0
  166. package/src/git/BranchList.tsx +128 -0
  167. package/src/git/CommitGraph.tsx +239 -0
  168. package/src/git/CommitList.tsx +258 -0
  169. package/src/git/DiffViewer.tsx +219 -0
  170. package/src/git/index.ts +4 -0
  171. package/src/icons/iconMap.ts +146 -0
  172. package/src/icons/index.ts +9 -0
  173. package/src/index.ts +13 -0
  174. package/src/layout/README.md +307 -0
  175. package/src/layout/components/ExplorerLayout/ExplorerLayout.tsx +178 -0
  176. package/src/layout/examples/SimpleExample.tsx +60 -0
  177. package/src/layout/index.ts +6 -0
  178. package/src/lib/utils.ts +1 -0
  179. package/src/list/README.md +303 -0
  180. package/src/list/architecture.md +807 -0
  181. package/src/list/components/CalculatedGridView.tsx +252 -0
  182. package/src/list/components/DragPreview.tsx +102 -0
  183. package/src/list/components/ListContextMenu.tsx +274 -0
  184. package/src/list/components/ListItem.tsx +761 -0
  185. package/src/list/components/ListItems.tsx +919 -0
  186. package/src/list/components/MasonryView.tsx +241 -0
  187. package/src/list/components/SearchFilter.tsx +44 -0
  188. package/src/list/components/TreemapView.tsx +709 -0
  189. package/src/list/components/ViewSizeControls.tsx +205 -0
  190. package/src/list/components/ViewTypeSelector.tsx +312 -0
  191. package/src/list/components/VirtualizedDetailsView.tsx +231 -0
  192. package/src/list/components/VirtualizedGrid.tsx +164 -0
  193. package/src/list/components/VirtualizedList.tsx +154 -0
  194. package/src/list/components/VirtualizedMasonryView.tsx +344 -0
  195. package/src/list/components/shared/EmptyState.tsx +103 -0
  196. package/src/list/components/shared/ErrorBoundary.tsx +123 -0
  197. package/src/list/components/shared/ErrorDisplay.tsx +100 -0
  198. package/src/list/components/shared/ListLoader.tsx +146 -0
  199. package/src/list/components/shared/LoadingIndicator.tsx +80 -0
  200. package/src/list/index.ts +92 -0
  201. package/src/list/models/ListItemsModel.ts +1301 -0
  202. package/src/list/models/TreemapModel.ts +204 -0
  203. package/src/list/providers/ListItemsProvider.ts +313 -0
  204. package/src/list/providers/TestListProvider.ts +604 -0
  205. package/src/list/tasks.md +937 -0
  206. package/src/list/types/ListTypes.ts +178 -0
  207. package/src/list/utils/BenchmarkLogger.ts +243 -0
  208. package/src/list/utils/DragDropManager.ts +320 -0
  209. package/src/list/utils/GridLayoutCalculator.ts +290 -0
  210. package/src/list/utils/ListAccessibility.ts +367 -0
  211. package/src/list/utils/ListKeyboard.ts +414 -0
  212. package/src/list/utils/MasonryLayoutCalculator.ts +302 -0
  213. package/src/list/utils/MasonryLayoutEngine.ts +401 -0
  214. package/src/list/utils/__tests__/MasonryLayoutEngine.test.ts +157 -0
  215. package/src/list/utils/__tests__/VirtualizedMasonryView.test.tsx +251 -0
  216. package/src/media/AlbumSidebar.tsx +48 -0
  217. package/src/media/MediaBrowser.tsx +92 -0
  218. package/src/media/MediaBrowserModel.ts +138 -0
  219. package/src/media/MediaGrid.tsx +50 -0
  220. package/src/media/MediaList.tsx +49 -0
  221. package/src/media/MediaPreview.tsx +63 -0
  222. package/src/media/MediaTimeline.tsx +38 -0
  223. package/src/media/MockMediaProvider.ts +70 -0
  224. package/src/media/index.ts +18 -0
  225. package/src/media/types.ts +21 -0
  226. package/src/styles/variables.css +60 -0
  227. package/src/tree/DEVELOPMENT_SUMMARY.md +170 -0
  228. package/src/tree/__tests__/TreeModel.test.ts +16 -0
  229. package/src/tree/architecture.md +530 -0
  230. package/src/tree/components/Tree.tsx +283 -0
  231. package/src/tree/components/TreeCheckbox.tsx +147 -0
  232. package/src/tree/components/TreeContextMenu.tsx +139 -0
  233. package/src/tree/components/TreeNodeList.tsx +329 -0
  234. package/src/tree/components/TreeTable.tsx +382 -0
  235. package/src/tree/index.ts +58 -0
  236. package/src/tree/models/TreeModel.ts +839 -0
  237. package/src/tree/providers/SimpleTreeProvider.ts +463 -0
  238. package/src/tree/providers/TestTreeProvider.ts +946 -0
  239. package/src/tree/providers/TreeProvider.ts +308 -0
  240. package/src/tree/tasks.md +2046 -0
  241. package/src/tree/types/TreeTypes.ts +279 -0
  242. package/src/tree/utils/SelectionTheme.ts +150 -0
  243. package/src/tree/utils/logger.ts +203 -0
  244. 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;