Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ function AppInner() {

return (
<>
<div className="bg-bg-dark flex h-screen overflow-hidden [@media(max-width:768px)]:flex-col">
<div className="bg-bg-dark mobile-layout flex h-screen overflow-hidden">
<LeftSidebar
onSelectWorkspace={handleWorkspaceSwitch}
onAddProject={handleAddProjectCallback}
Expand All @@ -633,8 +633,8 @@ function AppInner() {
sortedWorkspacesByProject={sortedWorkspacesByProject}
workspaceRecency={workspaceRecency}
/>
<div className="flex min-w-0 flex-1 flex-col overflow-hidden [@media(max-width:768px)]:w-full">
<div className="flex flex-1 overflow-hidden [@media(max-width:768px)]:flex-col">
<div className="mobile-main-content flex min-w-0 flex-1 flex-col overflow-hidden">
<div className="mobile-layout flex flex-1 overflow-hidden">
{selectedWorkspace ? (
<ErrorBoundary
workspaceInfo={`${selectedWorkspace.projectName}/${selectedWorkspace.namedWorkspacePath?.split("/").pop() ?? selectedWorkspace.workspaceId}`}
Expand Down
7 changes: 3 additions & 4 deletions src/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -777,7 +777,6 @@ export const ChatInput: React.FC<ChatInputProps> = ({
return (
<div
className="bg-separator border-border-light relative flex flex-col gap-1 border-t px-[15px] pt-[5px] pb-[15px]"
style={{ containerType: "inline-size" }}
data-component="ChatInputSection"
>
<ChatInputToast toast={toast} onDismiss={handleToastDismiss} />
Expand Down Expand Up @@ -850,14 +849,14 @@ export const ChatInput: React.FC<ChatInputProps> = ({

{/* Thinking Slider - hide on small viewports */}
<div
className="max-@[600px]:hidden flex items-center"
className="flex items-center max-[550px]:hidden"
data-component="ThinkingSliderGroup"
>
<ThinkingSliderComponent modelString={preferredModel} />
</div>

{/* Context 1M Checkbox - hide on smaller viewports */}
<div className="max-@[500px]:hidden flex items-center" data-component="Context1MGroup">
<div className="flex items-center max-[450px]:hidden" data-component="Context1MGroup">
<Context1MCheckbox modelString={preferredModel} />
</div>
{preferredModel && (
Expand All @@ -876,7 +875,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
</Suspense>
</div>
)}
<div className="max-@[700px]:hidden ml-auto flex items-center gap-1.5">
<div className="ml-auto flex items-center gap-1.5 max-[550px]:hidden">
<div
className={cn(
"flex gap-0 bg-toggle-bg rounded",
Expand Down
13 changes: 5 additions & 8 deletions src/components/LeftSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function LeftSidebar(props: LeftSidebarProps) {
title="Open sidebar"
aria-label="Open sidebar menu"
className={cn(
"hidden max-md:flex fixed top-3 left-3 z-[998]",
"hidden mobile-menu-btn fixed top-3 left-3 z-[998]",
"w-10 h-10 bg-separator border border-border-light rounded-md cursor-pointer",
"items-center justify-center text-foreground text-xl transition-all duration-200",
"shadow-[0_2px_4px_rgba(0,0,0,0.3)]",
Expand All @@ -53,8 +53,8 @@ export function LeftSidebar(props: LeftSidebarProps) {
{/* Overlay backdrop - only visible on mobile when sidebar is open */}
<div
className={cn(
"hidden max-md:block fixed inset-0 bg-black/50 z-[999] backdrop-blur-sm",
collapsed && "max-md:hidden"
"hidden mobile-overlay fixed inset-0 bg-black/50 z-[999] backdrop-blur-sm",
collapsed && "!hidden"
)}
onClick={onToggleCollapsed}
/>
Expand All @@ -65,11 +65,8 @@ export function LeftSidebar(props: LeftSidebarProps) {
"h-screen bg-separator border-r border-dark flex flex-col shrink-0",
"transition-all duration-200 overflow-hidden relative z-[100]",
collapsed ? "w-8" : "w-72",
"max-md:fixed max-md:left-0 max-md:top-0 max-md:w-72 max-md:z-[1000]",
"max-md:transition-transform max-md:duration-300",
collapsed
? "max-md:-translate-x-full max-md:shadow-none"
: "max-md:translate-x-0 max-md:shadow-[2px_0_8px_rgba(0,0,0,0.5)]"
"mobile-sidebar",
collapsed && "mobile-sidebar-collapsed"
)}
>
{!collapsed && <TitleBar />}
Expand Down
38 changes: 22 additions & 16 deletions src/components/ProjectSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { HTML5Backend, getEmptyImage } from "react-dnd-html5-backend";
import { useDrag, useDrop, useDragLayer } from "react-dnd";
import { sortProjectsByOrder, reorderProjects, normalizeOrder } from "@/utils/projectOrdering";
import { matchesKeybind, formatKeybind, KEYBINDS } from "@/utils/ui/keybinds";
import { abbreviatePath } from "@/utils/ui/pathAbbreviation";
import { abbreviatePath, splitAbbreviatedPath } from "@/utils/ui/pathAbbreviation";
import {
partitionWorkspacesByAge,
formatOldWorkspaceThreshold,
Expand Down Expand Up @@ -78,7 +78,7 @@ const DraggableProjectItemBase: React.FC<DraggableProjectItemProps> = ({
<div
ref={(node) => drag(drop(node))}
className={cn(
"py-1 px-3 flex items-center border-l-transparent transition-all duration-150 bg-separator",
"py-2 px-3 flex items-center border-l-transparent transition-all duration-150 bg-separator",
isDragging ? "cursor-grabbing opacity-40 [&_*]:!cursor-grabbing" : "cursor-grab",
isOver && "bg-accent/[0.08]",
selected && "bg-hover border-l-accent",
Expand Down Expand Up @@ -130,21 +130,19 @@ const ProjectDragLayer: React.FC = () => {

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

const name = item.projectPath.split("/").pop() ?? item.projectPath;
const abbrevPath = abbreviatePath(item.projectPath);
const { dirPath, basename } = splitAbbreviatedPath(abbrevPath);

return (
<div className="pointer-events-none fixed inset-0 z-[9999] cursor-grabbing">
<div style={{ transform: `translate(${currentOffset.x + 10}px, ${currentOffset.y + 10}px)` }}>
<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)]">
<span className="text-dim mr-1.5 text-xs">β Ώ</span>
<span className="text-muted mr-2 text-[10px]">β–Ά</span>
<span className="text-muted mr-2 text-xs">β–Ά</span>
<div className="min-w-0 flex-1">
<div className="text-foreground truncate text-sm font-medium tracking-[0.2px]">
{name}
</div>
<div className="text-muted-dark font-monospace mt-0.5 truncate text-[11px]">
{abbrevPath}
<div className="text-muted-dark font-monospace truncate text-sm leading-none">
<span>{dirPath}</span>
<span className="text-foreground font-medium">{basename}</span>
</div>
</div>
</div>
Expand Down Expand Up @@ -489,18 +487,26 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
<span
data-project-path={projectPath}
aria-hidden="true"
className="text-muted mr-2 shrink-0 text-[10px] transition-transform duration-200"
className="text-muted mr-2 shrink-0 text-xs transition-transform duration-200"
style={{ transform: isExpanded ? "rotate(90deg)" : "rotate(0deg)" }}
>
β–Ά
</span>
<div className="min-w-0 flex-1 pr-2">
<div className="text-foreground truncate text-sm font-medium tracking-[0.2px]">
{projectName}
</div>
<div className="flex min-w-0 flex-1 items-center pr-2">
<TooltipWrapper inline>
<div className="text-muted-dark font-monospace mt-px truncate text-[11px]">
{abbreviatePath(projectPath)}
<div className="text-muted-dark font-monospace truncate text-sm leading-none">
{(() => {
const abbrevPath = abbreviatePath(projectPath);
const { dirPath, basename } = splitAbbreviatedPath(abbrevPath);
return (
<>
<span>{dirPath}</span>
<span className="text-foreground font-medium">
{basename}
</span>
</>
);
})()}
</div>
<Tooltip className="tooltip" align="left">
{projectPath}
Expand Down
39 changes: 37 additions & 2 deletions src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,8 @@ body,
overflow: hidden;
}

/* Mobile improvements */
@media (max-width: 768px) {
/* Mobile improvements - only apply to touch devices */
@media (max-width: 768px) and (pointer: coarse) {
html {
-webkit-text-size-adjust: 100%;
touch-action: manipulation;
Expand All @@ -273,6 +273,41 @@ body,
min-height: 44px;
min-width: 44px;
}

/* Show mobile menu button only on touch devices */
.mobile-menu-btn {
display: flex !important;
}

/* Show mobile overlay only on touch devices */
.mobile-overlay {
display: block !important;
}

/* Mobile sidebar positioning only on touch devices */
.mobile-sidebar {
position: fixed !important;
left: 0 !important;
top: 0 !important;
width: 18rem !important;
z-index: 1000 !important;
transition: transform 0.3s !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.5) !important;
}

.mobile-sidebar-collapsed {
transform: translateX(-100%) !important;
box-shadow: none !important;
}

/* Mobile layout - stack vertically on touch devices */
.mobile-layout {
flex-direction: column !important;
}

.mobile-main-content {
width: 100% !important;
}
}

code {
Expand Down
50 changes: 49 additions & 1 deletion src/utils/ui/pathAbbreviation.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { abbreviatePath } from "./pathAbbreviation";
import { abbreviatePath, splitAbbreviatedPath } from "./pathAbbreviation";

describe("abbreviatePath", () => {
it("should abbreviate all directory components except the last one", () => {
Expand Down Expand Up @@ -32,3 +32,51 @@ describe("abbreviatePath", () => {
);
});
});

describe("splitAbbreviatedPath", () => {
it("should split abbreviated path into directory and basename", () => {
expect(splitAbbreviatedPath("/U/a/P/c/cmux")).toEqual({
dirPath: "/U/a/P/c/",
basename: "cmux",
});
});

it("should handle paths without leading slash", () => {
expect(splitAbbreviatedPath("U/a/P/c/cmux")).toEqual({
dirPath: "U/a/P/c/",
basename: "cmux",
});
});

it("should handle single directory paths", () => {
expect(splitAbbreviatedPath("/Users")).toEqual({
dirPath: "/",
basename: "Users",
});
expect(splitAbbreviatedPath("Users")).toEqual({
dirPath: "",
basename: "Users",
});
});

it("should handle root path", () => {
expect(splitAbbreviatedPath("/")).toEqual({
dirPath: "/",
basename: "",
});
});

it("should handle empty string", () => {
expect(splitAbbreviatedPath("")).toEqual({
dirPath: "",
basename: "",
});
});

it("should handle paths with long basenames", () => {
expect(splitAbbreviatedPath("/U/a/very-long-project-name")).toEqual({
dirPath: "/U/a/",
basename: "very-long-project-name",
});
});
});
20 changes: 20 additions & 0 deletions src/utils/ui/pathAbbreviation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,23 @@ export function abbreviatePath(path: string): string {

return abbreviated.join("/");
}

/**
* Split an abbreviated path into directory path and basename
* Example: /U/a/P/c/cmux -> { dirPath: "/U/a/P/c/", basename: "cmux" }
*/
export function splitAbbreviatedPath(path: string): { dirPath: string; basename: string } {
if (!path || typeof path !== "string") {
return { dirPath: "", basename: path };
}

const lastSlashIndex = path.lastIndexOf("/");
if (lastSlashIndex === -1) {
return { dirPath: "", basename: path };
}

return {
dirPath: path.slice(0, lastSlashIndex + 1), // Include the trailing slash
basename: path.slice(lastSlashIndex + 1),
};
}