Skip to content

Add drag-and-drop project reordering to the sidebar#185

Merged
juliusmarminge merged 7 commits intomainfrom
t3code/reorder-projects-drag-drop
Mar 9, 2026
Merged

Add drag-and-drop project reordering to the sidebar#185
juliusmarminge merged 7 commits intomainfrom
t3code/reorder-projects-drag-drop

Conversation

@juliusmarminge
Copy link
Member

@juliusmarminge juliusmarminge commented Mar 6, 2026

CleanShot.2026-03-09.at.14.15.02.mp4

Summary

  • add project drag-and-drop reordering in the sidebar using @dnd-kit (core, sortable, modifiers, utilities)
  • introduce sortable project item wiring and vertical-only drag constraints
  • prevent accidental project toggle clicks during drag gestures via pointer-move/click suppression logic
  • keep existing project/thread interactions (context menu, new thread action, keyboard toggle) while moving from Collapsible to inline animated expand/collapse layout
  • extend store logic and tests to cover project reordering behavior

Testing

  • Updated unit tests in apps/web/src/store.test.ts for project reorder behavior
  • Not run: bun lint
  • Not run: bun typecheck
  • Not run: bun run test

Note

Medium Risk
Moderate UI/state change that adds new drag interactions and persists project ordering in localStorage, which could impact sidebar behavior and read-model sync ordering if edge cases are missed.

Overview
Sidebar projects can now be reordered via drag-and-drop. The sidebar wraps each project in a @dnd-kit sortable context (vertical-only, parent-bounded) and adds click/keyboard suppression logic to prevent accidental expand/collapse toggles during drag gestures.

Project ordering is now stateful and persisted. The Zustand store adds reorderProjects, persists projectOrderCwds alongside expanded state, and updates read-model syncing to preserve existing (or persisted) project order when server updates arrive; tests were extended to cover both reorder and sync-order preservation.

Small UI plumbing updates include a hideScrollbars option on ScrollArea (used by SidebarContent), a Collapsible panel animation tweak, and minor build tooling adjustments (Vite override / config cleanup).

Written by Cursor Bugbot for commit 2c6bbc3. This will update automatically on new commits. Configure here.

Note

Add drag-and-drop project reordering to the sidebar

  • Wraps the sidebar project list in DndContext and SortableContext from @dnd-kit, with a new SortableProjectItem component that applies transform/transition styles and drag handle props during drag interactions.
  • Adds reorderProjects to the Zustand store as a pure function that moves a project to a target index; project order is persisted to and restored from localStorage.
  • When syncing the server read model, the existing in-memory order is preserved; if none exists, the persisted localStorage order is used; otherwise new projects are appended in incoming order.
  • Replaces CollapsibleTrigger with a SidebarMenuButton that handles click, keyboard (Enter/Space), and context menu events directly, with click suppression immediately after a drag to prevent unintended toggles.
  • Risk: project order is now stored in localStorage under a new projectOrderCwds key; users who clear storage will lose their custom ordering.

Macroscope summarized 2c6bbc3.

@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 82de4b9a-1b6e-4109-9149-c152af40ab9f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch t3code/reorder-projects-drag-drop

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Sortable wrapper div creates invalid HTML in list
    • Changed SortableProjectItem to render a SidebarMenuItem (
    • ) instead of a
      , and removed the redundant inner SidebarMenuItem wrapper, producing valid
      HTML.

Create PR

Or push these changes by commenting:

@cursor push 410550ecbe
Preview (410550ecbe)
diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx
--- a/apps/web/src/components/Sidebar.tsx
+++ b/apps/web/src/components/Sidebar.tsx
@@ -273,15 +273,17 @@
 
 function SortableProjectItem({
   projectId,
+  className,
   children,
 }: {
   projectId: ProjectId;
+  className?: string;
   children: (handleProps: SortableProjectHandleProps) => React.ReactNode;
 }) {
   const { attributes, listeners, setNodeRef, transform, transition, isDragging, isOver } =
     useSortable({ id: projectId });
   return (
-    <div
+    <SidebarMenuItem
       ref={setNodeRef}
       style={{
         transform: CSS.Translate.toString(transform),
@@ -289,10 +291,10 @@
       }}
       className={`rounded-md ${
         isDragging ? "z-20 opacity-80" : ""
-      } ${isOver && !isDragging ? "ring-1 ring-primary/40" : ""}`}
+      } ${isOver && !isDragging ? "ring-1 ring-primary/40" : ""} ${className ?? ""}`}
     >
       {children({ attributes, listeners })}
-    </div>
+    </SidebarMenuItem>
   );
 }
 
@@ -1205,9 +1207,9 @@
                       : projectThreads;
 
                   return (
-                    <SortableProjectItem key={project.id} projectId={project.id}>
+                    <SortableProjectItem key={project.id} projectId={project.id} className="group/collapsible">
                       {(dragHandleProps) => (
-                        <SidebarMenuItem className="group/collapsible">
+                        <>
                           <div className="group/project-header relative">
                             <SidebarMenuButton
                               size="sm"
@@ -1448,7 +1450,7 @@
                               </SidebarMenuSub>
                             </div>
                           </div>
-                        </SidebarMenuItem>
+                        </>
                       )}
                     </SortableProjectItem>
                   );

@vercel
Copy link

vercel bot commented Mar 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
t3code-marketing Error Error Mar 6, 2026 10:50pm

Request Review

@github-actions github-actions bot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Mar 9, 2026
- wire project list to dnd-kit sortable with vertical drag constraints
- prevent accidental expand/collapse clicks during drag gestures
- update store logic/tests and add dnd-kit dependencies
- prevent accidental project toggles after drag operations
- switch project rows to Collapsible for stable expand/collapse animation
- add optional hidden scrollbar mode to ScrollArea and apply it in SidebarContent
- Reset post-drag click suppression when drag is canceled
- Consume only the synthetic click after drag release, then clear suppression
@juliusmarminge juliusmarminge force-pushed the t3code/reorder-projects-drag-drop branch from 28df73c to f32db73 Compare March 9, 2026 21:11
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Ambiguous operator precedence in fallback order calculation
    • Added explicit outer parentheses around the fallback arithmetic expression to make the precedence between ?? and + unambiguous.

Create PR

Or push these changes by commenting:

@cursor push 148d455f06
Preview (148d455f06)
diff --git a/apps/web/src/store.ts b/apps/web/src/store.ts
--- a/apps/web/src/store.ts
+++ b/apps/web/src/store.ts
@@ -155,7 +155,7 @@
       const orderIndex =
         previousIndex ??
         persistedIndex ??
-        (usePersistedOrder ? persistedProjectOrderCwds.length : previous.length) + incomingIndex;
+        ((usePersistedOrder ? persistedProjectOrderCwds.length : previous.length) + incomingIndex);
       return { project, incomingIndex, orderIndex };
     })
     .toSorted((a, b) => {

const orderIndex =
previousIndex ??
persistedIndex ??
(usePersistedOrder ? persistedProjectOrderCwds.length : previous.length) + incomingIndex;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ambiguous operator precedence in fallback order calculation

Low Severity

The orderIndex fallback expression relies on + having higher precedence than ?? without explicit parentheses. While the current behavior is correct (the addition only applies when both previousIndex and persistedIndex are nullish), a reader could easily misinterpret this as adding incomingIndex to the result of the entire ?? chain. Wrapping the fallback arithmetic in parentheses would make the intent unambiguous and prevent accidental breakage if the expression is modified later.

Fix in Cursor Fix in Web

- Prefer pointer-based hit testing during project reordering
- Fall back to corner-based collision to make drop targeting more reliable
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Drag cancel doesn't suppress synthetic click after release
    • Removed the line that reset suppressProjectClickAfterDragRef to false in handleProjectDragCancel, so the flag stays true and suppresses the synthetic click fired on pointer release after a cancelled drag, matching handleProjectDragEnd's behavior.

Create PR

Or push these changes by commenting:

@cursor push 13c88d8bd1
Preview (13c88d8bd1)
diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx
--- a/apps/web/src/components/Sidebar.tsx
+++ b/apps/web/src/components/Sidebar.tsx
@@ -872,7 +872,6 @@
 
   const handleProjectDragCancel = useCallback((_event: DragCancelEvent) => {
     dragInProgressRef.current = false;
-    suppressProjectClickAfterDragRef.current = false;
   }, []);
 
   const handleProjectTitlePointerDownCapture = useCallback(() => {

@juliusmarminge
Copy link
Member Author

@cursor push 13c88d8

…vent unintended toggle

Co-authored-by: Julius Marminge <juliusmarminge@users.noreply.github.com>

Applied via @cursor push command
@juliusmarminge juliusmarminge merged commit 9fb9467 into main Mar 9, 2026
8 checks passed
@juliusmarminge juliusmarminge deleted the t3code/reorder-projects-drag-drop branch March 9, 2026 22:08
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue.

const orderIndex =
previousIndex ??
persistedIndex ??
(usePersistedOrder ? persistedProjectOrderCwds.length : previous.length) + incomingIndex;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Operator precedence bug in order index fallback calculation

Low Severity

The + operator has higher precedence than the ternary ?:, so the fallback orderIndex expression is parsed as usePersistedOrder ? persistedProjectOrderCwds.length : (previous.length + incomingIndex) instead of the likely intended (usePersistedOrder ? persistedProjectOrderCwds.length : previous.length) + incomingIndex. When usePersistedOrder is true, incomingIndex is not added, so all new projects receive the same orderIndex. The sort tiebreaker on incomingIndex masks this today, but the asymmetry is almost certainly unintentional and fragile.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants