diffhub 0.1.1 → 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.
- package/.next/standalone/apps/web/.next/BUILD_ID +1 -1
- package/.next/standalone/apps/web/.next/build-manifest.json +3 -3
- package/.next/standalone/apps/web/.next/prerender-manifest.json +3 -3
- package/.next/standalone/apps/web/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found/page.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.rsc +16 -15
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +16 -15
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_index.segment.rsc +5 -4
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/api/comments/route.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/api/comments/route.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/api/diff/route.js +2 -2
- package/.next/standalone/apps/web/.next/server/app/api/diff/route.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/api/discard/route.js +2 -2
- package/.next/standalone/apps/web/.next/server/app/api/discard/route.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/api/file/route.js +2 -2
- package/.next/standalone/apps/web/.next/server/app/api/file/route.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/api/files/route.js +2 -2
- package/.next/standalone/apps/web/.next/server/app/api/files/route.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/index.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/index.rsc +15 -14
- package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +15 -14
- package/.next/standalone/apps/web/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/index.segments/_index.segment.rsc +5 -4
- package/.next/standalone/apps/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/apps/web/.next/server/app/page.js +2 -2
- package/.next/standalone/apps/web/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__05ejtyr._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0e2dp4h._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0egk6ui._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0i6i-~n._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0sv4hr9._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0t.tl18._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__06b81~v._.js → [root-of-the-server]__0giwc4b._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0jit913._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0v19a7g._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0f40lcw._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0oc3qg_._.js +3 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{apps_web_0b_ykcu._.js → _0qo42r0._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{apps_web_08kf15u._.js → apps_web_0758ax4._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/node_modules_0v8w2j~._.js +70 -0
- package/.next/standalone/apps/web/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/apps/web/.next/server/pages/404.html +1 -1
- package/.next/standalone/apps/web/.next/server/pages/500.html +1 -1
- package/.next/standalone/apps/web/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/apps/web/.next/static/chunks/{0-ci0c9di0qo8.js → 00zp83w4g1v1r.js} +3 -3
- package/.next/standalone/apps/web/.next/static/chunks/042ip11u2i2z6.js +138 -0
- package/.next/standalone/apps/web/.next/static/chunks/085s9eg867ha-.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0_vmme_k_ff_u.js +67 -0
- package/.next/standalone/apps/web/.next/static/chunks/0ap~_hc7r17_6.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0nt5-rwas5thn.js +67 -0
- package/.next/standalone/apps/web/.next/static/chunks/0p~3-4_.v0cft.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0wx-2dr44ql-g.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/{0syypqto3~pe_.js → 0y0o261rjun_2.js} +8 -8
- package/.next/standalone/apps/web/.next/static/chunks/0y5z3t-z1c8ks.js.map +5 -0
- package/.next/standalone/apps/web/.next/static/chunks/17b.xoi.b6rcl.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/turbopack-0_n_4n~_4no2a.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/turbopack-worker-0sjn--fhq~1cg.js +1 -0
- package/.next/standalone/apps/web/.next/static/media/diffs.worker.09unk0quktc_5.ts +1 -0
- package/.next/standalone/apps/web/package.json +4 -4
- package/README.md +10 -3
- package/package.json +4 -4
- package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__05vwx85._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__09vmjc2._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0etmu4u._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0g-_a7n._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0mshgfw._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/[root-of-the-server]__0qixima._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0f~hmsk._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0khyzju._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0l2mjim._.js +0 -70
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0kwaklj._.js +0 -3
- package/.next/standalone/apps/web/.next/static/chunks/0.ae0sud8tm7k.js +0 -204
- package/.next/standalone/apps/web/.next/static/chunks/0di~ntk7iivm4.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/0pklg0nmdvay8.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/0up9_7hiwl_dt.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/0~984a88e4rp9.js +0 -204
- package/.next/standalone/apps/web/.next/static/chunks/13y8355z8m13w.js +0 -1
- package/.next/standalone/apps/web/AGENTS.md +0 -60
- package/.next/standalone/apps/web/CHANGELOG.md +0 -7
- package/.next/standalone/apps/web/CLAUDE.md +0 -1
- package/.next/standalone/apps/web/README.md +0 -78
- package/.next/standalone/apps/web/app/api/comments/route.ts +0 -20
- package/.next/standalone/apps/web/app/api/diff/route.ts +0 -15
- package/.next/standalone/apps/web/app/api/discard/route.ts +0 -15
- package/.next/standalone/apps/web/app/api/file/route.ts +0 -17
- package/.next/standalone/apps/web/app/api/files/route.ts +0 -14
- package/.next/standalone/apps/web/app/api/open/route.ts +0 -64
- package/.next/standalone/apps/web/app/favicon.ico +0 -0
- package/.next/standalone/apps/web/app/globals.css +0 -214
- package/.next/standalone/apps/web/app/layout.tsx +0 -52
- package/.next/standalone/apps/web/app/page.tsx +0 -6
- package/.next/standalone/apps/web/bin/diffhub.mjs +0 -147
- package/.next/standalone/apps/web/components/ContextMenu.tsx +0 -161
- package/.next/standalone/apps/web/components/DiffApp.tsx +0 -419
- package/.next/standalone/apps/web/components/DiffViewer.tsx +0 -565
- package/.next/standalone/apps/web/components/FileDiffHeader.tsx +0 -119
- package/.next/standalone/apps/web/components/FileList.tsx +0 -455
- package/.next/standalone/apps/web/components/KeyboardShortcutsDialog.tsx +0 -79
- package/.next/standalone/apps/web/components/SidebarHelpMenu.tsx +0 -86
- package/.next/standalone/apps/web/components/StatusBar.tsx +0 -212
- package/.next/standalone/apps/web/components/icons/file-status-icons.tsx +0 -48
- package/.next/standalone/apps/web/components/theme-provider.tsx +0 -12
- package/.next/standalone/apps/web/components/ui/button.tsx +0 -90
- package/.next/standalone/apps/web/components/ui/empty.tsx +0 -82
- package/.next/standalone/apps/web/components/ui/input.tsx +0 -18
- package/.next/standalone/apps/web/components/ui/kbd.tsx +0 -14
- package/.next/standalone/apps/web/components/ui/separator.tsx +0 -23
- package/.next/standalone/apps/web/components/ui/sheet.tsx +0 -109
- package/.next/standalone/apps/web/components/ui/sidebar.tsx +0 -700
- package/.next/standalone/apps/web/components/ui/skeleton.tsx +0 -9
- package/.next/standalone/apps/web/components/ui/toggle.tsx +0 -35
- package/.next/standalone/apps/web/components/ui/tooltip.tsx +0 -52
- package/.next/standalone/apps/web/components.json +0 -27
- package/.next/standalone/apps/web/lib/comments.ts +0 -52
- package/.next/standalone/apps/web/lib/export-comments.ts +0 -13
- package/.next/standalone/apps/web/lib/git.ts +0 -201
- package/.next/standalone/apps/web/lib/use-mobile.ts +0 -19
- package/.next/standalone/apps/web/lib/utils.ts +0 -5
- package/.next/standalone/apps/web/next.config.ts +0 -19
- package/.next/standalone/apps/web/oxfmt.config.ts +0 -6
- package/.next/standalone/apps/web/oxlint.config.ts +0 -13
- package/.next/standalone/apps/web/postcss.config.mjs +0 -7
- package/.next/standalone/apps/web/public/file.svg +0 -1
- package/.next/standalone/apps/web/public/glide-variable-italic.woff2 +0 -0
- package/.next/standalone/apps/web/public/glide-variable.woff2 +0 -0
- package/.next/standalone/apps/web/public/globe.svg +0 -1
- package/.next/standalone/apps/web/public/next.svg +0 -1
- package/.next/standalone/apps/web/public/operator-mono-book-italic.woff2 +0 -0
- package/.next/standalone/apps/web/public/operator-mono-book.woff2 +0 -0
- package/.next/standalone/apps/web/public/operator-mono-medium-italic.woff2 +0 -0
- package/.next/standalone/apps/web/public/operator-mono-medium.woff2 +0 -0
- package/.next/standalone/apps/web/public/vercel.svg +0 -1
- package/.next/standalone/apps/web/public/window.svg +0 -1
- package/.next/standalone/apps/web/tsconfig.json +0 -34
- /package/.next/standalone/apps/web/.next/static/{ZhI_-YaFho-fQoajjgwSH → mAVCL4HCmtJwT35feBPdK}/_buildManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{ZhI_-YaFho-fQoajjgwSH → mAVCL4HCmtJwT35feBPdK}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{ZhI_-YaFho-fQoajjgwSH → mAVCL4HCmtJwT35feBPdK}/_ssgManifest.js +0 -0
|
@@ -1,455 +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={{
|
|
164
|
-
containIntrinsicBlockSize: "26px",
|
|
165
|
-
contentVisibility: "auto",
|
|
166
|
-
paddingLeft: indent,
|
|
167
|
-
paddingRight: 8,
|
|
168
|
-
}}
|
|
169
|
-
// oxlint-disable-next-line react-perf/jsx-no-new-function-as-prop
|
|
170
|
-
onClick={() => onSelect(node.path)}
|
|
171
|
-
// oxlint-disable-next-line react-perf/jsx-no-new-function-as-prop
|
|
172
|
-
onContextMenu={(e) => {
|
|
173
|
-
e.preventDefault();
|
|
174
|
-
onContextMenu(e.clientX, e.clientY, node.path);
|
|
175
|
-
}}
|
|
176
|
-
>
|
|
177
|
-
<FileStatusIcon size={14} className={iconClass} />
|
|
178
|
-
<span className="flex-1 truncate text-[12px] leading-tight">{node.name}</span>
|
|
179
|
-
{commentCount > 0 && (
|
|
180
|
-
<span className="flex shrink-0 items-center gap-0.5 text-[10px] text-sidebar-foreground/50">
|
|
181
|
-
<BubbleDotsIcon size={10} />
|
|
182
|
-
{commentCount}
|
|
183
|
-
</span>
|
|
184
|
-
)}
|
|
185
|
-
</button>
|
|
186
|
-
);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
interface FolderRowProps {
|
|
190
|
-
node: FolderNode;
|
|
191
|
-
depth: number;
|
|
192
|
-
isCollapsed: boolean;
|
|
193
|
-
onToggle: (path: string) => void;
|
|
194
|
-
children: React.ReactNode;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const FolderRow = memo(function FolderRow({
|
|
198
|
-
node,
|
|
199
|
-
depth,
|
|
200
|
-
isCollapsed,
|
|
201
|
-
onToggle,
|
|
202
|
-
children,
|
|
203
|
-
}: FolderRowProps) {
|
|
204
|
-
const indent = depth * 16 + 8;
|
|
205
|
-
const segments = node.name.split("/");
|
|
206
|
-
|
|
207
|
-
return (
|
|
208
|
-
<>
|
|
209
|
-
<button
|
|
210
|
-
type="button"
|
|
211
|
-
aria-expanded={!isCollapsed}
|
|
212
|
-
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"
|
|
213
|
-
style={{ paddingLeft: indent, paddingRight: 8 }}
|
|
214
|
-
// oxlint-disable-next-line react-perf/jsx-no-new-function-as-prop
|
|
215
|
-
onClick={() => onToggle(node.path)}
|
|
216
|
-
>
|
|
217
|
-
{isCollapsed ? (
|
|
218
|
-
<FolderIcon size={12} className="shrink-0 text-sidebar-foreground/50" />
|
|
219
|
-
) : (
|
|
220
|
-
<FolderOpenIcon size={12} className="shrink-0 text-sidebar-foreground/50" />
|
|
221
|
-
)}
|
|
222
|
-
<span className="truncate text-[12px] text-sidebar-foreground/70">
|
|
223
|
-
{segments.map((seg, i) => (
|
|
224
|
-
// oxlint-disable-next-line react/no-array-index-key
|
|
225
|
-
<Fragment key={i}>
|
|
226
|
-
{seg}
|
|
227
|
-
{i < segments.length - 1 && <span className="text-sidebar-foreground/30">/</span>}
|
|
228
|
-
</Fragment>
|
|
229
|
-
))}
|
|
230
|
-
</span>
|
|
231
|
-
</button>
|
|
232
|
-
{!isCollapsed && (
|
|
233
|
-
<div className="relative">
|
|
234
|
-
{/* Indent guide line — centred on the folder icon (icon is 12px wide, offset 6px) */}
|
|
235
|
-
<div
|
|
236
|
-
className="pointer-events-none absolute inset-y-0 border-l border-sidebar-border/60"
|
|
237
|
-
style={{ left: indent + 6 }}
|
|
238
|
-
/>
|
|
239
|
-
{children}
|
|
240
|
-
</div>
|
|
241
|
-
)}
|
|
242
|
-
</>
|
|
243
|
-
);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
// ── Main component ──────────────────────────────────────────────────────────
|
|
247
|
-
|
|
248
|
-
const DEFAULT_SIDEBAR_WIDTH = 256;
|
|
249
|
-
const MIN_SIDEBAR_WIDTH = 8;
|
|
250
|
-
|
|
251
|
-
export const FileList = ({
|
|
252
|
-
files,
|
|
253
|
-
selectedFile,
|
|
254
|
-
onSelectFile,
|
|
255
|
-
comments,
|
|
256
|
-
repoPath,
|
|
257
|
-
filterQuery,
|
|
258
|
-
onFilterChange,
|
|
259
|
-
viewedFiles,
|
|
260
|
-
}: FileListProps) => {
|
|
261
|
-
const [contextMenu, setContextMenu] = useState<{
|
|
262
|
-
x: number;
|
|
263
|
-
y: number;
|
|
264
|
-
file: string;
|
|
265
|
-
} | null>(null);
|
|
266
|
-
const [collapsedFolders, setCollapsedFolders] = useState<Set<string>>(new Set());
|
|
267
|
-
|
|
268
|
-
const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_SIDEBAR_WIDTH);
|
|
269
|
-
const dragRef = useRef<{ startX: number; startWidth: number } | null>(null);
|
|
270
|
-
|
|
271
|
-
useEffect(() => {
|
|
272
|
-
const handleMouseMove = (e: MouseEvent) => {
|
|
273
|
-
if (!dragRef.current) {
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
const newWidth = Math.max(
|
|
277
|
-
MIN_SIDEBAR_WIDTH,
|
|
278
|
-
dragRef.current.startWidth + e.clientX - dragRef.current.startX,
|
|
279
|
-
);
|
|
280
|
-
setSidebarWidth(newWidth);
|
|
281
|
-
};
|
|
282
|
-
const handleMouseUp = () => {
|
|
283
|
-
if (!dragRef.current) {
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
dragRef.current = null;
|
|
287
|
-
document.body.style.cursor = "";
|
|
288
|
-
document.body.style.userSelect = "";
|
|
289
|
-
};
|
|
290
|
-
document.addEventListener("mousemove", handleMouseMove);
|
|
291
|
-
document.addEventListener("mouseup", handleMouseUp);
|
|
292
|
-
return () => {
|
|
293
|
-
document.removeEventListener("mousemove", handleMouseMove);
|
|
294
|
-
document.removeEventListener("mouseup", handleMouseUp);
|
|
295
|
-
};
|
|
296
|
-
}, []);
|
|
297
|
-
|
|
298
|
-
const handleRailMouseDown = useCallback(
|
|
299
|
-
(e: React.MouseEvent) => {
|
|
300
|
-
e.preventDefault();
|
|
301
|
-
dragRef.current = { startWidth: sidebarWidth, startX: e.clientX };
|
|
302
|
-
document.body.style.cursor = "col-resize";
|
|
303
|
-
document.body.style.userSelect = "none";
|
|
304
|
-
},
|
|
305
|
-
[sidebarWidth],
|
|
306
|
-
);
|
|
307
|
-
|
|
308
|
-
const filtered = useMemo(
|
|
309
|
-
() =>
|
|
310
|
-
filterQuery
|
|
311
|
-
? files.filter((f) => f.file.toLowerCase().includes(filterQuery.toLowerCase()))
|
|
312
|
-
: files,
|
|
313
|
-
[files, filterQuery],
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
const MAX_FILES = 500;
|
|
317
|
-
const cappedFiles = useMemo(
|
|
318
|
-
() => (filtered.length > MAX_FILES ? filtered.slice(0, MAX_FILES) : filtered),
|
|
319
|
-
[filtered],
|
|
320
|
-
);
|
|
321
|
-
const tree = useMemo(() => compactTree(buildTree(cappedFiles)), [cappedFiles]);
|
|
322
|
-
|
|
323
|
-
const commentsByFile = useMemo(() => {
|
|
324
|
-
const map = new Map<string, number>();
|
|
325
|
-
for (const c of comments) {
|
|
326
|
-
map.set(c.file, (map.get(c.file) ?? 0) + 1);
|
|
327
|
-
}
|
|
328
|
-
return map;
|
|
329
|
-
}, [comments]);
|
|
330
|
-
|
|
331
|
-
const toggleFolder = useCallback((path: string) => {
|
|
332
|
-
setCollapsedFolders((prev) => {
|
|
333
|
-
const next = new Set(prev);
|
|
334
|
-
if (next.has(path)) {
|
|
335
|
-
next.delete(path);
|
|
336
|
-
} else {
|
|
337
|
-
next.add(path);
|
|
338
|
-
}
|
|
339
|
-
return next;
|
|
340
|
-
});
|
|
341
|
-
}, []);
|
|
342
|
-
|
|
343
|
-
const handleContextMenu = useCallback((x: number, y: number, file: string) => {
|
|
344
|
-
setContextMenu({ file, x, y });
|
|
345
|
-
}, []);
|
|
346
|
-
|
|
347
|
-
const renderTree = (nodes: TreeNode[], depth: number): React.ReactNode =>
|
|
348
|
-
nodes.map((node) => {
|
|
349
|
-
if (node.type === "folder") {
|
|
350
|
-
return (
|
|
351
|
-
<FolderRow
|
|
352
|
-
key={node.path}
|
|
353
|
-
node={node}
|
|
354
|
-
depth={depth}
|
|
355
|
-
isCollapsed={collapsedFolders.has(node.path)}
|
|
356
|
-
onToggle={toggleFolder}
|
|
357
|
-
>
|
|
358
|
-
{renderTree(node.children, depth + 1)}
|
|
359
|
-
</FolderRow>
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return (
|
|
364
|
-
<FileRow
|
|
365
|
-
key={node.path}
|
|
366
|
-
node={node}
|
|
367
|
-
depth={depth}
|
|
368
|
-
isSelected={selectedFile === node.path}
|
|
369
|
-
isViewed={viewedFiles.has(node.path)}
|
|
370
|
-
commentCount={commentsByFile.get(node.path) ?? 0}
|
|
371
|
-
onSelect={onSelectFile}
|
|
372
|
-
onContextMenu={handleContextMenu}
|
|
373
|
-
/>
|
|
374
|
-
);
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
return (
|
|
378
|
-
<Sidebar
|
|
379
|
-
collapsible="none"
|
|
380
|
-
className="relative overflow-hidden border-r border-sidebar-border"
|
|
381
|
-
style={{ "--sidebar-width": `${sidebarWidth}px` } as React.CSSProperties}
|
|
382
|
-
>
|
|
383
|
-
{/* Filter */}
|
|
384
|
-
<SidebarHeader className="border-b border-sidebar-border h-[53px] flex-row items-center py-0 px-2">
|
|
385
|
-
<div className="relative flex w-full items-center">
|
|
386
|
-
<MagnifyingGlassIcon
|
|
387
|
-
size={12}
|
|
388
|
-
className="pointer-events-none absolute left-2.5 text-sidebar-foreground/40"
|
|
389
|
-
/>
|
|
390
|
-
<input
|
|
391
|
-
type="text"
|
|
392
|
-
value={filterQuery}
|
|
393
|
-
// oxlint-disable-next-line react-perf/jsx-no-new-function-as-prop
|
|
394
|
-
onChange={(e) => onFilterChange(e.target.value)}
|
|
395
|
-
placeholder="Filter files…"
|
|
396
|
-
aria-label="Filter files"
|
|
397
|
-
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"
|
|
398
|
-
/>
|
|
399
|
-
{filterQuery && (
|
|
400
|
-
<button
|
|
401
|
-
type="button"
|
|
402
|
-
// oxlint-disable-next-line react-perf/jsx-no-new-function-as-prop
|
|
403
|
-
onClick={() => onFilterChange("")}
|
|
404
|
-
aria-label="Clear filter"
|
|
405
|
-
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"
|
|
406
|
-
>
|
|
407
|
-
×
|
|
408
|
-
</button>
|
|
409
|
-
)}
|
|
410
|
-
</div>
|
|
411
|
-
</SidebarHeader>
|
|
412
|
-
|
|
413
|
-
{/* Tree */}
|
|
414
|
-
<SidebarContent className="gap-0 py-1">
|
|
415
|
-
{filtered.length === 0 ? (
|
|
416
|
-
<div className="flex flex-col items-center gap-2 px-4 py-8 text-center">
|
|
417
|
-
<p className="text-xs text-sidebar-foreground/50">No changes</p>
|
|
418
|
-
{filterQuery && (
|
|
419
|
-
<p className="text-[10px] text-sidebar-foreground/30">
|
|
420
|
-
No files match “{filterQuery}”
|
|
421
|
-
</p>
|
|
422
|
-
)}
|
|
423
|
-
</div>
|
|
424
|
-
) : (
|
|
425
|
-
<>
|
|
426
|
-
{renderTree(tree, 0)}
|
|
427
|
-
{filtered.length > MAX_FILES && (
|
|
428
|
-
<p className="px-3 py-2 text-[10px] text-sidebar-foreground/40">
|
|
429
|
-
Showing {MAX_FILES} of {filtered.length} files
|
|
430
|
-
</p>
|
|
431
|
-
)}
|
|
432
|
-
</>
|
|
433
|
-
)}
|
|
434
|
-
</SidebarContent>
|
|
435
|
-
|
|
436
|
-
{/* Context menu */}
|
|
437
|
-
{contextMenu && (
|
|
438
|
-
<ContextMenu
|
|
439
|
-
x={contextMenu.x}
|
|
440
|
-
y={contextMenu.y}
|
|
441
|
-
filePath={contextMenu.file}
|
|
442
|
-
repoPath={repoPath}
|
|
443
|
-
// oxlint-disable-next-line react-perf/jsx-no-new-function-as-prop
|
|
444
|
-
onClose={() => setContextMenu(null)}
|
|
445
|
-
/>
|
|
446
|
-
)}
|
|
447
|
-
{/* Resize rail */}
|
|
448
|
-
<div
|
|
449
|
-
aria-hidden
|
|
450
|
-
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"
|
|
451
|
-
onMouseDown={handleRailMouseDown}
|
|
452
|
-
/>
|
|
453
|
-
</Sidebar>
|
|
454
|
-
);
|
|
455
|
-
};
|
|
@@ -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
|
-
};
|