From 67b70e7bfc6565ef25301756aa91abc50dce3cab Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 6 Sep 2026 20:14:50 -0700 Subject: [PATCH 1/5] fix(web): use plain sidebar drag dividers --- apps/web/src/components/Sidebar.tsx | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index deb00e7181c7..f07c11f31ee4 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -571,11 +571,18 @@ function SidebarSectionPlaceholder(props: { {props.showHint ? (
- {props.label} + {props.label} +
) : null} @@ -599,13 +606,11 @@ function SidebarDragBoundary(props: { className="pointer-events-none relative mx-0.5 h-0" > {props.visible ? ( -
+
{props.label} From 6c9ed4e6d6547b60b576af99c6a707a736c5a7ee Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 6 Sep 2026 20:20:48 -0700 Subject: [PATCH 2/5] fix(web): show each sidebar section divider only once --- apps/web/src/components/Sidebar.tsx | 33 +++-------------------------- 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index f07c11f31ee4..97bd2ec92535 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -554,38 +554,17 @@ function SortableSidebarMarker(props: { ); } -// Empty targets stay measurable without reserving space at rest. The sorting -// strategy opens their hint space during a drag. +// Empty targets stay measurable without reserving space at rest. During a +// drag their section header supplies the label; the target only opens a slot. function SidebarSectionPlaceholder(props: { marker: "active-placeholder" | "settled-placeholder"; - label: string; - showHint: boolean; - isDropTarget: boolean; }) { return ( - {props.showHint ? ( -
- {props.label} - -
- ) : null} -
+ /> ); } @@ -4809,9 +4788,6 @@ export default function Sidebar() { , ); break; @@ -4856,9 +4832,6 @@ export default function Sidebar() { , ); break; From 8a7bd35a5e1fed13b7d91db0dc86fe67a3c59b69 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 6 Sep 2026 20:22:16 -0700 Subject: [PATCH 3/5] fix(web): restore dashed empty sidebar drop targets --- apps/web/src/components/Sidebar.tsx | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 97bd2ec92535..c85aaef294a1 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -554,17 +554,31 @@ function SortableSidebarMarker(props: { ); } -// Empty targets stay measurable without reserving space at rest. During a -// drag their section header supplies the label; the target only opens a slot. +// Empty targets stay measurable without reserving space at rest. The sorting +// strategy opens their hint space during a drag. function SidebarSectionPlaceholder(props: { marker: "active-placeholder" | "settled-placeholder"; + label: string; + showHint: boolean; + isDropTarget: boolean; }) { return ( + > + {props.showHint ? ( +
+ {props.label} +
+ ) : null} +
); } @@ -4788,6 +4802,9 @@ export default function Sidebar() { , ); break; @@ -4832,6 +4849,9 @@ export default function Sidebar() { , ); break; From 4fee3ec6e117b2507ea3f5abbc8bdf468e099247 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 6 Sep 2026 20:24:59 -0700 Subject: [PATCH 4/5] fix(web): reveal empty targets as dragged threads leave sections --- apps/web/src/components/Sidebar.drag.test.ts | 25 ++++++++++++++++ apps/web/src/components/Sidebar.tsx | 30 +++++++++++++------- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/apps/web/src/components/Sidebar.drag.test.ts b/apps/web/src/components/Sidebar.drag.test.ts index 5c8c95f34400..1058dfcd8859 100644 --- a/apps/web/src/components/Sidebar.drag.test.ts +++ b/apps/web/src/components/Sidebar.drag.test.ts @@ -622,6 +622,31 @@ describe("sidebar drag projection", () => { expect(strategy({ ...smaller, index: 2 })?.y).toBe(-62.5); }); + it.each(["active", "settled"] as const)( + "reveals the mounted empty %s target when its last row leaves and hides it on return", + (section) => { + const items = [ + pinnedHeader, + thread("p", "pinned"), + divider, + marker("active-placeholder"), + thread("a", "active"), + settledHeader, + marker("settled-placeholder"), + thread("s", "settled"), + ]; + const active = section === "active" ? "a" : "s"; + const input = { items, settledOrder: ["s"], settledExpanded: true }; + const placeholderId = sidebarMarkerId(`${section}-placeholder`); + const resting = preview(input, active, active); + expect(resting.get(placeholderId)?.scaleY).toBe(0); + const leaving = preview(input, active, sidebarMarkerId("pinned-header")); + expect(leaving.get(placeholderId)?.scaleY).toBe(1); + const returning = preview(input, active, active); + expect(returning.get(placeholderId)?.scaleY).toBe(0); + }, + ); + it("uses shelf height for empty target sizing when card height differs from its default", () => { const items = [ pinnedHeader, diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index c85aaef294a1..5af7acd20d2f 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -633,10 +633,10 @@ function SidebarSectionHeader(props: { }) { const snoozed = props.marker === "snoozed-header"; const className = cn( - "flex h-full w-full items-center gap-2 rounded-md border border-dashed border-transparent px-2 text-left text-xs font-medium", + "flex h-full w-full items-center gap-2 px-2 text-left text-xs font-medium", snoozed ? "text-blue-600 dark:text-blue-400" : "text-sidebar-muted-foreground/60", props.dragging && "text-sidebar-foreground/80", - props.isDropTarget && "border-primary/40 bg-primary/5 text-primary", + props.isDropTarget && "text-primary", ); const content = ( <> @@ -3259,9 +3259,7 @@ export default function Sidebar() { items.push(...pinnedRows); items.push({ kind: "marker", marker: "pinned-divider" }); const activeRows = rowsOf(activeThreads, "active"); - if (activeRows.length === 0) { - items.push({ kind: "marker", marker: "active-placeholder" }); - } + items.push({ kind: "marker", marker: "active-placeholder" }); items.push(...activeRows); if (snoozedThreads.length > 0) { items.push({ kind: "marker", marker: "snoozed-header" }); @@ -3269,9 +3267,7 @@ export default function Sidebar() { } items.push({ kind: "marker", marker: "settled-header" }); const settledRows = rowsOf(renderedSettledThreads, "settled"); - if (settledRows.length === 0) { - items.push({ kind: "marker", marker: "settled-placeholder" }); - } + items.push({ kind: "marker", marker: "settled-placeholder" }); items.push(...settledRows); return items; }, [ @@ -4803,7 +4799,14 @@ export default function Sidebar() { key="active-placeholder" marker="active-placeholder" label="Active" - showHint={from !== null} + showHint={ + from !== null && + (activeThreads.length === 0 || + (from === "active" && + activeThreads.length === 1 && + dragTargetSection !== null && + dragTargetSection !== "active")) + } isDropTarget={dragTargetSection === "active"} />, ); @@ -4850,7 +4853,14 @@ export default function Sidebar() { key="settled-placeholder" marker="settled-placeholder" label="Settled" - showHint={from !== null && from !== "settled"} + showHint={ + from !== null && + (renderedSettledThreads.length === 0 || + (from === "settled" && + renderedSettledThreads.length === 1 && + dragTargetSection !== null && + dragTargetSection !== "settled")) + } isDropTarget={dragTargetSection === "settled"} />, ); From 1c0d4e8a968ab9d903252654e8bda1cbb7a6fd69 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 6 Sep 2026 20:27:46 -0700 Subject: [PATCH 5/5] fix(web): reveal empty drop targets without sliding --- apps/web/src/components/Sidebar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 5af7acd20d2f..5df4a9ea8b8b 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -545,7 +545,8 @@ function SortableSidebarMarker(props: { className={cn("list-none", props.className)} style={{ transform: CSS.Translate.toString(transform), - transition, + // A newly revealed target must not slide from its hidden position. + transition: props.marker.endsWith("-placeholder") ? "none" : transition, visibility: transform?.scaleY === 0 ? "hidden" : undefined, }} >