@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,329 @@
1
+ /**
2
+ * TreeNodeList - Recursive component for rendering tree node lists
3
+ *
4
+ * This component handles the recursive rendering of tree nodes and their children,
5
+ * providing proper indentation and hierarchy visualization.
6
+ */
7
+
8
+ import React, { useState, useRef, useEffect } from 'react';
9
+ import { observer } from 'mobx-react-lite';
10
+ import { ChevronRight, ChevronDown, Loader2, MoreVertical } from 'lucide-react';
11
+ import { cn } from '../../lib/utils';
12
+ import { resolveIcon } from '../../icons/iconMap';
13
+ import { TreeCheckbox } from './TreeCheckbox';
14
+ import { getSelectionClasses, getCustomColorVariables, DEFAULT_SELECTION_THEME } from '../utils/SelectionTheme';
15
+ import type { TreeNodeData } from '../types/TreeTypes';
16
+ import type { TreeModel } from '../models/TreeModel';
17
+ import { logger } from '../utils/logger';
18
+ import { useFileBrowserContext } from '../../file-browser/context/FileBrowserContext';
19
+
20
+ // Inline rename input for tree nodes
21
+ const TreeInlineRename: React.FC<{
22
+ currentName: string;
23
+ onCommit: (newName: string) => void;
24
+ onCancel: () => void;
25
+ }> = ({ currentName, onCommit, onCancel }) => {
26
+ const [value, setValue] = useState(currentName);
27
+ const inputRef = useRef<HTMLInputElement>(null);
28
+ const mountTimeRef = useRef(Date.now());
29
+ const committedRef = useRef(false);
30
+
31
+ useEffect(() => {
32
+ mountTimeRef.current = Date.now();
33
+ const raf = requestAnimationFrame(() => {
34
+ const input = inputRef.current;
35
+ if (input) {
36
+ input.focus();
37
+ const dotIndex = currentName.lastIndexOf('.');
38
+ input.setSelectionRange(0, dotIndex > 0 ? dotIndex : currentName.length);
39
+ }
40
+ });
41
+ return () => cancelAnimationFrame(raf);
42
+ }, [currentName]);
43
+
44
+ const commit = () => {
45
+ if (committedRef.current) return;
46
+ committedRef.current = true;
47
+ const trimmed = value.trim();
48
+ if (trimmed && trimmed !== currentName) {
49
+ onCommit(trimmed);
50
+ } else {
51
+ onCancel();
52
+ }
53
+ };
54
+
55
+ const handleBlur = () => {
56
+ if (Date.now() - mountTimeRef.current < 200) return;
57
+ commit();
58
+ };
59
+
60
+ return (
61
+ <input
62
+ ref={inputRef}
63
+ value={value}
64
+ onChange={(e) => setValue(e.target.value)}
65
+ onKeyDown={(e) => {
66
+ e.stopPropagation();
67
+ if (e.key === 'Enter') { e.preventDefault(); commit(); }
68
+ if (e.key === 'Escape') { e.preventDefault(); onCancel(); }
69
+ }}
70
+ onBlur={handleBlur}
71
+ onClick={(e) => e.stopPropagation()}
72
+ onDoubleClick={(e) => e.stopPropagation()}
73
+ className="flex-1 bg-background border border-primary rounded px-1 py-0 text-[13px] outline-none min-w-0"
74
+ />
75
+ );
76
+ };
77
+
78
+ export interface TreeNodeListProps {
79
+ /** Tree nodes to render */
80
+ nodes: TreeNodeData[];
81
+ /** Tree model instance */
82
+ treeModel: TreeModel;
83
+ /** Current depth level (for indentation) */
84
+ depth?: number;
85
+ /** Additional CSS classes */
86
+ className?: string;
87
+ }
88
+
89
+ /**
90
+ * TreeNodeList component renders a list of tree nodes with proper nesting and interaction
91
+ */
92
+ export const TreeNodeList = observer<TreeNodeListProps>(({
93
+ nodes,
94
+ treeModel,
95
+ depth = 0,
96
+ className
97
+ }) => {
98
+ const fileBrowserCtx = useFileBrowserContext();
99
+
100
+ const renderNode = (node: TreeNodeData) => {
101
+ const isSelected = treeModel.isNodeSelected(node.id);
102
+ const isExpanded = treeModel.isNodeExpanded(node.id);
103
+ const isFocused = treeModel.focusedNode === node.id;
104
+ const isLoading = treeModel.isNodeLoading(node.id);
105
+ const isDirectory = node.type === 'directory';
106
+ const hasChildren = node.hasChildren || (node.children && node.children.length > 0);
107
+ const showCheckbox = treeModel.provider.useCheckboxSelection;
108
+ const [isHovered, setIsHovered] = useState(false);
109
+ const isRenaming = fileBrowserCtx?.renameState?.itemId === node.id && fileBrowserCtx?.renameState?.source === 'tree';
110
+
111
+ // Get selection theme
112
+ const selectionTheme = treeModel.provider.getSelectionTheme?.() || DEFAULT_SELECTION_THEME;
113
+
114
+ const nodeClasses = cn(
115
+ // Base styles
116
+ 'group flex items-center gap-2 py-1 px-2 text-[13px] cursor-default select-none transition-colors duration-150',
117
+ 'focus:outline-none',
118
+
119
+ // Selection styling - must come before hover so selected state is visible
120
+ isSelected
121
+ ? 'bg-accent text-accent-foreground'
122
+ : 'hover:bg-muted/50',
123
+
124
+ // Focus styles
125
+ isFocused && !isSelected && 'ring-1 ring-primary/50',
126
+ );
127
+
128
+ // Handle node click
129
+ const handleNodeClick = (event: React.MouseEvent) => {
130
+ event.stopPropagation();
131
+ logger.interaction('TreeNode clicked', node.id, { name: node.name });
132
+
133
+ if (event.ctrlKey || event.metaKey) {
134
+ // Multi-select with Ctrl/Cmd
135
+ if (treeModel.provider.isMultiSelectEnabled) {
136
+ if (isSelected) {
137
+ treeModel.deselectNode(node);
138
+ } else {
139
+ treeModel.selectNode(node);
140
+ }
141
+ }
142
+ } else {
143
+ // Single select
144
+ if (!isSelected) {
145
+ treeModel.selectNode(node);
146
+ }
147
+ }
148
+ };
149
+
150
+ // Handle expand/collapse click
151
+ const handleExpandClick = (event: React.MouseEvent) => {
152
+ event.stopPropagation();
153
+ logger.interaction('TreeNode expand/collapse clicked', node.id, { name: node.name });
154
+
155
+ if (hasChildren) {
156
+ treeModel.toggleExpansion(node);
157
+ }
158
+ };
159
+
160
+ // Handle context menu (right-click)
161
+ const handleContextMenu = (event: React.MouseEvent) => {
162
+ event.preventDefault();
163
+ event.stopPropagation();
164
+ logger.interaction('TreeNode context menu', node.id, { name: node.name });
165
+
166
+ // Don't change selection on right-click, but ensure we show menu for this node
167
+ if (!isSelected && treeModel.selectedNodesArray.length <= 1) {
168
+ treeModel.selectNode(node);
169
+ }
170
+
171
+ // Show context menu at cursor position
172
+ const rect = { x: event.clientX, y: event.clientY };
173
+
174
+ // Check if this node is part of a multi-selection
175
+ const selectedNodes = treeModel.selectedNodesArray;
176
+ if (selectedNodes.length > 1 && isSelected) {
177
+ treeModel.showMultiNodeContextMenu(selectedNodes, event.nativeEvent);
178
+ } else {
179
+ treeModel.showContextMenu(node, event.nativeEvent);
180
+ }
181
+ };
182
+
183
+ // Handle menu button click - shows context menu without changing selection
184
+ const handleMenuButtonClick = (event: React.MouseEvent) => {
185
+ event.stopPropagation();
186
+ logger.interaction('TreeNode menu button clicked', node.id, { name: node.name });
187
+
188
+ // Show context menu positioned near the button
189
+ const rect = event.currentTarget.getBoundingClientRect();
190
+ const fakeEvent = new MouseEvent('contextmenu', {
191
+ clientX: rect.right - 10,
192
+ clientY: rect.bottom,
193
+ bubbles: true,
194
+ cancelable: true
195
+ });
196
+
197
+ // If this node is part of a multi-selection, show multi-menu
198
+ const selectedNodes = treeModel.selectedNodesArray;
199
+ if (selectedNodes.length > 1 && isSelected) {
200
+ treeModel.showMultiNodeContextMenu(selectedNodes, fakeEvent);
201
+ } else {
202
+ // Show menu for this specific node without changing selection
203
+ treeModel.showContextMenu(node, fakeEvent);
204
+ }
205
+ };
206
+
207
+ const iconSize = 'w-4 h-4';
208
+
209
+ // Get custom icon from provider
210
+ const renderIcon = () => {
211
+ // Try to get custom icon from provider first
212
+ const customIcon = treeModel.provider.getNodeIcon?.(node);
213
+ if (customIcon) {
214
+ if (typeof customIcon === 'string') {
215
+ return resolveIcon(customIcon, iconSize);
216
+ } else {
217
+ const IconComponent = customIcon;
218
+ return <IconComponent className={iconSize} />;
219
+ }
220
+ }
221
+
222
+ // Default icons via shared icon map
223
+ if (isDirectory) {
224
+ return resolveIcon(isExpanded ? 'folder-open' : 'folder', iconSize);
225
+ } else {
226
+ return resolveIcon('file', iconSize);
227
+ }
228
+ };
229
+
230
+ return (
231
+ <div key={node.id} className="w-full">
232
+ {/* Node */}
233
+ <div
234
+ className={nodeClasses}
235
+ style={{ paddingLeft: `${depth * 20 + 8}px` }}
236
+ onClick={handleNodeClick}
237
+ onContextMenu={handleContextMenu}
238
+ onMouseEnter={() => setIsHovered(true)}
239
+ onMouseLeave={() => setIsHovered(false)}
240
+ tabIndex={isFocused ? 0 : -1}
241
+ role="treeitem"
242
+ aria-expanded={hasChildren ? isExpanded : undefined}
243
+ aria-selected={isSelected}
244
+ aria-level={depth + 1}
245
+ aria-label={`${node.type === 'directory' ? 'Folder' : 'File'}: ${node.name}`}
246
+ >
247
+ {/* Expand/Collapse Button */}
248
+ <div className="flex-shrink-0 w-4 h-4">
249
+ {hasChildren && (
250
+ <button
251
+ className="w-4 h-4 flex items-center justify-center hover:bg-muted rounded-sm transition-colors"
252
+ onClick={handleExpandClick}
253
+ aria-label={isExpanded ? 'Collapse' : 'Expand'}
254
+ >
255
+ {isLoading ? (
256
+ <Loader2 className="w-3 h-3 animate-spin" />
257
+ ) : isExpanded ? (
258
+ <ChevronDown className="w-3 h-3" />
259
+ ) : (
260
+ <ChevronRight className="w-3 h-3" />
261
+ )}
262
+ </button>
263
+ )}
264
+ </div>
265
+
266
+ {/* Checkbox (if enabled) */}
267
+ {showCheckbox && (
268
+ <TreeCheckbox
269
+ state={treeModel.getCheckboxState(node.id)}
270
+ onChange={(newState) => {
271
+ logger.interaction('TreeCheckbox changed', node.id, { name: node.name, newState });
272
+ treeModel.handleCheckboxChange(node, newState);
273
+ }}
274
+ nodeId={node.id}
275
+ />
276
+ )}
277
+
278
+ {/* Icon */}
279
+ <div className="flex-shrink-0 w-4 h-4">
280
+ {renderIcon()}
281
+ </div>
282
+
283
+ {/* Node Name */}
284
+ {isRenaming && fileBrowserCtx ? (
285
+ <TreeInlineRename
286
+ currentName={fileBrowserCtx.renameState!.currentName}
287
+ onCommit={fileBrowserCtx.onRenameCommit}
288
+ onCancel={fileBrowserCtx.onRenameCancel}
289
+ />
290
+ ) : (
291
+ <span className="flex-1 truncate">
292
+ {node.name}
293
+ </span>
294
+ )}
295
+
296
+ {/* Menu Button - positioned on the right */}
297
+ <button
298
+ className={cn(
299
+ 'flex-shrink-0 w-6 h-6 flex items-center justify-center rounded hover:bg-muted/70 transition-colors cursor-pointer',
300
+ // Show on hover or when node is selected
301
+ 'opacity-0 group-hover:opacity-100',
302
+ (isSelected || isHovered) && 'opacity-100'
303
+ )}
304
+ onClick={handleMenuButtonClick}
305
+ aria-label="Show context menu"
306
+ >
307
+ <MoreVertical className="w-4 h-4" />
308
+ </button>
309
+ </div>
310
+
311
+ {/* Children */}
312
+ {isExpanded && hasChildren && node.children && (
313
+ <TreeNodeList
314
+ nodes={node.children}
315
+ treeModel={treeModel}
316
+ depth={depth + 1}
317
+ className={className}
318
+ />
319
+ )}
320
+ </div>
321
+ );
322
+ };
323
+
324
+ return (
325
+ <div className={cn('w-full', className)}>
326
+ {nodes.map(renderNode)}
327
+ </div>
328
+ );
329
+ });