Skip to content

Commit cb99a07

Browse files
authored
🤖 fix: bump newly added projects to top (#1134)
- New projects are treated as most-recent and appear at the top of the Projects list - Drag preview now reuses the same base row styling for better cohesion _Generated with `mux`_
1 parent f5aef11 commit cb99a07

File tree

3 files changed

+39
-21
lines changed

3 files changed

+39
-21
lines changed

src/browser/components/ProjectSidebar.tsx

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,23 @@ export type { WorkspaceSelection } from "./WorkspaceListItem";
3636
// Draggable project item moved to module scope to avoid remounting on every parent render.
3737
// Defining components inside another component causes a new function identity each render,
3838
// which forces React to unmount/remount the subtree. That led to hover flicker and high CPU.
39+
40+
const PROJECT_ITEM_BASE_CLASS =
41+
"py-2 px-3 flex items-center border-l-transparent bg-sidebar transition-colors duration-150";
42+
43+
function getProjectItemClassName(opts: {
44+
isDragging: boolean;
45+
isOver: boolean;
46+
selected: boolean;
47+
}): string {
48+
return cn(
49+
PROJECT_ITEM_BASE_CLASS,
50+
opts.isDragging ? "cursor-grabbing opacity-35 [&_*]:!cursor-grabbing" : "cursor-grab",
51+
opts.isOver && "bg-accent/[0.08]",
52+
opts.selected && "bg-hover border-l-accent",
53+
"hover:[&_button]:opacity-100 hover:[&_[data-drag-handle]]:opacity-100"
54+
);
55+
}
3956
type DraggableProjectItemProps = React.PropsWithChildren<{
4057
projectPath: string;
4158
onReorder: (draggedPath: string, targetPath: string) => void;
@@ -87,13 +104,11 @@ const DraggableProjectItemBase: React.FC<DraggableProjectItemProps> = ({
87104
return (
88105
<div
89106
ref={(node) => drag(drop(node))}
90-
className={cn(
91-
"py-2 px-3 flex items-center border-l-transparent transition-all duration-150 bg-sidebar",
92-
isDragging ? "cursor-grabbing opacity-40 [&_*]:!cursor-grabbing" : "cursor-grab",
93-
isOver && "bg-accent/[0.08]",
94-
selected && "bg-hover border-l-accent",
95-
"hover:[&_button]:opacity-100 hover:[&_[data-drag-handle]]:opacity-100"
96-
)}
107+
className={getProjectItemClassName({
108+
isDragging,
109+
isOver,
110+
selected: !!selected,
111+
})}
97112
{...rest}
98113
>
99114
{children}
@@ -141,18 +156,17 @@ const ProjectDragLayer: React.FC = () => {
141156
if (!isDragging || !currentOffset || !item?.projectPath) return null;
142157

143158
const abbrevPath = PlatformPaths.abbreviate(item.projectPath);
144-
const { dirPath, basename } = PlatformPaths.splitAbbreviated(abbrevPath);
159+
const { basename } = PlatformPaths.splitAbbreviated(abbrevPath);
145160

146161
return (
147162
<div className="pointer-events-none fixed inset-0 z-[9999] cursor-grabbing">
148163
<div style={{ transform: `translate(${currentOffset.x + 10}px, ${currentOffset.y + 10}px)` }}>
149-
<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)]">
150-
<span className="text-muted mr-2 text-xs"></span>
151-
<div className="min-w-0 flex-1">
152-
<div className="text-muted-dark font-monospace truncate text-sm leading-tight">
153-
<span>{dirPath}</span>
154-
<span className="text-foreground font-medium">{basename}</span>
155-
</div>
164+
<div className={cn(PROJECT_ITEM_BASE_CLASS, "w-fit max-w-64 rounded-sm shadow-lg")}>
165+
<span className="text-secondary mr-2 flex h-5 w-5 shrink-0 items-center justify-center">
166+
<ChevronRight size={12} />
167+
</span>
168+
<div className="flex min-w-0 flex-1 items-center pr-2">
169+
<span className="text-foreground truncate text-sm font-medium">{basename}</span>
156170
</div>
157171
</div>
158172
</div>

src/common/utils/projectOrdering.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,11 @@ describe("projectOrdering", () => {
6969
expect(result).toEqual(["/a", "/b"]);
7070
});
7171

72-
it("appends new projects to the end", () => {
72+
it("prepends new projects to the front", () => {
7373
const projects = createProjects(["/a", "/b", "/c", "/d"]);
7474
const order = ["/b", "/a"];
7575
const result = normalizeOrder(order, projects);
76-
expect(result).toEqual(["/b", "/a", "/c", "/d"]);
76+
expect(result).toEqual(["/c", "/d", "/b", "/a"]);
7777
});
7878

7979
it("preserves order of existing projects", () => {
@@ -130,13 +130,13 @@ describe("projectOrdering", () => {
130130
// After projects load, normalization should work normally:
131131
// 1. projectOrder is still ["/a", "/b", "/c"] from localStorage
132132
// 2. Projects are now loaded with an additional project ["/d"]
133-
// 3. Normalization should append the new project
133+
// 3. Normalization should treat the new project as "most recent" and put it first
134134
const projectOrder = ["/a", "/b", "/c"];
135135
const loadedProjects = createProjects(["/a", "/b", "/c", "/d"]);
136136

137137
const result = normalizeOrder(projectOrder, loadedProjects);
138138

139-
expect(result).toEqual(["/a", "/b", "/c", "/d"]);
139+
expect(result).toEqual(["/d", "/a", "/b", "/c"]);
140140
});
141141
});
142142
});

src/common/utils/projectOrdering.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,17 @@ export function reorderProjects(
5656
/**
5757
* Normalize an order array against the current set of projects.
5858
* - Removes paths that no longer exist
59-
* - Appends new paths to the end (preserving their natural order)
59+
* - Prepends new paths to the front (preserving their natural order)
60+
*
61+
* UX rationale: when a user adds a project, they almost always want to use it next. Putting newly
62+
* added projects at the top makes that action feel "recent" even if the user already has a custom
63+
* project ordering.
6064
*/
6165
export function normalizeOrder(order: string[], projects: Map<string, ProjectConfig>): string[] {
6266
const present = new Set(projects.keys());
6367
const filtered = order.filter((p) => present.has(p));
6468
const missing = Array.from(projects.keys()).filter((p) => !filtered.includes(p));
65-
return [...filtered, ...missing];
69+
return [...missing, ...filtered];
6670
}
6771

6872
/**

0 commit comments

Comments
 (0)