diffhub 0.1.0 → 0.1.2

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 (189) hide show
  1. package/.next/standalone/apps/web/.next/BUILD_ID +1 -1
  2. package/.next/standalone/apps/web/.next/build-manifest.json +3 -3
  3. package/.next/standalone/apps/web/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/apps/web/.next/required-server-files.json +4 -4
  5. package/.next/standalone/apps/web/.next/server/app/_global-error.html +1 -1
  6. package/.next/standalone/apps/web/.next/server/app/_global-error.rsc +1 -1
  7. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  8. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  10. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  11. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  12. package/.next/standalone/apps/web/.next/server/app/_not-found/page.js +1 -1
  13. package/.next/standalone/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  14. package/.next/standalone/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  15. package/.next/standalone/apps/web/.next/server/app/_not-found.html +1 -1
  16. package/.next/standalone/apps/web/.next/server/app/_not-found.rsc +17 -16
  17. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +17 -16
  18. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  19. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_index.segment.rsc +6 -5
  20. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  21. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  22. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/apps/web/.next/server/app/api/comments/route.js +1 -1
  24. package/.next/standalone/apps/web/.next/server/app/api/comments/route.js.nft.json +1 -1
  25. package/.next/standalone/apps/web/.next/server/app/api/diff/route.js +2 -2
  26. package/.next/standalone/apps/web/.next/server/app/api/diff/route.js.nft.json +1 -1
  27. package/.next/standalone/apps/web/.next/server/app/api/discard/route.js +2 -2
  28. package/.next/standalone/apps/web/.next/server/app/api/discard/route.js.nft.json +1 -1
  29. package/.next/standalone/apps/web/.next/server/app/api/file/route.js +2 -2
  30. package/.next/standalone/apps/web/.next/server/app/api/file/route.js.nft.json +1 -1
  31. package/.next/standalone/apps/web/.next/server/app/api/files/route.js +2 -2
  32. package/.next/standalone/apps/web/.next/server/app/api/files/route.js.nft.json +1 -1
  33. package/.next/standalone/apps/web/.next/server/app/index.html +1 -1
  34. package/.next/standalone/apps/web/.next/server/app/index.rsc +16 -15
  35. package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
  36. package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +16 -15
  37. package/.next/standalone/apps/web/.next/server/app/index.segments/_head.segment.rsc +4 -4
  38. package/.next/standalone/apps/web/.next/server/app/index.segments/_index.segment.rsc +6 -5
  39. package/.next/standalone/apps/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  40. package/.next/standalone/apps/web/.next/server/app/page/react-loadable-manifest.json +2 -2
  41. package/.next/standalone/apps/web/.next/server/app/page.js +2 -2
  42. package/.next/standalone/apps/web/.next/server/app/page.js.nft.json +1 -1
  43. package/.next/standalone/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__05ejtyr._.js +3 -0
  45. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0e2dp4h._.js +3 -0
  46. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0egk6ui._.js +3 -0
  47. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0i6i-~n._.js +3 -0
  48. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0sv4hr9._.js +3 -0
  49. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0t.tl18._.js +3 -0
  50. package/.next/standalone/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__06b81~v._.js → [root-of-the-server]__0giwc4b._.js} +2 -2
  51. package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0jit913._.js +3 -0
  52. package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0v19a7g._.js +3 -0
  53. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0f40lcw._.js +3 -0
  54. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0oc3qg_._.js +3 -3
  55. package/.next/standalone/apps/web/.next/server/chunks/ssr/{apps_web_0b_ykcu._.js → _0qo42r0._.js} +2 -2
  56. package/.next/standalone/apps/web/.next/server/chunks/ssr/{apps_web_08kf15u._.js → apps_web_0758ax4._.js} +2 -2
  57. package/.next/standalone/apps/web/.next/server/chunks/ssr/node_modules_0v8w2j~._.js +70 -0
  58. package/.next/standalone/apps/web/.next/server/middleware-build-manifest.js +3 -3
  59. package/.next/standalone/apps/web/.next/server/pages/404.html +1 -1
  60. package/.next/standalone/apps/web/.next/server/pages/500.html +1 -1
  61. package/.next/standalone/apps/web/.next/server/server-reference-manifest.js +1 -1
  62. package/.next/standalone/apps/web/.next/server/server-reference-manifest.json +1 -1
  63. package/.next/standalone/apps/web/.next/static/chunks/00zp83w4g1v1r.js +16 -0
  64. package/.next/standalone/apps/web/.next/static/chunks/042ip11u2i2z6.js +138 -0
  65. package/.next/standalone/apps/web/.next/static/chunks/07ndp5x2cvqo..css +3 -0
  66. package/.next/standalone/apps/web/.next/static/chunks/085s9eg867ha-.js +1 -0
  67. package/.next/standalone/apps/web/.next/static/chunks/0_vmme_k_ff_u.js +67 -0
  68. package/.next/standalone/apps/web/.next/static/chunks/0ap~_hc7r17_6.js +1 -0
  69. package/.next/standalone/apps/web/.next/static/chunks/0nt5-rwas5thn.js +67 -0
  70. package/.next/standalone/apps/web/.next/static/chunks/0p~3-4_.v0cft.js +1 -0
  71. package/.next/standalone/apps/web/.next/static/chunks/0wx-2dr44ql-g.js +1 -0
  72. package/.next/standalone/apps/web/.next/static/chunks/{0syypqto3~pe_.js → 0y0o261rjun_2.js} +8 -8
  73. package/.next/standalone/apps/web/.next/static/chunks/0y5z3t-z1c8ks.js.map +5 -0
  74. package/.next/standalone/apps/web/.next/static/chunks/17b.xoi.b6rcl.js +1 -0
  75. package/.next/standalone/apps/web/.next/static/chunks/turbopack-0_n_4n~_4no2a.js +1 -0
  76. package/.next/standalone/apps/web/.next/static/chunks/turbopack-worker-0sjn--fhq~1cg.js +1 -0
  77. package/.next/standalone/apps/web/.next/static/media/diffs.worker.09unk0quktc_5.ts +1 -0
  78. package/.next/standalone/apps/web/package.json +12 -2
  79. package/.next/standalone/apps/web/server.js +1 -1
  80. package/README.md +10 -3
  81. package/bin/diffhub.mjs +1 -1
  82. package/package.json +12 -2
  83. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__05vwx85._.js +0 -3
  84. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__09vmjc2._.js +0 -3
  85. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0etmu4u._.js +0 -3
  86. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0g-_a7n._.js +0 -3
  87. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0mshgfw._.js +0 -3
  88. package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0qixima._.js +0 -3
  89. package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0khyzju._.js +0 -3
  90. package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0l2mjim._.js +0 -70
  91. package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0q2lon0._.js +0 -3
  92. package/.next/standalone/apps/web/.next/server/chunks/ssr/_059a1m-._.js +0 -3
  93. package/.next/standalone/apps/web/.next/static/chunks/0.ae0sud8tm7k.js +0 -204
  94. package/.next/standalone/apps/web/.next/static/chunks/0di~ntk7iivm4.js +0 -1
  95. package/.next/standalone/apps/web/.next/static/chunks/0pklg0nmdvay8.js +0 -1
  96. package/.next/standalone/apps/web/.next/static/chunks/0up9_7hiwl_dt.js +0 -1
  97. package/.next/standalone/apps/web/.next/static/chunks/0~984a88e4rp9.js +0 -204
  98. package/.next/standalone/apps/web/.next/static/chunks/13y8355z8m13w.js +0 -1
  99. package/.next/standalone/apps/web/.next/static/chunks/14gfmthe5zcf..js +0 -16
  100. package/.next/standalone/apps/web/.next/static/chunks/15--1d_raviej.css +0 -3
  101. package/.next/standalone/apps/web/AGENTS.md +0 -60
  102. package/.next/standalone/apps/web/CLAUDE.md +0 -1
  103. package/.next/standalone/apps/web/README.md +0 -78
  104. package/.next/standalone/apps/web/app/api/comments/route.ts +0 -20
  105. package/.next/standalone/apps/web/app/api/diff/route.ts +0 -15
  106. package/.next/standalone/apps/web/app/api/discard/route.ts +0 -15
  107. package/.next/standalone/apps/web/app/api/file/route.ts +0 -17
  108. package/.next/standalone/apps/web/app/api/files/route.ts +0 -14
  109. package/.next/standalone/apps/web/app/api/open/route.ts +0 -64
  110. package/.next/standalone/apps/web/app/favicon.ico +0 -0
  111. package/.next/standalone/apps/web/app/globals.css +0 -214
  112. package/.next/standalone/apps/web/app/layout.tsx +0 -52
  113. package/.next/standalone/apps/web/app/page.tsx +0 -6
  114. package/.next/standalone/apps/web/bin/diffhub.mjs +0 -147
  115. package/.next/standalone/apps/web/components/ContextMenu.tsx +0 -161
  116. package/.next/standalone/apps/web/components/DiffApp.tsx +0 -363
  117. package/.next/standalone/apps/web/components/DiffViewer.tsx +0 -565
  118. package/.next/standalone/apps/web/components/FileDiffHeader.tsx +0 -119
  119. package/.next/standalone/apps/web/components/FileList.tsx +0 -438
  120. package/.next/standalone/apps/web/components/KeyboardShortcutsDialog.tsx +0 -79
  121. package/.next/standalone/apps/web/components/SidebarHelpMenu.tsx +0 -86
  122. package/.next/standalone/apps/web/components/StatusBar.tsx +0 -212
  123. package/.next/standalone/apps/web/components/icons/file-status-icons.tsx +0 -48
  124. package/.next/standalone/apps/web/components/theme-provider.tsx +0 -12
  125. package/.next/standalone/apps/web/components/ui/button.tsx +0 -90
  126. package/.next/standalone/apps/web/components/ui/empty.tsx +0 -82
  127. package/.next/standalone/apps/web/components/ui/input.tsx +0 -18
  128. package/.next/standalone/apps/web/components/ui/kbd.tsx +0 -14
  129. package/.next/standalone/apps/web/components/ui/separator.tsx +0 -23
  130. package/.next/standalone/apps/web/components/ui/sheet.tsx +0 -109
  131. package/.next/standalone/apps/web/components/ui/sidebar.tsx +0 -700
  132. package/.next/standalone/apps/web/components/ui/skeleton.tsx +0 -9
  133. package/.next/standalone/apps/web/components/ui/toggle.tsx +0 -35
  134. package/.next/standalone/apps/web/components/ui/tooltip.tsx +0 -52
  135. package/.next/standalone/apps/web/components.json +0 -27
  136. package/.next/standalone/apps/web/lib/comments.ts +0 -52
  137. package/.next/standalone/apps/web/lib/export-comments.ts +0 -13
  138. package/.next/standalone/apps/web/lib/git.ts +0 -201
  139. package/.next/standalone/apps/web/lib/use-mobile.ts +0 -19
  140. package/.next/standalone/apps/web/lib/utils.ts +0 -5
  141. package/.next/standalone/apps/web/next.config.ts +0 -19
  142. package/.next/standalone/apps/web/node_modules/@img/sharp-darwin-arm64/lib/sharp-darwin-arm64.node +0 -0
  143. package/.next/standalone/apps/web/node_modules/@img/sharp-darwin-arm64/package.json +0 -40
  144. package/.next/standalone/apps/web/node_modules/@img/sharp-libvips-darwin-arm64/README.md +0 -46
  145. package/.next/standalone/apps/web/node_modules/@img/sharp-libvips-darwin-arm64/lib/glib-2.0/include/glibconfig.h +0 -220
  146. package/.next/standalone/apps/web/node_modules/@img/sharp-libvips-darwin-arm64/lib/index.js +0 -1
  147. package/.next/standalone/apps/web/node_modules/@img/sharp-libvips-darwin-arm64/lib/libvips-cpp.8.17.3.dylib +0 -0
  148. package/.next/standalone/apps/web/node_modules/@img/sharp-libvips-darwin-arm64/package.json +0 -36
  149. package/.next/standalone/apps/web/node_modules/@img/sharp-libvips-darwin-arm64/versions.json +0 -30
  150. package/.next/standalone/apps/web/oxfmt.config.ts +0 -6
  151. package/.next/standalone/apps/web/oxlint.config.ts +0 -13
  152. package/.next/standalone/apps/web/postcss.config.mjs +0 -7
  153. package/.next/standalone/apps/web/public/file.svg +0 -1
  154. package/.next/standalone/apps/web/public/glide-variable-italic.woff2 +0 -0
  155. package/.next/standalone/apps/web/public/glide-variable.woff2 +0 -0
  156. package/.next/standalone/apps/web/public/globe.svg +0 -1
  157. package/.next/standalone/apps/web/public/next.svg +0 -1
  158. package/.next/standalone/apps/web/public/operator-mono-book-italic.woff2 +0 -0
  159. package/.next/standalone/apps/web/public/operator-mono-book.woff2 +0 -0
  160. package/.next/standalone/apps/web/public/operator-mono-medium-italic.woff2 +0 -0
  161. package/.next/standalone/apps/web/public/operator-mono-medium.woff2 +0 -0
  162. package/.next/standalone/apps/web/public/vercel.svg +0 -1
  163. package/.next/standalone/apps/web/public/window.svg +0 -1
  164. package/.next/standalone/apps/web/tsconfig.json +0 -34
  165. package/.next/standalone/apps/web/tsconfig.tsbuildinfo +0 -1
  166. /package/.next/standalone/apps/web/.next/static/{x0qhb6ReQoZ53hlmgg2-_ → mAVCL4HCmtJwT35feBPdK}/_buildManifest.js +0 -0
  167. /package/.next/standalone/apps/web/.next/static/{x0qhb6ReQoZ53hlmgg2-_ → mAVCL4HCmtJwT35feBPdK}/_clientMiddlewareManifest.js +0 -0
  168. /package/.next/standalone/apps/web/.next/static/{x0qhb6ReQoZ53hlmgg2-_ → mAVCL4HCmtJwT35feBPdK}/_ssgManifest.js +0 -0
  169. /package/.next/standalone/{apps/web/node_modules → node_modules}/semver/classes/comparator.js +0 -0
  170. /package/.next/standalone/{apps/web/node_modules → node_modules}/semver/classes/range.js +0 -0
  171. /package/.next/standalone/{apps/web/node_modules → node_modules}/semver/classes/semver.js +0 -0
  172. /package/.next/standalone/{apps/web/node_modules → node_modules}/semver/functions/cmp.js +0 -0
  173. /package/.next/standalone/{apps/web/node_modules → node_modules}/semver/functions/coerce.js +0 -0
  174. /package/.next/standalone/{apps/web/node_modules → node_modules}/semver/functions/compare.js +0 -0
  175. /package/.next/standalone/{apps/web/node_modules → node_modules}/semver/functions/eq.js +0 -0
  176. /package/.next/standalone/{apps/web/node_modules → node_modules}/semver/functions/gt.js +0 -0
  177. /package/.next/standalone/{apps/web/node_modules → node_modules}/semver/functions/gte.js +0 -0
  178. /package/.next/standalone/{apps/web/node_modules → node_modules}/semver/functions/lt.js +0 -0
  179. /package/.next/standalone/{apps/web/node_modules → node_modules}/semver/functions/lte.js +0 -0
  180. /package/.next/standalone/{apps/web/node_modules → node_modules}/semver/functions/neq.js +0 -0
  181. /package/.next/standalone/{apps/web/node_modules → node_modules}/semver/functions/parse.js +0 -0
  182. /package/.next/standalone/{apps/web/node_modules → node_modules}/semver/functions/satisfies.js +0 -0
  183. /package/.next/standalone/{apps/web/node_modules → node_modules}/semver/internal/constants.js +0 -0
  184. /package/.next/standalone/{apps/web/node_modules → node_modules}/semver/internal/debug.js +0 -0
  185. /package/.next/standalone/{apps/web/node_modules → node_modules}/semver/internal/identifiers.js +0 -0
  186. /package/.next/standalone/{apps/web/node_modules → node_modules}/semver/internal/lrucache.js +0 -0
  187. /package/.next/standalone/{apps/web/node_modules → node_modules}/semver/internal/parse-options.js +0 -0
  188. /package/.next/standalone/{apps/web/node_modules → node_modules}/semver/internal/re.js +0 -0
  189. /package/.next/standalone/{apps/web/node_modules → node_modules}/semver/package.json +0 -0
@@ -1,438 +0,0 @@
1
- "use client";
2
-
3
- import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
4
- import { BubbleDotsIcon, FolderIcon, FolderOpenIcon, MagnifyingGlassIcon } from "blode-icons-react";
5
- import { FileAddedIcon, FileDiffIcon, FileRemovedIcon } from "./icons/file-status-icons";
6
- import type { DiffFileStat } from "@/lib/git";
7
- import type { Comment } from "@/lib/comments";
8
- import { ContextMenu } from "./ContextMenu";
9
- import { cn } from "@/lib/utils";
10
- import { Sidebar, SidebarContent, SidebarHeader } from "@/components/ui/sidebar";
11
-
12
- interface FileListProps {
13
- files: DiffFileStat[];
14
- selectedFile: string | null;
15
- onSelectFile: (file: string) => void;
16
- comments: Comment[];
17
- repoPath: string;
18
- filterQuery: string;
19
- onFilterChange: (q: string) => void;
20
- viewedFiles: Set<string>;
21
- }
22
-
23
- // ── Tree types ──────────────────────────────────────────────────────────────
24
-
25
- interface FileNode {
26
- type: "file";
27
- name: string;
28
- path: string;
29
- fileStat: DiffFileStat;
30
- }
31
-
32
- interface FolderNode {
33
- type: "folder";
34
- /** May be "a/b/c" after compaction of single-child chains. */
35
- name: string;
36
- /** Path of the deepest folder in the (possibly compacted) chain. */
37
- path: string;
38
- children: TreeNode[];
39
- }
40
-
41
- type TreeNode = FileNode | FolderNode;
42
-
43
- // ── Phase 1: Build hierarchical tree from flat file list ────────────────────
44
-
45
- const buildTree = (files: DiffFileStat[]): TreeNode[] => {
46
- interface RawNode {
47
- files: DiffFileStat[];
48
- folders: Record<string, RawNode>;
49
- }
50
- const root: RawNode = { files: [], folders: {} };
51
-
52
- for (const fileStat of files) {
53
- const parts = fileStat.file.split("/");
54
- let current = root;
55
- for (let i = 0; i < parts.length - 1; i += 1) {
56
- const part = parts[i];
57
- if (!current.folders[part]) {
58
- current.folders[part] = { files: [], folders: {} };
59
- }
60
- current = current.folders[part];
61
- }
62
- current.files.push(fileStat);
63
- }
64
-
65
- const convertFolder = (name: string, node: RawNode, parentPath: string): FolderNode => {
66
- const path = parentPath ? `${parentPath}/${name}` : name;
67
- const children: TreeNode[] = [];
68
-
69
- for (const [fn, child] of Object.entries(node.folders).toSorted()) {
70
- children.push(convertFolder(fn, child, path));
71
- }
72
- for (const fileStat of node.files) {
73
- const parts = fileStat.file.split("/");
74
- children.push({
75
- fileStat,
76
- name: parts.at(-1) ?? fileStat.file,
77
- path: fileStat.file,
78
- type: "file",
79
- });
80
- }
81
- return { children, name, path, type: "folder" };
82
- };
83
-
84
- const result: TreeNode[] = [];
85
- for (const [fn, node] of Object.entries(root.folders).toSorted()) {
86
- result.push(convertFolder(fn, node, ""));
87
- }
88
- for (const fileStat of root.files) {
89
- result.push({ fileStat, name: fileStat.file, path: fileStat.file, type: "file" });
90
- }
91
- return result;
92
- };
93
-
94
- // ── Phase 2: Compact single-child-folder chains (VS Code / Zed style) ───────
95
- //
96
- // When a folder has exactly 1 child that is itself a folder (and no files),
97
- // merge them into one display node: "parent/child" (recursive).
98
-
99
- const compactTree = (nodes: TreeNode[]): TreeNode[] =>
100
- nodes.map((node) => {
101
- if (node.type === "file") {
102
- return node;
103
- }
104
-
105
- const kids = compactTree(node.children);
106
-
107
- if (kids.length === 1 && kids[0].type === "folder") {
108
- const only = kids[0] as FolderNode;
109
- return { ...only, name: `${node.name}/${only.name}` } satisfies FolderNode;
110
- }
111
-
112
- return { ...node, children: kids };
113
- });
114
-
115
- // ── Sub-components ──────────────────────────────────────────────────────────
116
-
117
- interface FileRowProps {
118
- node: FileNode;
119
- depth: number;
120
- isSelected: boolean;
121
- isViewed: boolean;
122
- commentCount: number;
123
- onSelect: (path: string) => void;
124
- onContextMenu: (x: number, y: number, file: string) => void;
125
- }
126
-
127
- const FileRow = memo(function FileRow({
128
- node,
129
- depth,
130
- isSelected,
131
- isViewed,
132
- commentCount,
133
- onSelect,
134
- onContextMenu,
135
- }: FileRowProps) {
136
- const indent = depth * 16 + 8;
137
- const { insertions, deletions } = node.fileStat;
138
-
139
- let FileStatusIcon = FileDiffIcon;
140
- if (insertions > 0 && deletions === 0) {
141
- FileStatusIcon = FileAddedIcon;
142
- } else if (deletions > 0 && insertions === 0) {
143
- FileStatusIcon = FileRemovedIcon;
144
- }
145
-
146
- let iconClass = "shrink-0 text-sidebar-foreground/40";
147
- if (insertions > 0 && deletions === 0) {
148
- iconClass = "shrink-0 text-diff-green";
149
- } else if (deletions > 0 && insertions === 0) {
150
- iconClass = "shrink-0 text-destructive";
151
- }
152
-
153
- return (
154
- <button
155
- type="button"
156
- className={cn(
157
- "flex w-full cursor-pointer items-center gap-1.5 py-1 text-left transition-colors",
158
- isSelected
159
- ? "bg-sidebar-accent text-sidebar-accent-foreground"
160
- : "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
161
- isViewed && "opacity-50",
162
- )}
163
- style={{ paddingLeft: indent, paddingRight: 8 }}
164
- // oxlint-disable-next-line react-perf/jsx-no-new-function-as-prop
165
- onClick={() => onSelect(node.path)}
166
- // oxlint-disable-next-line react-perf/jsx-no-new-function-as-prop
167
- onContextMenu={(e) => {
168
- e.preventDefault();
169
- onContextMenu(e.clientX, e.clientY, node.path);
170
- }}
171
- >
172
- <FileStatusIcon size={14} className={iconClass} />
173
- <span className="flex-1 truncate text-[12px] leading-tight">{node.name}</span>
174
- {commentCount > 0 && (
175
- <span className="flex shrink-0 items-center gap-0.5 text-[10px] text-sidebar-foreground/50">
176
- <BubbleDotsIcon size={10} />
177
- {commentCount}
178
- </span>
179
- )}
180
- </button>
181
- );
182
- });
183
-
184
- interface FolderRowProps {
185
- node: FolderNode;
186
- depth: number;
187
- isCollapsed: boolean;
188
- onToggle: (path: string) => void;
189
- children: React.ReactNode;
190
- }
191
-
192
- const FolderRow = memo(function FolderRow({
193
- node,
194
- depth,
195
- isCollapsed,
196
- onToggle,
197
- children,
198
- }: FolderRowProps) {
199
- const indent = depth * 16 + 8;
200
- const segments = node.name.split("/");
201
-
202
- return (
203
- <>
204
- <button
205
- type="button"
206
- aria-expanded={!isCollapsed}
207
- className="flex w-full items-center gap-1.5 py-1 text-left transition-colors hover:bg-sidebar-accent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-sidebar-ring"
208
- style={{ paddingLeft: indent, paddingRight: 8 }}
209
- // oxlint-disable-next-line react-perf/jsx-no-new-function-as-prop
210
- onClick={() => onToggle(node.path)}
211
- >
212
- {isCollapsed ? (
213
- <FolderIcon size={12} className="shrink-0 text-sidebar-foreground/50" />
214
- ) : (
215
- <FolderOpenIcon size={12} className="shrink-0 text-sidebar-foreground/50" />
216
- )}
217
- <span className="truncate text-[12px] text-sidebar-foreground/70">
218
- {segments.map((seg, i) => (
219
- // oxlint-disable-next-line react/no-array-index-key
220
- <Fragment key={i}>
221
- {seg}
222
- {i < segments.length - 1 && <span className="text-sidebar-foreground/30">/</span>}
223
- </Fragment>
224
- ))}
225
- </span>
226
- </button>
227
- {!isCollapsed && (
228
- <div className="relative">
229
- {/* Indent guide line — centred on the folder icon (icon is 12px wide, offset 6px) */}
230
- <div
231
- className="pointer-events-none absolute inset-y-0 border-l border-sidebar-border/60"
232
- style={{ left: indent + 6 }}
233
- />
234
- {children}
235
- </div>
236
- )}
237
- </>
238
- );
239
- });
240
-
241
- // ── Main component ──────────────────────────────────────────────────────────
242
-
243
- const DEFAULT_SIDEBAR_WIDTH = 256;
244
- const MIN_SIDEBAR_WIDTH = 8;
245
-
246
- export const FileList = ({
247
- files,
248
- selectedFile,
249
- onSelectFile,
250
- comments,
251
- repoPath,
252
- filterQuery,
253
- onFilterChange,
254
- viewedFiles,
255
- }: FileListProps) => {
256
- const [contextMenu, setContextMenu] = useState<{
257
- x: number;
258
- y: number;
259
- file: string;
260
- } | null>(null);
261
- const [collapsedFolders, setCollapsedFolders] = useState<Set<string>>(new Set());
262
-
263
- const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_SIDEBAR_WIDTH);
264
- const dragRef = useRef<{ startX: number; startWidth: number } | null>(null);
265
-
266
- useEffect(() => {
267
- const handleMouseMove = (e: MouseEvent) => {
268
- if (!dragRef.current) {
269
- return;
270
- }
271
- const newWidth = Math.max(
272
- MIN_SIDEBAR_WIDTH,
273
- dragRef.current.startWidth + e.clientX - dragRef.current.startX,
274
- );
275
- setSidebarWidth(newWidth);
276
- };
277
- const handleMouseUp = () => {
278
- if (!dragRef.current) {
279
- return;
280
- }
281
- dragRef.current = null;
282
- document.body.style.cursor = "";
283
- document.body.style.userSelect = "";
284
- };
285
- document.addEventListener("mousemove", handleMouseMove);
286
- document.addEventListener("mouseup", handleMouseUp);
287
- return () => {
288
- document.removeEventListener("mousemove", handleMouseMove);
289
- document.removeEventListener("mouseup", handleMouseUp);
290
- };
291
- }, []);
292
-
293
- const handleRailMouseDown = useCallback(
294
- (e: React.MouseEvent) => {
295
- e.preventDefault();
296
- dragRef.current = { startWidth: sidebarWidth, startX: e.clientX };
297
- document.body.style.cursor = "col-resize";
298
- document.body.style.userSelect = "none";
299
- },
300
- [sidebarWidth],
301
- );
302
-
303
- const filtered = useMemo(
304
- () =>
305
- filterQuery
306
- ? files.filter((f) => f.file.toLowerCase().includes(filterQuery.toLowerCase()))
307
- : files,
308
- [files, filterQuery],
309
- );
310
-
311
- const tree = useMemo(() => compactTree(buildTree(filtered)), [filtered]);
312
-
313
- const commentsByFile = useMemo(() => {
314
- const map = new Map<string, number>();
315
- for (const c of comments) {
316
- map.set(c.file, (map.get(c.file) ?? 0) + 1);
317
- }
318
- return map;
319
- }, [comments]);
320
-
321
- const toggleFolder = useCallback((path: string) => {
322
- setCollapsedFolders((prev) => {
323
- const next = new Set(prev);
324
- if (next.has(path)) {
325
- next.delete(path);
326
- } else {
327
- next.add(path);
328
- }
329
- return next;
330
- });
331
- }, []);
332
-
333
- const handleContextMenu = useCallback((x: number, y: number, file: string) => {
334
- setContextMenu({ file, x, y });
335
- }, []);
336
-
337
- const renderTree = (nodes: TreeNode[], depth: number): React.ReactNode =>
338
- nodes.map((node) => {
339
- if (node.type === "folder") {
340
- return (
341
- <FolderRow
342
- key={node.path}
343
- node={node}
344
- depth={depth}
345
- isCollapsed={collapsedFolders.has(node.path)}
346
- onToggle={toggleFolder}
347
- >
348
- {renderTree(node.children, depth + 1)}
349
- </FolderRow>
350
- );
351
- }
352
-
353
- return (
354
- <FileRow
355
- key={node.path}
356
- node={node}
357
- depth={depth}
358
- isSelected={selectedFile === node.path}
359
- isViewed={viewedFiles.has(node.path)}
360
- commentCount={commentsByFile.get(node.path) ?? 0}
361
- onSelect={onSelectFile}
362
- onContextMenu={handleContextMenu}
363
- />
364
- );
365
- });
366
-
367
- return (
368
- <Sidebar
369
- collapsible="none"
370
- className="relative overflow-hidden border-r border-sidebar-border"
371
- style={{ "--sidebar-width": `${sidebarWidth}px` } as React.CSSProperties}
372
- >
373
- {/* Filter */}
374
- <SidebarHeader className="border-b border-sidebar-border h-[53px] flex-row items-center py-0 px-2">
375
- <div className="relative flex w-full items-center">
376
- <MagnifyingGlassIcon
377
- size={12}
378
- className="pointer-events-none absolute left-2.5 text-sidebar-foreground/40"
379
- />
380
- <input
381
- type="text"
382
- value={filterQuery}
383
- // oxlint-disable-next-line react-perf/jsx-no-new-function-as-prop
384
- onChange={(e) => onFilterChange(e.target.value)}
385
- placeholder="Filter files…"
386
- aria-label="Filter files"
387
- className="w-full rounded-md border border-sidebar-border bg-sidebar-accent py-1.5 pl-7 pr-7 text-xs text-sidebar-foreground placeholder:text-sidebar-foreground/40 transition-colors focus:border-sidebar-ring/50 focus:outline-none"
388
- />
389
- {filterQuery && (
390
- <button
391
- type="button"
392
- // oxlint-disable-next-line react-perf/jsx-no-new-function-as-prop
393
- onClick={() => onFilterChange("")}
394
- aria-label="Clear filter"
395
- className="absolute right-2 text-sm leading-none text-sidebar-foreground/40 transition-colors hover:text-sidebar-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-sidebar-ring"
396
- >
397
- ×
398
- </button>
399
- )}
400
- </div>
401
- </SidebarHeader>
402
-
403
- {/* Tree */}
404
- <SidebarContent className="gap-0 py-1">
405
- {filtered.length === 0 ? (
406
- <div className="flex flex-col items-center gap-2 px-4 py-8 text-center">
407
- <p className="text-xs text-sidebar-foreground/50">No changes</p>
408
- {filterQuery && (
409
- <p className="text-[10px] text-sidebar-foreground/30">
410
- No files match &ldquo;{filterQuery}&rdquo;
411
- </p>
412
- )}
413
- </div>
414
- ) : (
415
- renderTree(tree, 0)
416
- )}
417
- </SidebarContent>
418
-
419
- {/* Context menu */}
420
- {contextMenu && (
421
- <ContextMenu
422
- x={contextMenu.x}
423
- y={contextMenu.y}
424
- filePath={contextMenu.file}
425
- repoPath={repoPath}
426
- // oxlint-disable-next-line react-perf/jsx-no-new-function-as-prop
427
- onClose={() => setContextMenu(null)}
428
- />
429
- )}
430
- {/* Resize rail */}
431
- <div
432
- aria-hidden
433
- className="absolute inset-y-0 right-0 z-20 w-[5px] cursor-col-resize after:absolute after:inset-y-0 after:left-1/2 after:-translate-x-1/2 after:w-px after:transition-colors hover:after:bg-sidebar-border"
434
- onMouseDown={handleRailMouseDown}
435
- />
436
- </Sidebar>
437
- );
438
- };
@@ -1,79 +0,0 @@
1
- "use client";
2
-
3
- import { Dialog } from "@base-ui/react/dialog";
4
- import { Kbd } from "@/components/ui/kbd";
5
-
6
- interface ShortcutRowProps {
7
- keys: string[];
8
- description: string;
9
- }
10
-
11
- const ShortcutRow = ({ keys, description }: ShortcutRowProps) => (
12
- <div className="flex items-center justify-between py-2.5 border-b border-border last:border-0">
13
- <span className="text-sm text-foreground">{description}</span>
14
- <div className="flex items-center gap-1">
15
- {keys.map((k) => (
16
- <Kbd key={k}>{k}</Kbd>
17
- ))}
18
- </div>
19
- </div>
20
- );
21
-
22
- const SHORTCUTS: ShortcutRowProps[] = [
23
- { description: "Next file", keys: ["j"] },
24
- { description: "Previous file", keys: ["k"] },
25
- { description: "Toggle viewed", keys: ["v"] },
26
- { description: "Toggle split / unified", keys: ["s"] },
27
- { description: "Refresh diff", keys: ["r"] },
28
- { description: "Focus file filter", keys: ["/"] },
29
- { description: "Open keyboard shortcuts", keys: ["?"] },
30
- ];
31
-
32
- interface KeyboardShortcutsDialogProps {
33
- open: boolean;
34
- onClose: () => void;
35
- }
36
-
37
- export const KeyboardShortcutsDialog = ({ open, onClose }: KeyboardShortcutsDialogProps) => (
38
- <Dialog.Root
39
- open={open}
40
- // oxlint-disable-next-line react-perf/jsx-no-new-function-as-prop
41
- onOpenChange={(o) => {
42
- if (!o) {
43
- onClose();
44
- }
45
- }}
46
- >
47
- <Dialog.Portal>
48
- <Dialog.Backdrop className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm transition-opacity duration-200 data-[starting-style]:opacity-0 data-[ending-style]:opacity-0" />
49
- <Dialog.Popup className="fixed left-1/2 top-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-xl border border-border bg-card shadow-2xl dark:shadow-none outline-none transition-[opacity,transform] duration-200 data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95">
50
- {/* Header */}
51
- <div className="flex items-center justify-between border-b border-border px-5 py-4">
52
- <Dialog.Title className="text-sm font-semibold text-foreground">
53
- Keyboard shortcuts
54
- </Dialog.Title>
55
- <Dialog.Close
56
- aria-label="Close"
57
- className="flex size-7 items-center justify-center rounded-md text-muted-foreground hover:bg-secondary hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring/50"
58
- >
59
- ×
60
- </Dialog.Close>
61
- </div>
62
-
63
- {/* Shortcuts list */}
64
- <div className="px-5 py-1">
65
- {SHORTCUTS.map((s) => (
66
- <ShortcutRow key={s.description} {...s} />
67
- ))}
68
- </div>
69
-
70
- {/* Footer */}
71
- <div className="border-t border-border px-5 py-3">
72
- <p className="text-xs text-muted-foreground">
73
- Press <Kbd>?</Kbd> anywhere to open this dialog
74
- </p>
75
- </div>
76
- </Dialog.Popup>
77
- </Dialog.Portal>
78
- </Dialog.Root>
79
- );
@@ -1,86 +0,0 @@
1
- "use client";
2
-
3
- import { KeyboardCableIcon } from "blode-icons-react";
4
- import { useEffect, useRef, useState } from "react";
5
- import { KeyboardShortcutsDialog } from "@/components/KeyboardShortcutsDialog";
6
- import { Kbd } from "@/components/ui/kbd";
7
-
8
- export const SidebarHelpMenu = () => {
9
- const [menuOpen, setMenuOpen] = useState(false);
10
- const [shortcutsOpen, setShortcutsOpen] = useState(false);
11
- const menuRef = useRef<HTMLDivElement>(null);
12
-
13
- // Close on outside click
14
- useEffect(() => {
15
- if (!menuOpen) {
16
- return;
17
- }
18
- const handleClick = (e: MouseEvent) => {
19
- if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
20
- setMenuOpen(false);
21
- }
22
- };
23
- document.addEventListener("mousedown", handleClick);
24
- return () => document.removeEventListener("mousedown", handleClick);
25
- }, [menuOpen]);
26
-
27
- // ? shortcut opens keyboard shortcuts dialog
28
- useEffect(() => {
29
- const handleKey = (e: KeyboardEvent) => {
30
- if (e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLInputElement) {
31
- return;
32
- }
33
- if (e.key === "?") {
34
- setShortcutsOpen(true);
35
- }
36
- };
37
- window.addEventListener("keydown", handleKey);
38
- return () => window.removeEventListener("keydown", handleKey);
39
- }, []);
40
-
41
- return (
42
- <div className="relative" ref={menuRef}>
43
- <button
44
- type="button"
45
- aria-label="Help and settings"
46
- // oxlint-disable-next-line react-perf/jsx-no-new-function-as-prop
47
- onClick={() => setMenuOpen((o) => !o)}
48
- className="flex size-6 items-center justify-center rounded-full border border-border text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring/50"
49
- >
50
- <svg
51
- aria-hidden="true"
52
- className="size-3.5"
53
- fill="currentColor"
54
- viewBox="0 0 16 16"
55
- xmlns="http://www.w3.org/2000/svg"
56
- >
57
- <path d="M7.569 9.75c-.332 0-.614-.27-.578-.6.021-.188.061-.372.136-.62q.158-.51.447-.82a3.4 3.4 0 0 1 .703-.577q.284-.182.507-.396.229-.219.358-.486a1.4 1.4 0 0 0 .13-.606 1.2 1.2 0 0 0-.171-.653 1.2 1.2 0 0 0-.466-.429 1.36 1.36 0 0 0-.647-.152q-.33 0-.628.148a1.23 1.23 0 0 0-.587.622c-.123.295-.367.555-.686.555h-.472c-.337 0-.616-.28-.55-.611q.103-.513.363-.905a2.55 2.55 0 0 1 1.08-.915A3.6 3.6 0 0 1 7.998 3q.888 0 1.563.32.68.319 1.057.91.382.586.382 1.392 0 .543-.172.972a2.4 2.4 0 0 1-.48.763 3.5 3.5 0 0 1-.74.595 3.2 3.2 0 0 0-.62.496 1.7 1.7 0 0 0-.353.605l-.034.106c-.1.316-.35.591-.682.591zM8.75 12a1 1 0 1 1-2 0 1 1 0 0 1 2 0" />
58
- </svg>
59
- </button>
60
-
61
- {menuOpen && (
62
- <div className="diffhub-menu-animate absolute bottom-full left-0 mb-1.5 z-50 min-w-[180px] rounded-lg border border-border bg-card shadow-lg dark:shadow-none py-1">
63
- <button
64
- type="button"
65
- className="flex w-full items-center gap-2.5 px-3 py-2 text-sm text-foreground hover:bg-secondary transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring/50"
66
- // oxlint-disable-next-line react-perf/jsx-no-new-function-as-prop
67
- onClick={() => {
68
- setShortcutsOpen(true);
69
- setMenuOpen(false);
70
- }}
71
- >
72
- <KeyboardCableIcon size={14} className="text-muted-foreground shrink-0" />
73
- Keyboard shortcuts
74
- <Kbd className="ml-auto">?</Kbd>
75
- </button>
76
- </div>
77
- )}
78
-
79
- <KeyboardShortcutsDialog
80
- open={shortcutsOpen}
81
- // oxlint-disable-next-line react-perf/jsx-no-new-function-as-prop
82
- onClose={() => setShortcutsOpen(false)}
83
- />
84
- </div>
85
- );
86
- };