Skip to content

Commit c1f6440

Browse files
authored
🤖 refactor: improve UI density and fix mobile-only features (#516)
## Changes ### Project Sidebar - Increased Density Previously showed project name and abbreviated path on two lines: ``` ProjectName /U/a/P/c/project-name ``` Now shows single line with styled basename: ``` /U/a/P/c/project-name ``` Where directory path is muted and basename is bold/prominent. Same visual hierarchy with 50% less vertical space. - Added `splitAbbreviatedPath()` utility function - Added comprehensive tests for the new utility - Applied to both project list items and drag preview ### ChatInput - Fixed Responsive Control Hiding - Replaced broken container query syntax (`max-@[Xpx]:hidden`) with media queries - Mode switch & thinking slider hide at 550px - Context 1M checkbox hides at 450px - Prevents controls from wrapping to multiple lines on constrained viewports ### Mobile Features - Touch Device Only Fixed all mobile-specific UI to only activate on touch devices (`pointer: coarse`), not just small viewports: - 44px minimum touch targets only on touch devices - Hamburger menu button only on touch devices - Sidebar overlay only on touch devices - Mobile layout stacking only on touch devices **Fixes:** - ✅ No more flashing sidebar when resizing desktop browser - ✅ No more broken button sizes on small desktop viewports - ✅ Chat no longer disappears on small desktop viewports - ✅ Desktop users can resize windows without triggering mobile UI - ✅ Touch devices still get optimized mobile interface ## Testing - ✅ All tests pass (955 pass) - ✅ TypeScript compilation clean - ✅ ESLint checks pass - Tested responsive behavior at various viewport sizes _Generated with `cmux`_
1 parent cbad70c commit c1f6440

File tree

7 files changed

+139
-34
lines changed

7 files changed

+139
-34
lines changed

src/App.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@ function AppInner() {
618618

619619
return (
620620
<>
621-
<div className="bg-bg-dark flex h-screen overflow-hidden [@media(max-width:768px)]:flex-col">
621+
<div className="bg-bg-dark mobile-layout flex h-screen overflow-hidden">
622622
<LeftSidebar
623623
onSelectWorkspace={handleWorkspaceSwitch}
624624
onAddProject={handleAddProjectCallback}
@@ -633,8 +633,8 @@ function AppInner() {
633633
sortedWorkspacesByProject={sortedWorkspacesByProject}
634634
workspaceRecency={workspaceRecency}
635635
/>
636-
<div className="flex min-w-0 flex-1 flex-col overflow-hidden [@media(max-width:768px)]:w-full">
637-
<div className="flex flex-1 overflow-hidden [@media(max-width:768px)]:flex-col">
636+
<div className="mobile-main-content flex min-w-0 flex-1 flex-col overflow-hidden">
637+
<div className="mobile-layout flex flex-1 overflow-hidden">
638638
{selectedWorkspace ? (
639639
<ErrorBoundary
640640
workspaceInfo={`${selectedWorkspace.projectName}/${selectedWorkspace.namedWorkspacePath?.split("/").pop() ?? selectedWorkspace.workspaceId}`}

src/components/ChatInput.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -777,7 +777,6 @@ export const ChatInput: React.FC<ChatInputProps> = ({
777777
return (
778778
<div
779779
className="bg-separator border-border-light relative flex flex-col gap-1 border-t px-[15px] pt-[5px] pb-[15px]"
780-
style={{ containerType: "inline-size" }}
781780
data-component="ChatInputSection"
782781
>
783782
<ChatInputToast toast={toast} onDismiss={handleToastDismiss} />
@@ -850,14 +849,14 @@ export const ChatInput: React.FC<ChatInputProps> = ({
850849

851850
{/* Thinking Slider - hide on small viewports */}
852851
<div
853-
className="max-@[600px]:hidden flex items-center"
852+
className="flex items-center max-[550px]:hidden"
854853
data-component="ThinkingSliderGroup"
855854
>
856855
<ThinkingSliderComponent modelString={preferredModel} />
857856
</div>
858857

859858
{/* Context 1M Checkbox - hide on smaller viewports */}
860-
<div className="max-@[500px]:hidden flex items-center" data-component="Context1MGroup">
859+
<div className="flex items-center max-[450px]:hidden" data-component="Context1MGroup">
861860
<Context1MCheckbox modelString={preferredModel} />
862861
</div>
863862
{preferredModel && (
@@ -876,7 +875,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
876875
</Suspense>
877876
</div>
878877
)}
879-
<div className="max-@[700px]:hidden ml-auto flex items-center gap-1.5">
878+
<div className="ml-auto flex items-center gap-1.5 max-[550px]:hidden">
880879
<div
881880
className={cn(
882881
"flex gap-0 bg-toggle-bg rounded",

src/components/LeftSidebar.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function LeftSidebar(props: LeftSidebarProps) {
3838
title="Open sidebar"
3939
aria-label="Open sidebar menu"
4040
className={cn(
41-
"hidden max-md:flex fixed top-3 left-3 z-[998]",
41+
"hidden mobile-menu-btn fixed top-3 left-3 z-[998]",
4242
"w-10 h-10 bg-separator border border-border-light rounded-md cursor-pointer",
4343
"items-center justify-center text-foreground text-xl transition-all duration-200",
4444
"shadow-[0_2px_4px_rgba(0,0,0,0.3)]",
@@ -53,8 +53,8 @@ export function LeftSidebar(props: LeftSidebarProps) {
5353
{/* Overlay backdrop - only visible on mobile when sidebar is open */}
5454
<div
5555
className={cn(
56-
"hidden max-md:block fixed inset-0 bg-black/50 z-[999] backdrop-blur-sm",
57-
collapsed && "max-md:hidden"
56+
"hidden mobile-overlay fixed inset-0 bg-black/50 z-[999] backdrop-blur-sm",
57+
collapsed && "!hidden"
5858
)}
5959
onClick={onToggleCollapsed}
6060
/>
@@ -65,11 +65,8 @@ export function LeftSidebar(props: LeftSidebarProps) {
6565
"h-screen bg-separator border-r border-dark flex flex-col shrink-0",
6666
"transition-all duration-200 overflow-hidden relative z-[100]",
6767
collapsed ? "w-8" : "w-72",
68-
"max-md:fixed max-md:left-0 max-md:top-0 max-md:w-72 max-md:z-[1000]",
69-
"max-md:transition-transform max-md:duration-300",
70-
collapsed
71-
? "max-md:-translate-x-full max-md:shadow-none"
72-
: "max-md:translate-x-0 max-md:shadow-[2px_0_8px_rgba(0,0,0,0.5)]"
68+
"mobile-sidebar",
69+
collapsed && "mobile-sidebar-collapsed"
7370
)}
7471
>
7572
{!collapsed && <TitleBar />}

src/components/ProjectSidebar.tsx

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { HTML5Backend, getEmptyImage } from "react-dnd-html5-backend";
99
import { useDrag, useDrop, useDragLayer } from "react-dnd";
1010
import { sortProjectsByOrder, reorderProjects, normalizeOrder } from "@/utils/projectOrdering";
1111
import { matchesKeybind, formatKeybind, KEYBINDS } from "@/utils/ui/keybinds";
12-
import { abbreviatePath } from "@/utils/ui/pathAbbreviation";
12+
import { abbreviatePath, splitAbbreviatedPath } from "@/utils/ui/pathAbbreviation";
1313
import {
1414
partitionWorkspacesByAge,
1515
formatOldWorkspaceThreshold,
@@ -78,7 +78,7 @@ const DraggableProjectItemBase: React.FC<DraggableProjectItemProps> = ({
7878
<div
7979
ref={(node) => drag(drop(node))}
8080
className={cn(
81-
"py-1 px-3 flex items-center border-l-transparent transition-all duration-150 bg-separator",
81+
"py-2 px-3 flex items-center border-l-transparent transition-all duration-150 bg-separator",
8282
isDragging ? "cursor-grabbing opacity-40 [&_*]:!cursor-grabbing" : "cursor-grab",
8383
isOver && "bg-accent/[0.08]",
8484
selected && "bg-hover border-l-accent",
@@ -130,21 +130,19 @@ const ProjectDragLayer: React.FC = () => {
130130

131131
if (!isDragging || !currentOffset || !item?.projectPath) return null;
132132

133-
const name = item.projectPath.split("/").pop() ?? item.projectPath;
134133
const abbrevPath = abbreviatePath(item.projectPath);
134+
const { dirPath, basename } = splitAbbreviatedPath(abbrevPath);
135135

136136
return (
137137
<div className="pointer-events-none fixed inset-0 z-[9999] cursor-grabbing">
138138
<div style={{ transform: `translate(${currentOffset.x + 10}px, ${currentOffset.y + 10}px)` }}>
139139
<div className="bg-hover/95 text-foreground border-l-accent flex w-fit max-w-72 min-w-44 items-center rounded border-l-[3px] px-3 py-1.5 shadow-[0_6px_24px_rgba(0,0,0,0.4)]">
140140
<span className="text-dim mr-1.5 text-xs"></span>
141-
<span className="text-muted mr-2 text-[10px]"></span>
141+
<span className="text-muted mr-2 text-xs"></span>
142142
<div className="min-w-0 flex-1">
143-
<div className="text-foreground truncate text-sm font-medium tracking-[0.2px]">
144-
{name}
145-
</div>
146-
<div className="text-muted-dark font-monospace mt-0.5 truncate text-[11px]">
147-
{abbrevPath}
143+
<div className="text-muted-dark font-monospace truncate text-sm leading-none">
144+
<span>{dirPath}</span>
145+
<span className="text-foreground font-medium">{basename}</span>
148146
</div>
149147
</div>
150148
</div>
@@ -489,18 +487,26 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
489487
<span
490488
data-project-path={projectPath}
491489
aria-hidden="true"
492-
className="text-muted mr-2 shrink-0 text-[10px] transition-transform duration-200"
490+
className="text-muted mr-2 shrink-0 text-xs transition-transform duration-200"
493491
style={{ transform: isExpanded ? "rotate(90deg)" : "rotate(0deg)" }}
494492
>
495493
496494
</span>
497-
<div className="min-w-0 flex-1 pr-2">
498-
<div className="text-foreground truncate text-sm font-medium tracking-[0.2px]">
499-
{projectName}
500-
</div>
495+
<div className="flex min-w-0 flex-1 items-center pr-2">
501496
<TooltipWrapper inline>
502-
<div className="text-muted-dark font-monospace mt-px truncate text-[11px]">
503-
{abbreviatePath(projectPath)}
497+
<div className="text-muted-dark font-monospace truncate text-sm leading-none">
498+
{(() => {
499+
const abbrevPath = abbreviatePath(projectPath);
500+
const { dirPath, basename } = splitAbbreviatedPath(abbrevPath);
501+
return (
502+
<>
503+
<span>{dirPath}</span>
504+
<span className="text-foreground font-medium">
505+
{basename}
506+
</span>
507+
</>
508+
);
509+
})()}
504510
</div>
505511
<Tooltip className="tooltip" align="left">
506512
{projectPath}

src/styles/globals.css

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,8 @@ body,
256256
overflow: hidden;
257257
}
258258

259-
/* Mobile improvements */
260-
@media (max-width: 768px) {
259+
/* Mobile improvements - only apply to touch devices */
260+
@media (max-width: 768px) and (pointer: coarse) {
261261
html {
262262
-webkit-text-size-adjust: 100%;
263263
touch-action: manipulation;
@@ -273,6 +273,41 @@ body,
273273
min-height: 44px;
274274
min-width: 44px;
275275
}
276+
277+
/* Show mobile menu button only on touch devices */
278+
.mobile-menu-btn {
279+
display: flex !important;
280+
}
281+
282+
/* Show mobile overlay only on touch devices */
283+
.mobile-overlay {
284+
display: block !important;
285+
}
286+
287+
/* Mobile sidebar positioning only on touch devices */
288+
.mobile-sidebar {
289+
position: fixed !important;
290+
left: 0 !important;
291+
top: 0 !important;
292+
width: 18rem !important;
293+
z-index: 1000 !important;
294+
transition: transform 0.3s !important;
295+
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.5) !important;
296+
}
297+
298+
.mobile-sidebar-collapsed {
299+
transform: translateX(-100%) !important;
300+
box-shadow: none !important;
301+
}
302+
303+
/* Mobile layout - stack vertically on touch devices */
304+
.mobile-layout {
305+
flex-direction: column !important;
306+
}
307+
308+
.mobile-main-content {
309+
width: 100% !important;
310+
}
276311
}
277312

278313
code {

src/utils/ui/pathAbbreviation.test.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { abbreviatePath } from "./pathAbbreviation";
1+
import { abbreviatePath, splitAbbreviatedPath } from "./pathAbbreviation";
22

33
describe("abbreviatePath", () => {
44
it("should abbreviate all directory components except the last one", () => {
@@ -32,3 +32,51 @@ describe("abbreviatePath", () => {
3232
);
3333
});
3434
});
35+
36+
describe("splitAbbreviatedPath", () => {
37+
it("should split abbreviated path into directory and basename", () => {
38+
expect(splitAbbreviatedPath("/U/a/P/c/cmux")).toEqual({
39+
dirPath: "/U/a/P/c/",
40+
basename: "cmux",
41+
});
42+
});
43+
44+
it("should handle paths without leading slash", () => {
45+
expect(splitAbbreviatedPath("U/a/P/c/cmux")).toEqual({
46+
dirPath: "U/a/P/c/",
47+
basename: "cmux",
48+
});
49+
});
50+
51+
it("should handle single directory paths", () => {
52+
expect(splitAbbreviatedPath("/Users")).toEqual({
53+
dirPath: "/",
54+
basename: "Users",
55+
});
56+
expect(splitAbbreviatedPath("Users")).toEqual({
57+
dirPath: "",
58+
basename: "Users",
59+
});
60+
});
61+
62+
it("should handle root path", () => {
63+
expect(splitAbbreviatedPath("/")).toEqual({
64+
dirPath: "/",
65+
basename: "",
66+
});
67+
});
68+
69+
it("should handle empty string", () => {
70+
expect(splitAbbreviatedPath("")).toEqual({
71+
dirPath: "",
72+
basename: "",
73+
});
74+
});
75+
76+
it("should handle paths with long basenames", () => {
77+
expect(splitAbbreviatedPath("/U/a/very-long-project-name")).toEqual({
78+
dirPath: "/U/a/",
79+
basename: "very-long-project-name",
80+
});
81+
});
82+
});

src/utils/ui/pathAbbreviation.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,23 @@ export function abbreviatePath(path: string): string {
3131

3232
return abbreviated.join("/");
3333
}
34+
35+
/**
36+
* Split an abbreviated path into directory path and basename
37+
* Example: /U/a/P/c/cmux -> { dirPath: "/U/a/P/c/", basename: "cmux" }
38+
*/
39+
export function splitAbbreviatedPath(path: string): { dirPath: string; basename: string } {
40+
if (!path || typeof path !== "string") {
41+
return { dirPath: "", basename: path };
42+
}
43+
44+
const lastSlashIndex = path.lastIndexOf("/");
45+
if (lastSlashIndex === -1) {
46+
return { dirPath: "", basename: path };
47+
}
48+
49+
return {
50+
dirPath: path.slice(0, lastSlashIndex + 1), // Include the trailing slash
51+
basename: path.slice(lastSlashIndex + 1),
52+
};
53+
}

0 commit comments

Comments
 (0)