@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,839 @@
1
+ /**
2
+ * TreeModel - MobX state management for Tree Component
3
+ *
4
+ * This class manages all tree state including nodes, selection, expansion,
5
+ * and loading states. It follows strict MobX patterns:
6
+ * - Uses makeAutoObservable(this, {})
7
+ * - Uses observable.map() for reactive collections
8
+ * - Uses composition over inheritance
9
+ * - All async operations use flow
10
+ */
11
+
12
+ import { makeAutoObservable, observable, flow } from 'mobx';
13
+ import type { TreeProvider } from '../providers/TreeProvider';
14
+ import type { TreeNodeData, TreeLoadOptions } from '../types/TreeTypes';
15
+ import type { TreeContextMenuItem } from '../types/TreeTypes';
16
+ import type { CheckboxState } from '../components/TreeCheckbox';
17
+ import { logger } from '../utils/logger';
18
+
19
+ export class TreeModel {
20
+ // Core Data - using observable collections
21
+ nodes: TreeNodeData[] = [];
22
+ nodeMap = observable.map<string, TreeNodeData>();
23
+
24
+ // Loading State
25
+ isLoading: boolean = false;
26
+ errors = observable.map<string, Error>();
27
+
28
+ // Selection State - using observable collections
29
+ selectedNodes = observable.map<string, TreeNodeData>();
30
+ focusedNode: string | null = null;
31
+
32
+ // Checkbox Selection State - for 3-state checkboxes
33
+ checkboxStates = observable.map<string, CheckboxState>();
34
+
35
+ // Expansion State - using observable collections
36
+ expandedNodes = observable.map<string, boolean>();
37
+ loadingNodes = observable.map<string, boolean>();
38
+
39
+ // Context menu state
40
+ contextMenuVisible: boolean = false;
41
+ contextMenuPosition: { x: number; y: number } = { x: 0, y: 0 };
42
+ contextMenuItems: TreeContextMenuItem[] = [];
43
+ contextMenuNodes: TreeNodeData[] = [];
44
+
45
+ constructor(public provider: TreeProvider) {
46
+ makeAutoObservable(this, {
47
+ // Most defaults are correct, empty overrides object
48
+ });
49
+ }
50
+
51
+ // =====================
52
+ // Computed Properties
53
+ // =====================
54
+
55
+ /**
56
+ * Whether the tree has any nodes loaded
57
+ */
58
+ get hasNodes(): boolean {
59
+ return this.nodes.length > 0;
60
+ }
61
+
62
+ /**
63
+ * Total count of nodes currently loaded
64
+ */
65
+ get nodeCount(): number {
66
+ return this.nodes.length;
67
+ }
68
+
69
+ /**
70
+ * Whether data has been loaded (regardless of success/failure)
71
+ */
72
+ get isLoaded(): boolean {
73
+ return !this.isLoading && (this.hasNodes || this.errors.size > 0);
74
+ }
75
+
76
+ /**
77
+ * Array of currently selected nodes (computed from selectedNodes map)
78
+ */
79
+ get selectedNodesArray(): TreeNodeData[] {
80
+ return Array.from(this.selectedNodes.values());
81
+ }
82
+
83
+ /**
84
+ * Whether any nodes are currently selected
85
+ */
86
+ get hasSelection(): boolean {
87
+ return this.selectedNodes.size > 0;
88
+ }
89
+
90
+ /**
91
+ * Check if a specific node is selected
92
+ */
93
+ isNodeSelected(nodeId: string): boolean {
94
+ return this.selectedNodes.has(nodeId);
95
+ }
96
+
97
+ /**
98
+ * Check if a specific node is expanded
99
+ */
100
+ isNodeExpanded(nodeId: string): boolean {
101
+ return this.expandedNodes.get(nodeId) ?? false;
102
+ }
103
+
104
+ /**
105
+ * Check if a specific node is loading
106
+ */
107
+ isNodeLoading(nodeId: string): boolean {
108
+ return this.loadingNodes.get(nodeId) ?? false;
109
+ }
110
+
111
+ // =====================
112
+ // Async Actions (using flow)
113
+ // =====================
114
+
115
+ /**
116
+ * Load nodes from the provider
117
+ * Uses MobX flow for proper async action handling
118
+ */
119
+ loadNodes = flow(function* (this: TreeModel, options?: TreeLoadOptions) {
120
+ this.isLoading = true;
121
+ this.errors.clear();
122
+
123
+ try {
124
+ const result = yield this.provider.loadNodes(options);
125
+
126
+ // Update nodes and nodeMap
127
+ this.nodes = result.nodes;
128
+
129
+ // Update node map for fast lookups
130
+ this.nodeMap.clear();
131
+ result.nodes.forEach((node: TreeNodeData) => this.nodeMap.set(node.id, node));
132
+
133
+ } catch (error) {
134
+ const errorKey = 'loadNodes';
135
+ this.errors.set(errorKey, error instanceof Error ? error : new Error(String(error)));
136
+ } finally {
137
+ this.isLoading = false;
138
+ }
139
+ });
140
+
141
+ // =====================
142
+ // Selection Actions (placeholder implementations)
143
+ // =====================
144
+
145
+ /**
146
+ * Select a specific node
147
+ * @param node Node to select
148
+ */
149
+ selectNode(node: TreeNodeData): void {
150
+ const previousSelection = this.selectedNodesArray.map(n => n.id);
151
+ logger.selection('selectNode called', node.id, {
152
+ previouslySelected: previousSelection,
153
+ isMultiSelect: this.provider.isMultiSelectEnabled
154
+ });
155
+
156
+ // Check if provider allows multi-selection
157
+ if (!this.provider.isMultiSelectEnabled) {
158
+ // Clear existing selection for single-select mode
159
+ logger.selection('clearing selection (single-select mode)', node.id);
160
+ this.selectedNodes.clear();
161
+ }
162
+
163
+ // Add node to selection
164
+ this.selectedNodes.set(node.id, node);
165
+ logger.selection('node added to selection', node.id, {
166
+ totalSelected: this.selectedNodes.size
167
+ });
168
+
169
+ // Set focused node
170
+ const previousFocus = this.focusedNode;
171
+ this.focusedNode = node.id;
172
+ logger.stateChange('TreeModel', 'focusedNode', previousFocus, this.focusedNode);
173
+
174
+ // Notify provider of selection change (if callback exists)
175
+ if (this.provider.onSelectionChange) {
176
+ logger.providerCall('onSelectionChange', {
177
+ selectedCount: this.selectedNodesArray.length,
178
+ selectionType: this.provider.isMultiSelectEnabled ? 'multi' : 'single'
179
+ });
180
+ this.provider.onSelectionChange({
181
+ selectedNodes: this.selectedNodesArray,
182
+ previousSelection: [], // TODO: Track previous selection in future enhancement
183
+ selectionType: this.provider.isMultiSelectEnabled ? 'multi' : 'single',
184
+ trigger: 'click'
185
+ });
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Deselect a specific node
191
+ * @param node Node to deselect
192
+ */
193
+ deselectNode(node: TreeNodeData): void {
194
+ logger.selection('deselectNode called', node.id, {
195
+ wasSelected: this.selectedNodes.has(node.id)
196
+ });
197
+
198
+ // Remove from selection if present
199
+ if (this.selectedNodes.has(node.id)) {
200
+ this.selectedNodes.delete(node.id);
201
+ logger.selection('node removed from selection', node.id, {
202
+ remainingSelected: this.selectedNodes.size
203
+ });
204
+
205
+ // Clear focus if this was the focused node
206
+ if (this.focusedNode === node.id) {
207
+ const previousFocus = this.focusedNode;
208
+ this.focusedNode = null;
209
+ logger.stateChange('TreeModel', 'focusedNode', previousFocus, this.focusedNode);
210
+ }
211
+
212
+ // Notify provider of selection change (if callback exists)
213
+ if (this.provider.onSelectionChange) {
214
+ logger.providerCall('onSelectionChange', {
215
+ selectedCount: this.selectedNodesArray.length,
216
+ selectionType: this.provider.isMultiSelectEnabled ? 'multi' : 'single'
217
+ });
218
+ this.provider.onSelectionChange({
219
+ selectedNodes: this.selectedNodesArray,
220
+ previousSelection: [], // TODO: Track previous selection in future enhancement
221
+ selectionType: this.provider.isMultiSelectEnabled ? 'multi' : 'single',
222
+ trigger: 'click'
223
+ });
224
+ }
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Clear all selected nodes
230
+ */
231
+ clearSelection(): void {
232
+ // Only proceed if there are selected nodes
233
+ if (this.selectedNodes.size > 0) {
234
+ this.selectedNodes.clear();
235
+ this.focusedNode = null;
236
+
237
+ // Notify provider of selection change (if callback exists)
238
+ if (this.provider.onSelectionChange) {
239
+ this.provider.onSelectionChange({
240
+ selectedNodes: [],
241
+ previousSelection: [], // TODO: Track previous selection in future enhancement
242
+ selectionType: this.provider.isMultiSelectEnabled ? 'multi' : 'single',
243
+ trigger: 'api'
244
+ });
245
+ }
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Select all nodes (only if multi-select is enabled)
251
+ */
252
+ selectAll(): void {
253
+ if (!this.provider.isMultiSelectEnabled) {
254
+ return; // Not allowed in single-select mode
255
+ }
256
+
257
+ // Add all nodes to selection
258
+ this.nodes.forEach(node => {
259
+ this.selectedNodes.set(node.id, node);
260
+ });
261
+
262
+ // Set focus to the first node if any exist
263
+ if (this.nodes.length > 0) {
264
+ const firstNode = this.nodes[0];
265
+ if (firstNode) {
266
+ this.focusedNode = firstNode.id;
267
+ }
268
+ }
269
+
270
+ // Notify provider of selection change (if callback exists)
271
+ if (this.provider.onSelectionChange) {
272
+ this.provider.onSelectionChange({
273
+ selectedNodes: this.selectedNodesArray,
274
+ previousSelection: [], // TODO: Track previous selection in future enhancement
275
+ selectionType: 'multi',
276
+ trigger: 'api'
277
+ });
278
+ }
279
+ }
280
+
281
+ // =====================
282
+ // Checkbox Selection Methods
283
+ // =====================
284
+
285
+ /**
286
+ * Get checkbox state for a specific node
287
+ */
288
+ getCheckboxState(nodeId: string): CheckboxState {
289
+ return this.checkboxStates.get(nodeId) ?? 'unchecked';
290
+ }
291
+
292
+ /**
293
+ * Handle checkbox click for a node
294
+ */
295
+ handleCheckboxChange(node: TreeNodeData, newState: CheckboxState): void {
296
+ logger.interaction('handleCheckboxChange called', node.id, {
297
+ newState,
298
+ useCheckboxSelection: this.provider.useCheckboxSelection,
299
+ allowPartialSelection: this.provider.allowPartialSelection,
300
+ currentCheckboxState: this.checkboxStates.get(node.id),
301
+ isCurrentlySelected: this.selectedNodes.has(node.id)
302
+ });
303
+
304
+ if (!this.provider.useCheckboxSelection) {
305
+ logger.interaction('handleCheckboxChange aborted - useCheckboxSelection is false', node.id);
306
+ return;
307
+ }
308
+
309
+ // Set the state for this node
310
+ const previousState = this.checkboxStates.get(node.id);
311
+ this.checkboxStates.set(node.id, newState);
312
+
313
+ logger.stateChange('TreeModel', 'checkboxStates', previousState, newState);
314
+
315
+ // Update selection based on checkbox state
316
+ const wasSelected = this.selectedNodes.has(node.id);
317
+ if (newState === 'checked') {
318
+ this.selectedNodes.set(node.id, node);
319
+ logger.selection('node added via checkbox', node.id, {
320
+ wasSelected,
321
+ nowSelected: true
322
+ });
323
+ } else {
324
+ this.selectedNodes.delete(node.id);
325
+ logger.selection('node removed via checkbox', node.id, {
326
+ wasSelected,
327
+ nowSelected: false
328
+ });
329
+ }
330
+
331
+ // If partial selection is allowed, update parent/child relationships
332
+ if (this.provider.allowPartialSelection) {
333
+ logger.debug('Updating parent/child checkbox relationships', {
334
+ component: 'Checkbox',
335
+ nodeId: node.id
336
+ });
337
+ this.updateChildCheckboxes(node, newState);
338
+ this.updateParentCheckboxes(node);
339
+ }
340
+
341
+ // Notify provider of selection change
342
+ if (this.provider.onSelectionChange) {
343
+ const selectionInfo = {
344
+ selectedNodes: this.selectedNodesArray,
345
+ previousSelection: [],
346
+ selectionType: 'multi' as const,
347
+ trigger: 'click' as const
348
+ };
349
+
350
+ logger.providerCall('onSelectionChange', {
351
+ selectedCount: selectionInfo.selectedNodes.length,
352
+ trigger: selectionInfo.trigger
353
+ });
354
+
355
+ this.provider.onSelectionChange(selectionInfo);
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Update all child checkboxes when parent state changes
361
+ */
362
+ private updateChildCheckboxes(node: TreeNodeData, state: CheckboxState): void {
363
+ logger.debug('updateChildCheckboxes called', {
364
+ component: 'Checkbox',
365
+ nodeId: node.id,
366
+ operation: 'update-children'
367
+ }, {
368
+ parentState: state,
369
+ hasChildren: !!node.children,
370
+ childrenCount: node.children?.length || 0
371
+ });
372
+
373
+ if (!node.children || state === 'indeterminate') {
374
+ logger.debug('updateChildCheckboxes skipped', {
375
+ component: 'Checkbox',
376
+ nodeId: node.id
377
+ }, {
378
+ reason: !node.children ? 'no-children' : 'indeterminate-state',
379
+ hasChildren: !!node.children,
380
+ state
381
+ });
382
+ return;
383
+ }
384
+
385
+ node.children.forEach(child => {
386
+ const previousState = this.checkboxStates.get(child.id);
387
+ this.checkboxStates.set(child.id, state);
388
+
389
+ logger.debug('updateChildCheckboxes - child updated', {
390
+ component: 'Checkbox',
391
+ nodeId: child.id
392
+ }, {
393
+ previousState,
394
+ newState: state,
395
+ parentNode: node.id
396
+ });
397
+
398
+ // Update selection
399
+ if (state === 'checked') {
400
+ this.selectedNodes.set(child.id, child);
401
+ logger.selection('child selected via parent', child.id, { parentNode: node.id });
402
+ } else {
403
+ this.selectedNodes.delete(child.id);
404
+ logger.selection('child deselected via parent', child.id, { parentNode: node.id });
405
+ }
406
+
407
+ // Recursively update grandchildren
408
+ this.updateChildCheckboxes(child, state);
409
+ });
410
+ }
411
+
412
+ /**
413
+ * Update parent checkbox state based on children
414
+ */
415
+ private updateParentCheckboxes(node: TreeNodeData): void {
416
+ const parent = this.findParentNode(node.id);
417
+
418
+ logger.debug('updateParentCheckboxes called', {
419
+ component: 'Checkbox',
420
+ nodeId: node.id,
421
+ operation: 'update-parent'
422
+ }, {
423
+ parentFound: !!parent,
424
+ parentId: parent?.id
425
+ });
426
+
427
+ if (!parent || !parent.children) {
428
+ logger.debug('updateParentCheckboxes skipped', {
429
+ component: 'Checkbox',
430
+ nodeId: node.id
431
+ }, {
432
+ reason: !parent ? 'no-parent' : 'parent-no-children'
433
+ });
434
+ return;
435
+ }
436
+
437
+ const childStates = parent.children.map(child => this.getCheckboxState(child.id));
438
+ const checkedCount = childStates.filter(state => state === 'checked').length;
439
+ const uncheckedCount = childStates.filter(state => state === 'unchecked').length;
440
+ const indeterminateCount = childStates.filter(state => state === 'indeterminate').length;
441
+
442
+ logger.debug('updateParentCheckboxes - analyzing children', {
443
+ component: 'Checkbox',
444
+ nodeId: parent.id
445
+ }, {
446
+ totalChildren: parent.children.length,
447
+ checkedCount,
448
+ uncheckedCount,
449
+ indeterminateCount,
450
+ childStates
451
+ });
452
+
453
+ let parentState: CheckboxState;
454
+
455
+ if (checkedCount === parent.children.length) {
456
+ // All children checked
457
+ parentState = 'checked';
458
+ } else if (checkedCount === 0 && indeterminateCount === 0) {
459
+ // No children checked or indeterminate
460
+ parentState = 'unchecked';
461
+ } else {
462
+ // Some children checked or indeterminate
463
+ parentState = 'indeterminate';
464
+ }
465
+
466
+ const previousParentState = this.checkboxStates.get(parent.id);
467
+ this.checkboxStates.set(parent.id, parentState);
468
+
469
+ logger.debug('updateParentCheckboxes - parent state calculated', {
470
+ component: 'Checkbox',
471
+ nodeId: parent.id
472
+ }, {
473
+ previousState: previousParentState,
474
+ newState: parentState,
475
+ logic: checkedCount === parent.children.length ? 'all-checked' :
476
+ (checkedCount === 0 && indeterminateCount === 0) ? 'none-checked' : 'mixed'
477
+ });
478
+
479
+ // Update selection based on parent state
480
+ if (parentState === 'checked') {
481
+ this.selectedNodes.set(parent.id, parent);
482
+ logger.selection('parent selected via children', parent.id, { newState: parentState });
483
+ } else {
484
+ this.selectedNodes.delete(parent.id);
485
+ logger.selection('parent deselected via children', parent.id, { newState: parentState });
486
+ }
487
+
488
+ // Recursively update grandparent
489
+ this.updateParentCheckboxes(parent);
490
+ }
491
+
492
+ /**
493
+ * Find parent node of a given node ID
494
+ */
495
+ private findParentNode(nodeId: string): TreeNodeData | null {
496
+ const findParent = (nodes: TreeNodeData[], targetId: string, parentNode: TreeNodeData | null = null): TreeNodeData | null => {
497
+ for (const node of nodes) {
498
+ if (node.id === targetId) {
499
+ return parentNode;
500
+ }
501
+ if (node.children && node.children.length > 0) {
502
+ const parent = findParent(node.children, targetId, node);
503
+ if (parent) return parent;
504
+ }
505
+ }
506
+ return null;
507
+ };
508
+
509
+ return findParent(this.nodes, nodeId);
510
+ }
511
+
512
+ // =====================
513
+ // Focus Management & Navigation
514
+ // =====================
515
+
516
+ /**
517
+ * Set focus to a specific node
518
+ * @param nodeId ID of node to focus
519
+ */
520
+ setFocus(nodeId: string): void {
521
+ if (this.nodeMap.has(nodeId)) {
522
+ this.focusedNode = nodeId;
523
+ }
524
+ }
525
+
526
+ /**
527
+ * Move focus to next visible node
528
+ */
529
+ focusNext(): boolean {
530
+ const flatNodes = this.getFlattenedVisibleNodes();
531
+ const currentIndex = flatNodes.findIndex(node => node.id === this.focusedNode);
532
+
533
+ if (currentIndex >= 0 && currentIndex < flatNodes.length - 1) {
534
+ const nextNode = flatNodes[currentIndex + 1];
535
+ if (nextNode) {
536
+ this.focusedNode = nextNode.id;
537
+ return true;
538
+ }
539
+ }
540
+ return false;
541
+ }
542
+
543
+ /**
544
+ * Move focus to previous visible node
545
+ */
546
+ focusPrevious(): boolean {
547
+ const flatNodes = this.getFlattenedVisibleNodes();
548
+ const currentIndex = flatNodes.findIndex(node => node.id === this.focusedNode);
549
+
550
+ if (currentIndex > 0) {
551
+ const prevNode = flatNodes[currentIndex - 1];
552
+ if (prevNode) {
553
+ this.focusedNode = prevNode.id;
554
+ return true;
555
+ }
556
+ }
557
+ return false;
558
+ }
559
+
560
+ /**
561
+ * Move focus to first visible node
562
+ */
563
+ focusFirst(): boolean {
564
+ const flatNodes = this.getFlattenedVisibleNodes();
565
+ if (flatNodes.length > 0) {
566
+ const firstNode = flatNodes[0];
567
+ if (firstNode) {
568
+ this.focusedNode = firstNode.id;
569
+ return true;
570
+ }
571
+ }
572
+ return false;
573
+ }
574
+
575
+ /**
576
+ * Move focus to last visible node
577
+ */
578
+ focusLast(): boolean {
579
+ const flatNodes = this.getFlattenedVisibleNodes();
580
+ if (flatNodes.length > 0) {
581
+ const lastNode = flatNodes[flatNodes.length - 1];
582
+ if (lastNode) {
583
+ this.focusedNode = lastNode.id;
584
+ return true;
585
+ }
586
+ }
587
+ return false;
588
+ }
589
+
590
+ /**
591
+ * Get flattened list of all currently visible nodes
592
+ * @private
593
+ */
594
+ private getFlattenedVisibleNodes(): TreeNodeData[] {
595
+ const result: TreeNodeData[] = [];
596
+
597
+ const processNodes = (nodes: TreeNodeData[]) => {
598
+ for (const node of nodes) {
599
+ result.push(node);
600
+ // Add children if node is expanded
601
+ if (this.isNodeExpanded(node.id) && node.children && node.children.length > 0) {
602
+ processNodes(node.children);
603
+ }
604
+ }
605
+ };
606
+
607
+ processNodes(this.nodes);
608
+ return result;
609
+ }
610
+
611
+ // =====================
612
+ // Expansion Actions (placeholder implementations)
613
+ // =====================
614
+
615
+ /**
616
+ * Expand a specific node
617
+ * @param node Node to expand
618
+ */
619
+ expandNode = flow(function* (this: TreeModel, node: TreeNodeData) {
620
+ logger.expansion('expandNode called', node.id, {
621
+ hasChildren: node.hasChildren,
622
+ currentlyExpanded: this.isNodeExpanded(node.id),
623
+ hasLoadedChildren: !!(node.children && node.children.length > 0)
624
+ });
625
+
626
+ // Guard: skip if node no longer exists in the tree (stale reference after rename/delete)
627
+ if (!this.nodeMap.has(node.id)) {
628
+ logger.expansion('expandNode skipped - node not in tree', node.id);
629
+ return;
630
+ }
631
+
632
+ // Set expansion state
633
+ this.expandedNodes.set(node.id, true);
634
+ logger.expansion('expansion state set to true', node.id);
635
+
636
+ // Notify provider of expansion (if callback exists)
637
+ if (this.provider.onNodeExpansion) {
638
+ logger.providerCall('onNodeExpansion', { nodeId: node.id, expanded: true });
639
+ this.provider.onNodeExpansion(node, true);
640
+ }
641
+
642
+ // Load children if they haven't been loaded yet
643
+ if (node.hasChildren && (!node.children || node.children.length === 0)) {
644
+ logger.expansion('loading children', node.id, { hasChildren: node.hasChildren });
645
+ this.loadingNodes.set(node.id, true);
646
+
647
+ try {
648
+ logger.providerCall('loadChildren', { nodeId: node.id });
649
+ const result = yield this.provider.loadChildren(node);
650
+ logger.expansion('children loaded successfully', node.id, {
651
+ childCount: result.nodes.length
652
+ });
653
+
654
+ // Update the node with children data both in nodeMap and original node
655
+ const existingNode = this.nodeMap.get(node.id);
656
+ if (existingNode) {
657
+ existingNode.children = result.nodes;
658
+
659
+ // Add children to nodeMap for fast lookups
660
+ result.nodes.forEach((child: TreeNodeData) => {
661
+ this.nodeMap.set(child.id, child);
662
+ });
663
+ }
664
+
665
+ // Also update the original node object to ensure reactivity
666
+ node.children = result.nodes;
667
+
668
+ // Update any references to this node in the main nodes array recursively
669
+ const updateNodeInTree = (nodes: TreeNodeData[]): void => {
670
+ for (const treeNode of nodes) {
671
+ if (treeNode.id === node.id) {
672
+ treeNode.children = result.nodes;
673
+ }
674
+ if (treeNode.children) {
675
+ updateNodeInTree(treeNode.children);
676
+ }
677
+ }
678
+ };
679
+
680
+ updateNodeInTree(this.nodes);
681
+
682
+ // IMPORTANT: Apply parent's checkbox state to newly loaded children
683
+ if (this.provider.allowPartialSelection && this.provider.useCheckboxSelection) {
684
+ const parentCheckboxState = this.getCheckboxState(node.id);
685
+
686
+ logger.debug('Applying parent checkbox state to newly loaded children', {
687
+ component: 'Checkbox',
688
+ nodeId: node.id,
689
+ operation: 'children-loaded-inheritance'
690
+ }, {
691
+ parentState: parentCheckboxState,
692
+ childrenCount: result.nodes.length,
693
+ childIds: result.nodes.map((child: TreeNodeData) => child.id)
694
+ });
695
+
696
+ if (parentCheckboxState === 'checked' || parentCheckboxState === 'unchecked') {
697
+ // Apply parent's state to all newly loaded children
698
+ result.nodes.forEach((child: TreeNodeData) => {
699
+ const previousChildState = this.checkboxStates.get(child.id);
700
+ this.checkboxStates.set(child.id, parentCheckboxState);
701
+
702
+ logger.debug('Child checkbox state inherited from parent', {
703
+ component: 'Checkbox',
704
+ nodeId: child.id
705
+ }, {
706
+ parentNode: node.id,
707
+ parentState: parentCheckboxState,
708
+ previousChildState,
709
+ newChildState: parentCheckboxState
710
+ });
711
+
712
+ // Update selection state accordingly
713
+ if (parentCheckboxState === 'checked') {
714
+ this.selectedNodes.set(child.id, child);
715
+ logger.selection('child auto-selected via parent inheritance', child.id, {
716
+ parentNode: node.id
717
+ });
718
+ } else {
719
+ this.selectedNodes.delete(child.id);
720
+ logger.selection('child auto-deselected via parent inheritance', child.id, {
721
+ parentNode: node.id
722
+ });
723
+ }
724
+
725
+ // Recursively apply to grandchildren if they exist
726
+ if (child.children && child.children.length > 0) {
727
+ this.updateChildCheckboxes(child, parentCheckboxState);
728
+ }
729
+ });
730
+ }
731
+ }
732
+
733
+ } catch (error) {
734
+ const errorKey = `loadChildren-${node.id}`;
735
+ this.errors.set(errorKey, error instanceof Error ? error : new Error(String(error)));
736
+
737
+ // Collapse node on error
738
+ this.expandedNodes.set(node.id, false);
739
+ } finally {
740
+ this.loadingNodes.delete(node.id);
741
+ }
742
+ }
743
+ });
744
+
745
+ /**
746
+ * Collapse a specific node
747
+ * @param node Node to collapse
748
+ */
749
+ collapseNode(node: TreeNodeData): void {
750
+ logger.expansion('collapseNode called', node.id, {
751
+ currentlyExpanded: this.isNodeExpanded(node.id)
752
+ });
753
+
754
+ // Set collapsed state
755
+ this.expandedNodes.set(node.id, false);
756
+ logger.expansion('expansion state set to false', node.id);
757
+
758
+ // Notify provider of collapse (if callback exists)
759
+ if (this.provider.onNodeExpansion) {
760
+ logger.providerCall('onNodeExpansion', { nodeId: node.id, expanded: false });
761
+ this.provider.onNodeExpansion(node, false);
762
+ }
763
+ }
764
+
765
+ /**
766
+ * Toggle expansion state of a specific node
767
+ * @param node Node to toggle
768
+ */
769
+ toggleExpansion(node: TreeNodeData): void {
770
+ if (this.isNodeExpanded(node.id)) {
771
+ this.collapseNode(node);
772
+ } else {
773
+ this.expandNode(node);
774
+ }
775
+ }
776
+
777
+ // =====================
778
+ // Context Menu Actions
779
+ // =====================
780
+
781
+ /**
782
+ * Show context menu for a specific node
783
+ * @param node Target node
784
+ * @param event Mouse event
785
+ */
786
+ showContextMenu(node: TreeNodeData, event: MouseEvent): void {
787
+ // Get context menu items from provider
788
+ const menuItems = this.provider.getNodeContextMenu?.(node) || [];
789
+
790
+ if (menuItems.length === 0) return;
791
+
792
+ // Set context menu state
793
+ this.contextMenuVisible = true;
794
+ this.contextMenuPosition = { x: event.clientX, y: event.clientY };
795
+ this.contextMenuItems = menuItems;
796
+ this.contextMenuNodes = [node];
797
+ }
798
+
799
+ /**
800
+ * Show context menu for multiple selected nodes
801
+ * @param nodes Selected nodes
802
+ * @param event Mouse event
803
+ */
804
+ showMultiNodeContextMenu(nodes: TreeNodeData[], event: MouseEvent): void {
805
+ // Get context menu items from provider
806
+ const menuItems = this.provider.getMultiNodeContextMenu?.(nodes) || [];
807
+
808
+ if (menuItems.length === 0) return;
809
+
810
+ // Set context menu state
811
+ this.contextMenuVisible = true;
812
+ this.contextMenuPosition = { x: event.clientX, y: event.clientY };
813
+ this.contextMenuItems = menuItems;
814
+ this.contextMenuNodes = nodes;
815
+ }
816
+
817
+ /**
818
+ * Hide context menu
819
+ */
820
+ hideContextMenu(): void {
821
+ this.contextMenuVisible = false;
822
+ this.contextMenuItems = [];
823
+ this.contextMenuNodes = [];
824
+ }
825
+
826
+ /**
827
+ * Handle context menu item click
828
+ * @param menuItem Clicked menu item
829
+ */
830
+ handleContextMenuAction(menuItem: TreeContextMenuItem): void {
831
+ // Call provider callback if available
832
+ if (this.provider.onContextMenuAction) {
833
+ this.provider.onContextMenuAction(menuItem.id, this.contextMenuNodes);
834
+ }
835
+
836
+ // Hide context menu
837
+ this.hideContextMenu();
838
+ }
839
+ }