@anymux/ui-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ExplorerLayout-CSIJd7N4.js +105 -0
- package/dist/ExplorerLayout-CSIJd7N4.js.map +1 -0
- package/dist/FileBrowserContext-B6jixa2j.js +11 -0
- package/dist/FileBrowserContext-B6jixa2j.js.map +1 -0
- package/dist/calendar-DSlrbHoj.js +761 -0
- package/dist/calendar-DSlrbHoj.js.map +1 -0
- package/dist/calendar.d.ts +3 -0
- package/dist/calendar.js +3 -0
- package/dist/contacts-DQXTZzHc.js +539 -0
- package/dist/contacts-DQXTZzHc.js.map +1 -0
- package/dist/contacts.d.ts +3 -0
- package/dist/contacts.js +3 -0
- package/dist/file-browser-m5atC3kF.js +6755 -0
- package/dist/file-browser-m5atC3kF.js.map +1 -0
- package/dist/file-browser.d.ts +11 -0
- package/dist/file-browser.js +9 -0
- package/dist/git-B55e6LL-.js +561 -0
- package/dist/git-B55e6LL-.js.map +1 -0
- package/dist/git.d.ts +2 -0
- package/dist/git.js +3 -0
- package/dist/iconMap-V4B8P-Uh.js +206 -0
- package/dist/iconMap-V4B8P-Uh.js.map +1 -0
- package/dist/icons-CIsIOZXR.js +0 -0
- package/dist/icons.d.ts +2 -0
- package/dist/icons.js +4 -0
- package/dist/index-BNmNIWBL.d.ts +71 -0
- package/dist/index-BNmNIWBL.d.ts.map +1 -0
- package/dist/index-Bryv_GCG.d.ts +1481 -0
- package/dist/index-Bryv_GCG.d.ts.map +1 -0
- package/dist/index-CuQIjSXs.d.ts +134 -0
- package/dist/index-CuQIjSXs.d.ts.map +1 -0
- package/dist/index-DSu19mq0.d.ts +153 -0
- package/dist/index-DSu19mq0.d.ts.map +1 -0
- package/dist/index-DmsyeHFr.d.ts +149 -0
- package/dist/index-DmsyeHFr.d.ts.map +1 -0
- package/dist/index-DxnJ8FYM.d.ts +17 -0
- package/dist/index-DxnJ8FYM.d.ts.map +1 -0
- package/dist/index-DzfY1Tok.d.ts +32 -0
- package/dist/index-DzfY1Tok.d.ts.map +1 -0
- package/dist/index-Ml_SgiKa.d.ts +1847 -0
- package/dist/index-Ml_SgiKa.d.ts.map +1 -0
- package/dist/index-kHr9udZD.d.ts +1025 -0
- package/dist/index-kHr9udZD.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +15 -0
- package/dist/layout-Ca_4r8ka.js +89 -0
- package/dist/layout-Ca_4r8ka.js.map +1 -0
- package/dist/layout.d.ts +2 -0
- package/dist/layout.js +5 -0
- package/dist/list-CxfT6hix.js +6831 -0
- package/dist/list-CxfT6hix.js.map +1 -0
- package/dist/list.d.ts +2 -0
- package/dist/list.js +5 -0
- package/dist/media-DZ292aKK.js +557 -0
- package/dist/media-DZ292aKK.js.map +1 -0
- package/dist/media.d.ts +3 -0
- package/dist/media.js +3 -0
- package/dist/tree-Dd9Z0Aso.js +3351 -0
- package/dist/tree-Dd9Z0Aso.js.map +1 -0
- package/dist/tree.d.ts +2 -0
- package/dist/tree.js +6 -0
- package/dist/types-common-CB3kRek8.d.ts +26 -0
- package/dist/types-common-CB3kRek8.d.ts.map +1 -0
- package/dist/utils-B4fdKKsy.js +3 -0
- package/package.json +109 -0
- package/src/calendar/AgendaView.tsx +37 -0
- package/src/calendar/CalendarBrowser.tsx +90 -0
- package/src/calendar/CalendarModel.ts +142 -0
- package/src/calendar/CalendarSidebar.tsx +81 -0
- package/src/calendar/DayView.tsx +76 -0
- package/src/calendar/EventCard.tsx +51 -0
- package/src/calendar/MockCalendarProvider.ts +98 -0
- package/src/calendar/MonthView.tsx +77 -0
- package/src/calendar/WeekView.tsx +129 -0
- package/src/calendar/index.ts +18 -0
- package/src/calendar/types.ts +25 -0
- package/src/contacts/ContactAvatar.tsx +35 -0
- package/src/contacts/ContactBrowser.tsx +56 -0
- package/src/contacts/ContactCard.tsx +37 -0
- package/src/contacts/ContactDetail.tsx +63 -0
- package/src/contacts/ContactGroupSidebar.tsx +40 -0
- package/src/contacts/ContactList.tsx +32 -0
- package/src/contacts/ContactListModel.ts +120 -0
- package/src/contacts/MockContactProvider.ts +77 -0
- package/src/contacts/index.ts +17 -0
- package/src/contacts/types.ts +26 -0
- package/src/demos/CalendarBrowserDemo.tsx +15 -0
- package/src/demos/ContactBrowserDemo.tsx +15 -0
- package/src/demos/MediaBrowserDemo.tsx +15 -0
- package/src/file-browser/adapters/DocumentViewerAdapter.ts +371 -0
- package/src/file-browser/adapters/FileSystemBridge.ts +168 -0
- package/src/file-browser/adapters/GitBrowserAdapter.ts +546 -0
- package/src/file-browser/adapters/README.md +504 -0
- package/src/file-browser/adapters/index.ts +27 -0
- package/src/file-browser/adapters/types.ts +70 -0
- package/src/file-browser/architecture.md +645 -0
- package/src/file-browser/components/CreateItemDialog.tsx +71 -0
- package/src/file-browser/components/DeleteConfirmDialog.tsx +58 -0
- package/src/file-browser/components/FileBrowser.tsx +473 -0
- package/src/file-browser/components/FileBrowserContent.tsx +209 -0
- package/src/file-browser/components/FileBrowserHeader.tsx +151 -0
- package/src/file-browser/components/FileBrowserToolbar.tsx +145 -0
- package/src/file-browser/components/LeftPanel/LeftPanel.tsx +103 -0
- package/src/file-browser/components/LeftPanel/LeftPanelTabs.tsx +70 -0
- package/src/file-browser/components/LeftPanel/TreeNavigationView.tsx +256 -0
- package/src/file-browser/components/PreviewPane.tsx +146 -0
- package/src/file-browser/components/RightPanel/FilePreview.tsx +219 -0
- package/src/file-browser/components/RightPanel/RightPanel.tsx +186 -0
- package/src/file-browser/components/RightPanel/RightPanelToolbar.tsx +113 -0
- package/src/file-browser/components/UploadProgress.tsx +123 -0
- package/src/file-browser/components/ViewerHost.tsx +208 -0
- package/src/file-browser/components/mobile/MobileNavigation.tsx +227 -0
- package/src/file-browser/components/navigation/NavigationButtons.tsx +171 -0
- package/src/file-browser/components/shared/ErrorBoundary.tsx +116 -0
- package/src/file-browser/components/shared/FileBrowserItem.tsx +195 -0
- package/src/file-browser/components/shared/FileIcon.tsx +169 -0
- package/src/file-browser/components/toolbar/ViewModeToggle.tsx +200 -0
- package/src/file-browser/components/views/ListView/ListView.tsx +484 -0
- package/src/file-browser/components/views/ThumbnailView/ThumbnailView.tsx +323 -0
- package/src/file-browser/components/views/TreeView/TreeNode.tsx +186 -0
- package/src/file-browser/components/views/TreeView/TreeNodeList.tsx +191 -0
- package/src/file-browser/components/views/TreeView/TreeView.tsx +200 -0
- package/src/file-browser/components/views/TreemapView/TreemapView.tsx +339 -0
- package/src/file-browser/context/FileBrowserContext.tsx +13 -0
- package/src/file-browser/examples/BasicUsage.tsx +20 -0
- package/src/file-browser/index.ts +98 -0
- package/src/file-browser/models/FileBrowserModel.ts +623 -0
- package/src/file-browser/models/LeftPanelManagerModel.ts +105 -0
- package/src/file-browser/models/NavigationManagerModel.ts +312 -0
- package/src/file-browser/models/ResponsiveLayoutManagerModel.ts +437 -0
- package/src/file-browser/models/RightPanelManagerModel.ts +190 -0
- package/src/file-browser/models/SelectionManagerModel.ts +252 -0
- package/src/file-browser/models/ToolbarManagerModel.ts +144 -0
- package/src/file-browser/models/UploadModel.ts +147 -0
- package/src/file-browser/models/ViewModeManagerModel.ts +185 -0
- package/src/file-browser/models/ViewerHostModel.ts +44 -0
- package/src/file-browser/models/ui/ListViewUIModel.ts +265 -0
- package/src/file-browser/models/ui/PreviewUIModel.ts +297 -0
- package/src/file-browser/models/ui/ThumbnailViewUIModel.ts +254 -0
- package/src/file-browser/models/ui/TreeViewUIModel.ts +128 -0
- package/src/file-browser/models/ui/TreemapViewUIModel.ts +350 -0
- package/src/file-browser/providers/FileSystemListProvider.ts +552 -0
- package/src/file-browser/providers/FileSystemProvider.ts +401 -0
- package/src/file-browser/providers/FileSystemTreeProvider.ts +231 -0
- package/src/file-browser/providers/GitProvider.ts +337 -0
- package/src/file-browser/providers/GitRepositoryProvider.ts +376 -0
- package/src/file-browser/providers/IFileBrowserProvider.ts +56 -0
- package/src/file-browser/providers/MemoryProvider.ts +303 -0
- package/src/file-browser/providers/index.ts +4 -0
- package/src/file-browser/registry/ViewerRegistry.ts +551 -0
- package/src/file-browser/registry/types.ts +144 -0
- package/src/file-browser/scripts/performanceBenchmark.ts +553 -0
- package/src/file-browser/services/ThumbnailCacheService.ts +128 -0
- package/src/file-browser/tasks.md +537 -0
- package/src/file-browser/types/FileBrowserTypes.ts +126 -0
- package/src/file-browser/types/ProviderTypes.ts +155 -0
- package/src/file-browser/types/UITypes.ts +235 -0
- package/src/file-browser/types/ViewModeTypes.ts +150 -0
- package/src/file-browser/utils/gestures.ts +327 -0
- package/src/file-browser/utils/performance.ts +563 -0
- package/src/file-browser/viewers/ImageViewer.tsx +163 -0
- package/src/file-browser/viewers/ImageViewerModel.ts +79 -0
- package/src/file-browser/viewers/TextViewer.tsx +95 -0
- package/src/file-browser/viewers/UnsupportedFileViewer.tsx +57 -0
- package/src/file-browser/viewers/index.ts +61 -0
- package/src/git/BranchList.tsx +128 -0
- package/src/git/CommitGraph.tsx +239 -0
- package/src/git/CommitList.tsx +258 -0
- package/src/git/DiffViewer.tsx +219 -0
- package/src/git/index.ts +4 -0
- package/src/icons/iconMap.ts +146 -0
- package/src/icons/index.ts +9 -0
- package/src/index.ts +13 -0
- package/src/layout/README.md +307 -0
- package/src/layout/components/ExplorerLayout/ExplorerLayout.tsx +178 -0
- package/src/layout/examples/SimpleExample.tsx +60 -0
- package/src/layout/index.ts +6 -0
- package/src/lib/utils.ts +1 -0
- package/src/list/README.md +303 -0
- package/src/list/architecture.md +807 -0
- package/src/list/components/CalculatedGridView.tsx +252 -0
- package/src/list/components/DragPreview.tsx +102 -0
- package/src/list/components/ListContextMenu.tsx +274 -0
- package/src/list/components/ListItem.tsx +761 -0
- package/src/list/components/ListItems.tsx +919 -0
- package/src/list/components/MasonryView.tsx +241 -0
- package/src/list/components/SearchFilter.tsx +44 -0
- package/src/list/components/TreemapView.tsx +709 -0
- package/src/list/components/ViewSizeControls.tsx +205 -0
- package/src/list/components/ViewTypeSelector.tsx +312 -0
- package/src/list/components/VirtualizedDetailsView.tsx +231 -0
- package/src/list/components/VirtualizedGrid.tsx +164 -0
- package/src/list/components/VirtualizedList.tsx +154 -0
- package/src/list/components/VirtualizedMasonryView.tsx +344 -0
- package/src/list/components/shared/EmptyState.tsx +103 -0
- package/src/list/components/shared/ErrorBoundary.tsx +123 -0
- package/src/list/components/shared/ErrorDisplay.tsx +100 -0
- package/src/list/components/shared/ListLoader.tsx +146 -0
- package/src/list/components/shared/LoadingIndicator.tsx +80 -0
- package/src/list/index.ts +92 -0
- package/src/list/models/ListItemsModel.ts +1301 -0
- package/src/list/models/TreemapModel.ts +204 -0
- package/src/list/providers/ListItemsProvider.ts +313 -0
- package/src/list/providers/TestListProvider.ts +604 -0
- package/src/list/tasks.md +937 -0
- package/src/list/types/ListTypes.ts +178 -0
- package/src/list/utils/BenchmarkLogger.ts +243 -0
- package/src/list/utils/DragDropManager.ts +320 -0
- package/src/list/utils/GridLayoutCalculator.ts +290 -0
- package/src/list/utils/ListAccessibility.ts +367 -0
- package/src/list/utils/ListKeyboard.ts +414 -0
- package/src/list/utils/MasonryLayoutCalculator.ts +302 -0
- package/src/list/utils/MasonryLayoutEngine.ts +401 -0
- package/src/list/utils/__tests__/MasonryLayoutEngine.test.ts +157 -0
- package/src/list/utils/__tests__/VirtualizedMasonryView.test.tsx +251 -0
- package/src/media/AlbumSidebar.tsx +48 -0
- package/src/media/MediaBrowser.tsx +92 -0
- package/src/media/MediaBrowserModel.ts +138 -0
- package/src/media/MediaGrid.tsx +50 -0
- package/src/media/MediaList.tsx +49 -0
- package/src/media/MediaPreview.tsx +63 -0
- package/src/media/MediaTimeline.tsx +38 -0
- package/src/media/MockMediaProvider.ts +70 -0
- package/src/media/index.ts +18 -0
- package/src/media/types.ts +21 -0
- package/src/styles/variables.css +60 -0
- package/src/tree/DEVELOPMENT_SUMMARY.md +170 -0
- package/src/tree/__tests__/TreeModel.test.ts +16 -0
- package/src/tree/architecture.md +530 -0
- package/src/tree/components/Tree.tsx +283 -0
- package/src/tree/components/TreeCheckbox.tsx +147 -0
- package/src/tree/components/TreeContextMenu.tsx +139 -0
- package/src/tree/components/TreeNodeList.tsx +329 -0
- package/src/tree/components/TreeTable.tsx +382 -0
- package/src/tree/index.ts +58 -0
- package/src/tree/models/TreeModel.ts +839 -0
- package/src/tree/providers/SimpleTreeProvider.ts +463 -0
- package/src/tree/providers/TestTreeProvider.ts +946 -0
- package/src/tree/providers/TreeProvider.ts +308 -0
- package/src/tree/tasks.md +2046 -0
- package/src/tree/types/TreeTypes.ts +279 -0
- package/src/tree/utils/SelectionTheme.ts +150 -0
- package/src/tree/utils/logger.ts +203 -0
- package/src/types-common.ts +21 -0
|
@@ -0,0 +1,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"><{commit.author.email}></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
|
+
};
|