@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,239 @@
1
+ import React from 'react';
2
+ import type { GitCommit } from '@anymux/file-system';
3
+
4
+ // ---- Graph Layout Types ----
5
+
6
+ export interface GraphNode {
7
+ sha: string;
8
+ column: number;
9
+ /** Paths to draw FROM this node downward to parents */
10
+ connections: GraphConnection[];
11
+ }
12
+
13
+ export interface GraphConnection {
14
+ /** Column of the parent commit */
15
+ toColumn: number;
16
+ /** Row index of the parent commit (-1 if parent is off-screen) */
17
+ toRow: number;
18
+ /** Color index for the connection line */
19
+ colorIndex: number;
20
+ }
21
+
22
+ // ---- Lane Colors ----
23
+
24
+ const LANE_COLORS = [
25
+ '#3b82f6', // blue-500
26
+ '#10b981', // emerald-500
27
+ '#f59e0b', // amber-500
28
+ '#ef4444', // red-500
29
+ '#8b5cf6', // violet-500
30
+ '#06b6d4', // cyan-500
31
+ '#ec4899', // pink-500
32
+ '#14b8a6', // teal-500
33
+ '#f97316', // orange-500
34
+ '#6366f1', // indigo-500
35
+ ];
36
+
37
+ function laneColor(index: number): string {
38
+ return LANE_COLORS[index % LANE_COLORS.length]!;
39
+ }
40
+
41
+ // ---- Graph Layout Algorithm ----
42
+
43
+ export function computeGraphLayout(commits: GitCommit[]): GraphNode[] {
44
+ const shaToRow = new Map<string, number>();
45
+ commits.forEach((c, i) => shaToRow.set(c.sha, i));
46
+
47
+ // Lanes: array of active SHAs that occupy each column
48
+ // When a commit is encountered, it takes over the lane reserved for it.
49
+ // When a commit has parents, lanes are allocated for them.
50
+ const lanes: (string | null)[] = [];
51
+ const nodes: GraphNode[] = [];
52
+
53
+ function findLane(sha: string): number {
54
+ const idx = lanes.indexOf(sha);
55
+ return idx >= 0 ? idx : -1;
56
+ }
57
+
58
+ function allocateLane(sha: string): number {
59
+ // Reuse empty lane if available
60
+ for (let i = 0; i < lanes.length; i++) {
61
+ if (lanes[i] === null) {
62
+ lanes[i] = sha;
63
+ return i;
64
+ }
65
+ }
66
+ lanes.push(sha);
67
+ return lanes.length - 1;
68
+ }
69
+
70
+ for (let row = 0; row < commits.length; row++) {
71
+ const commit = commits[row]!;
72
+ let column = findLane(commit.sha);
73
+
74
+ if (column === -1) {
75
+ // First commit or branch start — allocate new lane
76
+ column = allocateLane(commit.sha);
77
+ }
78
+
79
+ // Free this lane (will be reassigned to first parent)
80
+ lanes[column] = null;
81
+
82
+ const connections: GraphConnection[] = [];
83
+ const parents = commit.parents;
84
+
85
+ for (let pi = 0; pi < parents.length; pi++) {
86
+ const parentSha = parents[pi]!;
87
+ const parentRow = shaToRow.get(parentSha) ?? -1;
88
+ let parentLane = findLane(parentSha);
89
+
90
+ if (parentLane === -1) {
91
+ if (pi === 0) {
92
+ // First parent: reuse this commit's column for continuity
93
+ lanes[column] = parentSha;
94
+ parentLane = column;
95
+ } else {
96
+ // Merge parent: allocate a new lane
97
+ parentLane = allocateLane(parentSha);
98
+ }
99
+ }
100
+
101
+ connections.push({
102
+ toColumn: parentLane,
103
+ toRow: parentRow,
104
+ colorIndex: parentLane,
105
+ });
106
+ }
107
+
108
+ nodes.push({ sha: commit.sha, column, connections });
109
+ }
110
+
111
+ return nodes;
112
+ }
113
+
114
+ // ---- Graph Renderer ----
115
+
116
+ const ROW_HEIGHT = 40; // Must match commit row height
117
+ const COL_WIDTH = 14;
118
+ const NODE_RADIUS = 4;
119
+ const LEFT_PAD = 8;
120
+
121
+ export interface CommitGraphProps {
122
+ nodes: GraphNode[];
123
+ totalRows: number;
124
+ /** SHA of the HEAD commit — its lane will be visually highlighted */
125
+ headSha?: string;
126
+ className?: string;
127
+ }
128
+
129
+ export const CommitGraph: React.FC<CommitGraphProps> = ({ nodes, totalRows, headSha, className }) => {
130
+ if (nodes.length === 0) return null;
131
+
132
+ // Find the column of the HEAD commit for lane highlighting
133
+ const headNode = headSha ? nodes.find((n) => n.sha === headSha) : undefined;
134
+ const headColumn = headNode?.column;
135
+
136
+ const maxColumn = Math.max(...nodes.map((n) => {
137
+ const connMax = n.connections.length > 0
138
+ ? Math.max(...n.connections.map((c) => c.toColumn))
139
+ : 0;
140
+ return Math.max(n.column, connMax);
141
+ }));
142
+
143
+ const width = LEFT_PAD + (maxColumn + 1) * COL_WIDTH + NODE_RADIUS + 2;
144
+
145
+ function x(col: number): number {
146
+ return LEFT_PAD + col * COL_WIDTH + COL_WIDTH / 2;
147
+ }
148
+
149
+ function y(row: number): number {
150
+ return row * ROW_HEIGHT + ROW_HEIGHT / 2;
151
+ }
152
+
153
+ return (
154
+ <svg
155
+ className={className}
156
+ width={width}
157
+ height={totalRows * ROW_HEIGHT}
158
+ style={{ position: 'absolute', left: 0, top: 0, pointerEvents: 'none' }}
159
+ >
160
+ {/* Draw connections (lines) first so they're behind nodes */}
161
+ {nodes.map((node, row) =>
162
+ node.connections.map((conn, ci) => {
163
+ const x1 = x(node.column);
164
+ const y1 = y(row);
165
+ const x2 = x(conn.toColumn);
166
+ // If parent is off-screen, draw to bottom
167
+ const y2 = conn.toRow >= 0 ? y(conn.toRow) : totalRows * ROW_HEIGHT;
168
+ const color = laneColor(conn.colorIndex);
169
+ const isHeadLane = headColumn !== undefined && node.column === headColumn && conn.toColumn === headColumn;
170
+ const sw = isHeadLane ? 2.5 : 2;
171
+
172
+ if (x1 === x2) {
173
+ // Straight vertical line
174
+ return (
175
+ <line
176
+ key={`${node.sha}-${ci}`}
177
+ x1={x1}
178
+ y1={y1}
179
+ x2={x2}
180
+ y2={y2}
181
+ stroke={color}
182
+ strokeWidth={sw}
183
+ strokeLinecap="round"
184
+ opacity={headColumn !== undefined && !isHeadLane ? 0.5 : 1}
185
+ />
186
+ );
187
+ }
188
+
189
+ // Curved path for branch/merge
190
+ const midY = y1 + ROW_HEIGHT * 0.6;
191
+ return (
192
+ <path
193
+ key={`${node.sha}-${ci}`}
194
+ d={`M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`}
195
+ stroke={color}
196
+ strokeWidth={sw}
197
+ fill="none"
198
+ strokeLinecap="round"
199
+ opacity={headColumn !== undefined && !isHeadLane ? 0.5 : 1}
200
+ />
201
+ );
202
+ })
203
+ )}
204
+
205
+ {/* Draw active lane lines (continuation between nodes) */}
206
+
207
+ {/* Draw commit nodes on top */}
208
+ {nodes.map((node, row) => {
209
+ const isHead = node.sha === headSha;
210
+ const isOnHeadLane = headColumn !== undefined && node.column === headColumn;
211
+ const dimmed = headColumn !== undefined && !isOnHeadLane;
212
+ return (
213
+ <circle
214
+ key={node.sha}
215
+ cx={x(node.column)}
216
+ cy={y(row)}
217
+ r={isHead ? NODE_RADIUS + 1.5 : NODE_RADIUS}
218
+ fill={laneColor(node.column)}
219
+ stroke={isHead ? laneColor(node.column) : 'white'}
220
+ strokeWidth={isHead ? 2.5 : 1.5}
221
+ opacity={dimmed ? 0.5 : 1}
222
+ />
223
+ );
224
+ })}
225
+ </svg>
226
+ );
227
+ };
228
+
229
+ /** Compute the width the graph will take, for adding left padding to commit rows */
230
+ export function graphWidth(nodes: GraphNode[]): number {
231
+ if (nodes.length === 0) return 0;
232
+ const maxColumn = Math.max(...nodes.map((n) => {
233
+ const connMax = n.connections.length > 0
234
+ ? Math.max(...n.connections.map((c) => c.toColumn))
235
+ : 0;
236
+ return Math.max(n.column, connMax);
237
+ }));
238
+ return LEFT_PAD + (maxColumn + 1) * COL_WIDTH + NODE_RADIUS + 2;
239
+ }
@@ -0,0 +1,258 @@
1
+ import React, { useState, useMemo } from 'react';
2
+ import {
3
+ GitCommit as GitCommitIcon,
4
+ ChevronDown,
5
+ ChevronRight,
6
+ User,
7
+ Clock,
8
+ Hash,
9
+ FileText,
10
+ } from 'lucide-react';
11
+ import type { GitCommit } from '@anymux/file-system';
12
+ import { CommitGraph, computeGraphLayout, graphWidth } from './CommitGraph';
13
+
14
+ // ---- Helpers ----
15
+
16
+ function formatRelativeTime(date: Date): string {
17
+ const now = Date.now();
18
+ const diffMs = now - new Date(date).getTime();
19
+ const seconds = Math.floor(diffMs / 1000);
20
+ const minutes = Math.floor(seconds / 60);
21
+ const hours = Math.floor(minutes / 60);
22
+ const days = Math.floor(hours / 24);
23
+ const weeks = Math.floor(days / 7);
24
+ const months = Math.floor(days / 30);
25
+
26
+ if (seconds < 60) return 'just now';
27
+ if (minutes < 60) return `${minutes}m ago`;
28
+ if (hours < 24) return `${hours}h ago`;
29
+ if (days < 7) return `${days}d ago`;
30
+ if (weeks < 5) return `${weeks}w ago`;
31
+ if (months < 12) return `${months}mo ago`;
32
+ return new Date(date).toLocaleDateString();
33
+ }
34
+
35
+ function shortSha(sha: string): string {
36
+ return sha.slice(0, 7);
37
+ }
38
+
39
+ function firstLine(message: string): string {
40
+ return message.split('\n')[0] ?? message;
41
+ }
42
+
43
+ function authorInitials(name: string): string {
44
+ const parts = name.split(/\s+/);
45
+ if (parts.length >= 2) {
46
+ return (parts[0]![0]! + parts[parts.length - 1]![0]!).toUpperCase();
47
+ }
48
+ return name.slice(0, 2).toUpperCase();
49
+ }
50
+
51
+ /** Deterministic color from a string (for avatar backgrounds). */
52
+ function authorColor(name: string): string {
53
+ let hash = 0;
54
+ for (let i = 0; i < name.length; i++) {
55
+ hash = name.charCodeAt(i) + ((hash << 5) - hash);
56
+ }
57
+ const colors = [
58
+ 'bg-blue-500',
59
+ 'bg-emerald-500',
60
+ 'bg-violet-500',
61
+ 'bg-amber-500',
62
+ 'bg-rose-500',
63
+ 'bg-cyan-500',
64
+ 'bg-pink-500',
65
+ 'bg-teal-500',
66
+ ];
67
+ return colors[Math.abs(hash) % colors.length]!;
68
+ }
69
+
70
+ // ---- Types ----
71
+
72
+ export interface CommitListProps {
73
+ commits: GitCommit[];
74
+ headSha?: string;
75
+ /** Number of changed files per commit (keyed by sha). Optional enrichment. */
76
+ changedFileCounts?: Record<string, number>;
77
+ onSelectCommit?: (commit: GitCommit) => void;
78
+ selectedSha?: string;
79
+ /** Show commit graph (branch lanes) alongside the list */
80
+ showGraph?: boolean;
81
+ className?: string;
82
+ }
83
+
84
+ // ---- Commit Row ----
85
+
86
+ interface CommitRowProps {
87
+ commit: GitCommit;
88
+ isHead: boolean;
89
+ changedFileCount?: number;
90
+ isSelected: boolean;
91
+ onSelect: () => void;
92
+ }
93
+
94
+ function CommitRow({ commit, isHead, changedFileCount, isSelected, onSelect }: CommitRowProps) {
95
+ const [expanded, setExpanded] = useState(false);
96
+ const message = firstLine(commit.message);
97
+ const hasFullBody = commit.message.includes('\n');
98
+
99
+ return (
100
+ <div
101
+ className={`group border-b border-gray-100 dark:border-gray-800 transition-colors ${
102
+ isSelected
103
+ ? 'bg-blue-50 dark:bg-blue-900/20'
104
+ : 'hover:bg-gray-50 dark:hover:bg-gray-800/50'
105
+ }`}
106
+ >
107
+ {/* Main row */}
108
+ <button
109
+ onClick={onSelect}
110
+ className="flex items-center gap-2.5 w-full px-3 py-2 text-left"
111
+ >
112
+ {/* Expand toggle */}
113
+ <button
114
+ onClick={(e) => {
115
+ e.stopPropagation();
116
+ setExpanded(!expanded);
117
+ }}
118
+ className="w-4 h-4 flex items-center justify-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 flex-shrink-0"
119
+ >
120
+ {expanded ? (
121
+ <ChevronDown className="h-3 w-3" />
122
+ ) : (
123
+ <ChevronRight className="h-3 w-3" />
124
+ )}
125
+ </button>
126
+
127
+ {/* Author avatar */}
128
+ <div
129
+ className={`w-6 h-6 rounded-full flex items-center justify-center text-[9px] font-bold text-white flex-shrink-0 ${authorColor(commit.author.name)}`}
130
+ >
131
+ {authorInitials(commit.author.name)}
132
+ </div>
133
+
134
+ {/* Message + metadata */}
135
+ <div className="flex-1 min-w-0">
136
+ <div className="flex items-center gap-2">
137
+ <span className="text-xs font-medium truncate">
138
+ {message}
139
+ </span>
140
+ {isHead && (
141
+ <span className="px-1.5 py-0.5 text-[9px] font-bold bg-yellow-100 text-yellow-800 dark:bg-yellow-900/40 dark:text-yellow-300 rounded flex-shrink-0">
142
+ HEAD
143
+ </span>
144
+ )}
145
+ </div>
146
+ <div className="flex items-center gap-2 mt-0.5">
147
+ <span className="text-[10px] text-gray-500 dark:text-gray-400">
148
+ {commit.author.name}
149
+ </span>
150
+ <span className="text-[10px] text-gray-400 dark:text-gray-500">
151
+ {formatRelativeTime(commit.author.date)}
152
+ </span>
153
+ </div>
154
+ </div>
155
+
156
+ {/* SHA badge */}
157
+ <code className="text-[10px] font-mono text-gray-400 dark:text-gray-500 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded flex-shrink-0">
158
+ {shortSha(commit.sha)}
159
+ </code>
160
+ </button>
161
+
162
+ {/* Expanded detail */}
163
+ {expanded && (
164
+ <div className="px-3 pb-3 pl-[52px] space-y-2">
165
+ {/* Full commit message */}
166
+ {hasFullBody && (
167
+ <div className="text-xs text-gray-600 dark:text-gray-300 whitespace-pre-wrap bg-gray-50 dark:bg-gray-800/50 rounded p-2 border border-gray-100 dark:border-gray-700">
168
+ {commit.message}
169
+ </div>
170
+ )}
171
+
172
+ <div className="flex flex-wrap gap-x-4 gap-y-1 text-[10px] text-gray-500 dark:text-gray-400">
173
+ <div className="flex items-center gap-1">
174
+ <User className="h-3 w-3" />
175
+ <span>{commit.author.name}</span>
176
+ <span className="text-gray-400 dark:text-gray-500">&lt;{commit.author.email}&gt;</span>
177
+ </div>
178
+ <div className="flex items-center gap-1">
179
+ <Clock className="h-3 w-3" />
180
+ <span>{new Date(commit.author.date).toLocaleString()}</span>
181
+ </div>
182
+ <div className="flex items-center gap-1">
183
+ <Hash className="h-3 w-3" />
184
+ <code className="font-mono">{commit.sha}</code>
185
+ </div>
186
+ {commit.parents.length > 0 && (
187
+ <div className="flex items-center gap-1">
188
+ <GitCommitIcon className="h-3 w-3" />
189
+ <span>
190
+ {commit.parents.length === 1 ? 'Parent' : 'Parents'}:{' '}
191
+ {commit.parents.map((p) => shortSha(p)).join(', ')}
192
+ </span>
193
+ </div>
194
+ )}
195
+ {changedFileCount !== undefined && (
196
+ <div className="flex items-center gap-1">
197
+ <FileText className="h-3 w-3" />
198
+ <span>{changedFileCount} file{changedFileCount !== 1 ? 's' : ''} changed</span>
199
+ </div>
200
+ )}
201
+ </div>
202
+ </div>
203
+ )}
204
+ </div>
205
+ );
206
+ }
207
+
208
+ // ---- CommitList Component ----
209
+
210
+ export const CommitList: React.FC<CommitListProps> = ({
211
+ commits,
212
+ headSha,
213
+ changedFileCounts,
214
+ onSelectCommit,
215
+ selectedSha,
216
+ showGraph = true,
217
+ className,
218
+ }) => {
219
+ const graphNodes = useMemo(
220
+ () => (showGraph ? computeGraphLayout(commits) : []),
221
+ [commits, showGraph],
222
+ );
223
+ const gWidth = showGraph ? graphWidth(graphNodes) : 0;
224
+
225
+ if (commits.length === 0) {
226
+ return (
227
+ <div className={`flex items-center justify-center py-8 text-xs text-gray-400 ${className ?? ''}`}>
228
+ No commits found
229
+ </div>
230
+ );
231
+ }
232
+
233
+ return (
234
+ <div className={`overflow-auto ${className ?? ''}`}>
235
+ <div className="relative" style={{ minWidth: gWidth + 200 }}>
236
+ {showGraph && graphNodes.length > 0 && (
237
+ <CommitGraph
238
+ nodes={graphNodes}
239
+ totalRows={commits.length}
240
+ headSha={headSha}
241
+ />
242
+ )}
243
+ <div style={{ paddingLeft: gWidth }}>
244
+ {commits.map((commit) => (
245
+ <CommitRow
246
+ key={commit.sha}
247
+ commit={commit}
248
+ isHead={commit.sha === headSha}
249
+ changedFileCount={changedFileCounts?.[commit.sha]}
250
+ isSelected={commit.sha === selectedSha}
251
+ onSelect={() => onSelectCommit?.(commit)}
252
+ />
253
+ ))}
254
+ </div>
255
+ </div>
256
+ </div>
257
+ );
258
+ };