Skip to content

Commit 4e4eb4a

Browse files
authored
feat: Revamp right sidebar for changes and files (#202)
1 parent 187bd3c commit 4e4eb4a

File tree

26 files changed

+628
-185
lines changed

26 files changed

+628
-185
lines changed

apps/array/src/main/preload.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,8 +370,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
370370
ipcRenderer.invoke("show-split-context-menu"),
371371
showFileContextMenu: (
372372
filePath: string,
373+
options?: { showCollapseAll?: boolean },
373374
): Promise<ExternalAppContextMenuResult> =>
374-
ipcRenderer.invoke("show-file-context-menu", filePath),
375+
ipcRenderer.invoke("show-file-context-menu", filePath, options),
375376
folders: {
376377
getFolders: (): Promise<RegisteredFolder[]> =>
377378
ipcRenderer.invoke("get-folders"),

apps/array/src/main/services/contextMenu.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ export const showFileContextMenuService = createIpcService({
321321
handler: async (
322322
_event,
323323
filePath: string,
324+
options?: { showCollapseAll?: boolean },
324325
): Promise<ExternalAppContextMenuResult> => {
325326
return new Promise((resolve) => {
326327
const setupMenu = async () => {
@@ -329,7 +330,21 @@ export const showFileContextMenuService = createIpcService({
329330
resolve,
330331
);
331332

332-
showContextMenu(externalAppsItems, { action: null }).then(resolve);
333+
const template: MenuItemConstructorOptions[] = [];
334+
335+
if (options?.showCollapseAll) {
336+
template.push(
337+
{
338+
label: "Collapse All",
339+
click: () => resolve({ action: { type: "collapse-all" } }),
340+
},
341+
{ type: "separator" },
342+
);
343+
}
344+
345+
template.push(...externalAppsItems);
346+
347+
showContextMenu(template, { action: null }).then(resolve);
333348
};
334349

335350
setupMenu();

apps/array/src/main/services/contextMenu.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
export type ExternalAppAction =
33
| { type: "open-in-app"; appId: string }
44
| { type: "copy-path" }
5+
| { type: "collapse-all" }
56
| null;
67

78
export interface ExternalAppContextMenuResult {

apps/array/src/renderer/components/HeaderRow.tsx

Lines changed: 68 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,48 @@
1+
import { RightSidebarTrigger } from "@features/right-sidebar/components/RightSidebarTrigger";
2+
import { useRightSidebarStore } from "@features/right-sidebar/stores/rightSidebarStore";
13
import { SidebarTrigger } from "@features/sidebar/components/SidebarTrigger";
24
import { useSidebarStore } from "@features/sidebar/stores/sidebarStore";
5+
import { ChangesTabBadge } from "@features/task-detail/components/ChangesTabBadge";
36
import { Box, Flex } from "@radix-ui/themes";
47
import { useHeaderStore } from "@stores/headerStore";
5-
import { useEffect } from "react";
8+
import { useNavigationStore } from "@stores/navigationStore";
69

710
const HEADER_HEIGHT = 40;
811
const COLLAPSED_WIDTH = 110;
9-
const MIN_WIDTH = 140;
1012

1113
export function HeaderRow() {
1214
const content = useHeaderStore((state) => state.content);
15+
const view = useNavigationStore((state) => state.view);
16+
1317
const sidebarOpen = useSidebarStore((state) => state.open);
1418
const sidebarWidth = useSidebarStore((state) => state.width);
1519
const isResizing = useSidebarStore((state) => state.isResizing);
16-
const setWidth = useSidebarStore((state) => state.setWidth);
1720
const setIsResizing = useSidebarStore((state) => state.setIsResizing);
1821

19-
const handleMouseDown = (e: React.MouseEvent) => {
22+
const rightSidebarOpen = useRightSidebarStore((state) => state.open);
23+
const rightSidebarWidth = useRightSidebarStore((state) => state.width);
24+
const rightSidebarIsResizing = useRightSidebarStore(
25+
(state) => state.isResizing,
26+
);
27+
const setRightSidebarIsResizing = useRightSidebarStore(
28+
(state) => state.setIsResizing,
29+
);
30+
31+
const showRightSidebarSection = view.type === "task-detail";
32+
33+
const handleLeftSidebarMouseDown = (e: React.MouseEvent) => {
2034
e.preventDefault();
2135
setIsResizing(true);
2236
document.body.style.cursor = "col-resize";
2337
document.body.style.userSelect = "none";
2438
};
2539

26-
useEffect(() => {
27-
const handleMouseMove = (e: MouseEvent) => {
28-
if (!isResizing) return;
29-
30-
const maxWidth = window.innerWidth * 0.5;
31-
const newWidth = Math.max(MIN_WIDTH, Math.min(maxWidth, e.clientX));
32-
setWidth(newWidth);
33-
};
34-
35-
const handleMouseUp = () => {
36-
if (isResizing) {
37-
setIsResizing(false);
38-
document.body.style.cursor = "";
39-
document.body.style.userSelect = "";
40-
}
41-
};
42-
43-
document.addEventListener("mousemove", handleMouseMove);
44-
document.addEventListener("mouseup", handleMouseUp);
45-
46-
return () => {
47-
document.removeEventListener("mousemove", handleMouseMove);
48-
document.removeEventListener("mouseup", handleMouseUp);
49-
};
50-
}, [setWidth, isResizing, setIsResizing]);
40+
const handleRightSidebarMouseDown = (e: React.MouseEvent) => {
41+
e.preventDefault();
42+
setRightSidebarIsResizing(true);
43+
document.body.style.cursor = "col-resize";
44+
document.body.style.userSelect = "none";
45+
};
5146

5247
return (
5348
<Flex
@@ -63,6 +58,7 @@ export function HeaderRow() {
6358
align="center"
6459
justify="end"
6560
px="2"
61+
pr="3"
6662
style={{
6763
width: sidebarOpen ? `${sidebarWidth}px` : `${COLLAPSED_WIDTH}px`,
6864
minWidth: `${COLLAPSED_WIDTH}px`,
@@ -75,7 +71,7 @@ export function HeaderRow() {
7571
<SidebarTrigger />
7672
{sidebarOpen && (
7773
<Box
78-
onMouseDown={handleMouseDown}
74+
onMouseDown={handleLeftSidebarMouseDown}
7975
className="no-drag"
8076
style={{
8177
position: "absolute",
@@ -102,6 +98,48 @@ export function HeaderRow() {
10298
{content}
10399
</Flex>
104100
)}
101+
102+
{showRightSidebarSection && view.type === "task-detail" && view.data && (
103+
<Flex
104+
align="center"
105+
justify="between"
106+
px="2"
107+
pl="3"
108+
style={{
109+
width: rightSidebarOpen
110+
? `${rightSidebarWidth}px`
111+
: `${COLLAPSED_WIDTH}px`,
112+
minWidth: `${COLLAPSED_WIDTH}px`,
113+
height: "100%",
114+
borderLeft: "1px solid var(--gray-6)",
115+
transition: rightSidebarIsResizing
116+
? "none"
117+
: "width 0.2s ease-in-out",
118+
position: "relative",
119+
}}
120+
>
121+
<RightSidebarTrigger />
122+
{rightSidebarOpen && (
123+
<ChangesTabBadge taskId={view.data.id} task={view.data} />
124+
)}
125+
{rightSidebarOpen && (
126+
<Box
127+
onMouseDown={handleRightSidebarMouseDown}
128+
className="no-drag"
129+
style={{
130+
position: "absolute",
131+
left: 0,
132+
top: 0,
133+
bottom: 0,
134+
width: "4px",
135+
cursor: "col-resize",
136+
backgroundColor: "transparent",
137+
zIndex: 100,
138+
}}
139+
/>
140+
)}
141+
</Flex>
142+
)}
105143
</Flex>
106144
);
107145
}

apps/array/src/renderer/components/MainLayout.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@ import { StatusBar } from "@components/StatusBar";
33
import { UpdatePrompt } from "@components/UpdatePrompt";
44
import { CommandMenu } from "@features/command/components/CommandMenu";
55
import { usePanelLayoutStore } from "@features/panels/store/panelLayoutStore";
6+
import {
7+
RightSidebar,
8+
RightSidebarContent,
9+
useRightSidebarStore,
10+
} from "@features/right-sidebar";
611
import { SettingsView } from "@features/settings/components/SettingsView";
712
import { MainSidebar } from "@features/sidebar/components/MainSidebar";
13+
import { useSidebarStore } from "@features/sidebar/stores/sidebarStore";
814
import { TaskDetail } from "@features/task-detail/components/TaskDetail";
915
import { TaskInput } from "@features/task-detail/components/TaskInput";
1016
import { TaskList } from "@features/task-list/components/TaskList";
@@ -26,6 +32,8 @@ export function MainLayout() {
2632
goForward,
2733
} = useNavigationStore();
2834
const clearAllLayouts = usePanelLayoutStore((state) => state.clearAllLayouts);
35+
const toggleLeftSidebar = useSidebarStore((state) => state.toggle);
36+
const toggleRightSidebar = useRightSidebarStore((state) => state.toggle);
2937
useIntegrations();
3038
const [commandMenuOpen, setCommandMenuOpen] = useState(false);
3139

@@ -55,6 +63,8 @@ export function MainLayout() {
5563
useHotkeys("mod+,", () => handleOpenSettings());
5664
useHotkeys("mod+[", () => goBack());
5765
useHotkeys("mod+]", () => goForward());
66+
useHotkeys("mod+b", () => toggleLeftSidebar());
67+
useHotkeys("mod+shift+b", () => toggleRightSidebar());
5868

5969
useEffect(() => {
6070
const unsubscribeSettings = window.electronAPI?.onOpenSettings(() => {
@@ -118,6 +128,12 @@ export function MainLayout() {
118128

119129
{view.type === "settings" && <SettingsView />}
120130
</Box>
131+
132+
{view.type === "task-detail" && view.data && (
133+
<RightSidebar>
134+
<RightSidebarContent taskId={view.data.id} task={view.data} />
135+
</RightSidebar>
136+
)}
121137
</Flex>
122138

123139
<StatusBar />
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { Box, Flex } from "@radix-ui/themes";
2+
import React from "react";
3+
4+
const MIN_WIDTH = 140;
5+
6+
interface ResizableSidebarProps {
7+
children: React.ReactNode;
8+
open: boolean;
9+
width: number;
10+
setWidth: (width: number) => void;
11+
isResizing: boolean;
12+
setIsResizing: (isResizing: boolean) => void;
13+
side: "left" | "right";
14+
}
15+
16+
export const ResizableSidebar: React.FC<ResizableSidebarProps> = ({
17+
children,
18+
open,
19+
width,
20+
setWidth,
21+
isResizing,
22+
setIsResizing,
23+
side,
24+
}) => {
25+
const handleMouseDown = (e: React.MouseEvent) => {
26+
e.preventDefault();
27+
setIsResizing(true);
28+
document.body.style.cursor = "col-resize";
29+
document.body.style.userSelect = "none";
30+
};
31+
32+
React.useEffect(() => {
33+
const handleMouseMove = (e: MouseEvent) => {
34+
if (!isResizing) return;
35+
36+
const maxWidth = window.innerWidth * 0.5;
37+
const newWidth =
38+
side === "left"
39+
? Math.max(MIN_WIDTH, Math.min(maxWidth, e.clientX))
40+
: Math.max(
41+
MIN_WIDTH,
42+
Math.min(maxWidth, window.innerWidth - e.clientX),
43+
);
44+
setWidth(newWidth);
45+
};
46+
47+
const handleMouseUp = () => {
48+
if (isResizing) {
49+
setIsResizing(false);
50+
document.body.style.cursor = "";
51+
document.body.style.userSelect = "";
52+
}
53+
};
54+
55+
document.addEventListener("mousemove", handleMouseMove);
56+
document.addEventListener("mouseup", handleMouseUp);
57+
58+
return () => {
59+
document.removeEventListener("mousemove", handleMouseMove);
60+
document.removeEventListener("mouseup", handleMouseUp);
61+
};
62+
}, [setWidth, isResizing, setIsResizing, side]);
63+
64+
const isLeft = side === "left";
65+
66+
return (
67+
<Box
68+
style={{
69+
width: open ? `${width}px` : "0",
70+
minWidth: open ? `${width}px` : "0",
71+
maxWidth: open ? `${width}px` : "0",
72+
height: "100%",
73+
overflow: "hidden",
74+
transition: isResizing ? "none" : "width 0.2s ease-in-out",
75+
borderLeft: !isLeft && open ? "1px solid var(--gray-6)" : "none",
76+
borderRight: isLeft && open ? "1px solid var(--gray-6)" : "none",
77+
position: "relative",
78+
flexShrink: 0,
79+
}}
80+
>
81+
<Flex
82+
direction="column"
83+
style={{
84+
width: `${width}px`,
85+
height: "100%",
86+
}}
87+
>
88+
{children}
89+
</Flex>
90+
{open && (
91+
<Box
92+
onMouseDown={handleMouseDown}
93+
className="no-drag"
94+
style={{
95+
position: "absolute",
96+
left: isLeft ? undefined : 0,
97+
right: isLeft ? 0 : undefined,
98+
top: 0,
99+
bottom: 0,
100+
width: "4px",
101+
cursor: "col-resize",
102+
backgroundColor: "transparent",
103+
zIndex: 100,
104+
}}
105+
/>
106+
)}
107+
</Box>
108+
);
109+
};

apps/array/src/renderer/features/code-editor/components/CodeEditorPanel.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,15 @@ export function CodeEditorPanel({
5050
return <PanelMessage>Loading file...</PanelMessage>;
5151
}
5252

53-
if (error || !fileContent) {
53+
if (error || fileContent == null) {
5454
return <PanelMessage>Failed to load file</PanelMessage>;
5555
}
5656

57+
// If we ever allow editing in the CodeMirrorEditor, this can be removed
58+
if (fileContent.length === 0) {
59+
return <PanelMessage>File is empty</PanelMessage>;
60+
}
61+
5762
return (
5863
<Box height="100%" style={{ overflow: "hidden" }}>
5964
<CodeMirrorEditor

apps/array/src/renderer/features/panels/components/DraggableTab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ export const DraggableTab: React.FC<DraggableTabProps> = ({
153153
size="1"
154154
variant="ghost"
155155
color={isActive ? undefined : "gray"}
156-
className="opacity-0 transition-opacity group-hover:opacity-100"
156+
className={`transition-opacity ${isActive ? "opacity-100" : "opacity-0 group-hover:opacity-100"}`}
157157
aria-label="Close tab"
158158
onClick={(e) => {
159159
e.stopPropagation();

apps/array/src/renderer/features/panels/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ export {
77
export { useDragDropHandlers } from "./hooks/useDragDropHandlers";
88
export { usePanelLayoutStore } from "./store/panelLayoutStore";
99
export { usePanelStore } from "./store/panelStore";
10-
export { isDiffTabActiveInTree } from "./store/panelStoreHelpers";
10+
export {
11+
isDiffTabActiveInTree,
12+
isFileTabActiveInTree,
13+
} from "./store/panelStoreHelpers";
1114

1215
export type {
1316
GroupId,

0 commit comments

Comments
 (0)