authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-06 20:36:19-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-06 20:36:19-07:00
log490eb17d31d3037930d0e6b549393e9dbcebde60
treef022c2da1c082146ab0a5d6269b59860fe6cd116
parentf5fb056d248be557e14358d08525361eca047e01
signature Signed by PGP key B5690EEEBB952194

fix(web): clarify sidebar drag dividers and empty targets (#10464)


2 files changed, 50 insertions(+), 16 deletions(-)

apps/web/src/components/Sidebar.drag.test.ts+25
...@@ -622,6 +622,31 @@ describe("sidebar drag projection", () => {...@@ -622,6 +622,31 @@ describe("sidebar drag projection", () => {
622 expect(strategy({ ...smaller, index: 2 })?.y).toBe(-62.5);622 expect(strategy({ ...smaller, index: 2 })?.y).toBe(-62.5);
623 });623 });
624624
625 it.each(["active", "settled"] as const)(
626 "reveals the mounted empty %s target when its last row leaves and hides it on return",
627 (section) => {
628 const items = [
629 pinnedHeader,
630 thread("p", "pinned"),
631 divider,
632 marker("active-placeholder"),
633 thread("a", "active"),
634 settledHeader,
635 marker("settled-placeholder"),
636 thread("s", "settled"),
637 ];
638 const active = section === "active" ? "a" : "s";
639 const input = { items, settledOrder: ["s"], settledExpanded: true };
640 const placeholderId = sidebarMarkerId(`${section}-placeholder`);
641 const resting = preview(input, active, active);
642 expect(resting.get(placeholderId)?.scaleY).toBe(0);
643 const leaving = preview(input, active, sidebarMarkerId("pinned-header"));
644 expect(leaving.get(placeholderId)?.scaleY).toBe(1);
645 const returning = preview(input, active, active);
646 expect(returning.get(placeholderId)?.scaleY).toBe(0);
647 },
648 );
649
625 it("uses shelf height for empty target sizing when card height differs from its default", () => {650 it("uses shelf height for empty target sizing when card height differs from its default", () => {
626 const items = [651 const items = [
627 pinnedHeader,652 pinnedHeader,
apps/web/src/components/Sidebar.tsx+25-16
...@@ -545,7 +545,8 @@ function SortableSidebarMarker(props: {...@@ -545,7 +545,8 @@ function SortableSidebarMarker(props: {
545 className={cn("list-none", props.className)}545 className={cn("list-none", props.className)}
546 style={{546 style={{
547 transform: CSS.Translate.toString(transform),547 transform: CSS.Translate.toString(transform),
548 transition,548 // A newly revealed target must not slide from its hidden position.
549 transition: props.marker.endsWith("-placeholder") ? "none" : transition,
549 visibility: transform?.scaleY === 0 ? "hidden" : undefined,550 visibility: transform?.scaleY === 0 ? "hidden" : undefined,
550 }}551 }}
551 >552 >
...@@ -599,13 +600,11 @@ function SidebarDragBoundary(props: {...@@ -599,13 +600,11 @@ function SidebarDragBoundary(props: {
599 className="pointer-events-none relative mx-0.5 h-0"600 className="pointer-events-none relative mx-0.5 h-0"
600 >601 >
601 {props.visible ? (602 {props.visible ? (
602 <div className="sidebar-drag-boundary-label absolute inset-x-2 top-1 flex h-4 items-center gap-1.5">603 <div className="sidebar-drag-boundary-label absolute inset-x-2 top-1 flex h-4 items-center gap-2">
603 <span604 <span
604 className={cn(605 className={cn(
605 "inline-flex h-4 shrink-0 items-center rounded-sm border bg-sidebar px-1.5 text-[10px] leading-none font-medium",606 "shrink-0 text-xs font-medium",
606 props.isDropTarget607 props.isDropTarget ? "text-primary" : "text-sidebar-foreground/80",
607 ? "border-primary/40 text-primary"
608 : "border-sidebar-foreground/25 text-sidebar-foreground/80",
609 )}608 )}
610 >609 >
611 {props.label}610 {props.label}
...@@ -635,10 +634,10 @@ function SidebarSectionHeader(props: {...@@ -635,10 +634,10 @@ function SidebarSectionHeader(props: {
635}) {634}) {
636 const snoozed = props.marker === "snoozed-header";635 const snoozed = props.marker === "snoozed-header";
637 const className = cn(636 const className = cn(
638 "flex h-full w-full items-center gap-2 rounded-md border border-dashed border-transparent px-2 text-left text-xs font-medium",637 "flex h-full w-full items-center gap-2 px-2 text-left text-xs font-medium",
639 snoozed ? "text-blue-600 dark:text-blue-400" : "text-sidebar-muted-foreground/60",638 snoozed ? "text-blue-600 dark:text-blue-400" : "text-sidebar-muted-foreground/60",
640 props.dragging && "text-sidebar-foreground/80",639 props.dragging && "text-sidebar-foreground/80",
641 props.isDropTarget && "border-primary/40 bg-primary/5 text-primary",640 props.isDropTarget && "text-primary",
642 );641 );
643 const content = (642 const content = (
644 <>643 <>
...@@ -3261,9 +3260,7 @@ export default function Sidebar() {...@@ -3261,9 +3260,7 @@ export default function Sidebar() {
3261 items.push(...pinnedRows);3260 items.push(...pinnedRows);
3262 items.push({ kind: "marker", marker: "pinned-divider" });3261 items.push({ kind: "marker", marker: "pinned-divider" });
3263 const activeRows = rowsOf(activeThreads, "active");3262 const activeRows = rowsOf(activeThreads, "active");
3264 if (activeRows.length === 0) {3263 items.push({ kind: "marker", marker: "active-placeholder" });
3265 items.push({ kind: "marker", marker: "active-placeholder" });
3266 }
3267 items.push(...activeRows);3264 items.push(...activeRows);
3268 if (snoozedThreads.length > 0) {3265 if (snoozedThreads.length > 0) {
3269 items.push({ kind: "marker", marker: "snoozed-header" });3266 items.push({ kind: "marker", marker: "snoozed-header" });
...@@ -3271,9 +3268,7 @@ export default function Sidebar() {...@@ -3271,9 +3268,7 @@ export default function Sidebar() {
3271 }3268 }
3272 items.push({ kind: "marker", marker: "settled-header" });3269 items.push({ kind: "marker", marker: "settled-header" });
3273 const settledRows = rowsOf(renderedSettledThreads, "settled");3270 const settledRows = rowsOf(renderedSettledThreads, "settled");
3274 if (settledRows.length === 0) {3271 items.push({ kind: "marker", marker: "settled-placeholder" });
3275 items.push({ kind: "marker", marker: "settled-placeholder" });
3276 }
3277 items.push(...settledRows);3272 items.push(...settledRows);
3278 return items;3273 return items;
3279 }, [3274 }, [
...@@ -4805,7 +4800,14 @@ export default function Sidebar() {...@@ -4805,7 +4800,14 @@ export default function Sidebar() {
4805 key="active-placeholder"4800 key="active-placeholder"
4806 marker="active-placeholder"4801 marker="active-placeholder"
4807 label="Active"4802 label="Active"
4808 showHint={from !== null}4803 showHint={
4804 from !== null &&
4805 (activeThreads.length === 0 ||
4806 (from === "active" &&
4807 activeThreads.length === 1 &&
4808 dragTargetSection !== null &&
4809 dragTargetSection !== "active"))
4810 }
4809 isDropTarget={dragTargetSection === "active"}4811 isDropTarget={dragTargetSection === "active"}
4810 />,4812 />,
4811 );4813 );
...@@ -4852,7 +4854,14 @@ export default function Sidebar() {...@@ -4852,7 +4854,14 @@ export default function Sidebar() {
4852 key="settled-placeholder"4854 key="settled-placeholder"
4853 marker="settled-placeholder"4855 marker="settled-placeholder"
4854 label="Settled"4856 label="Settled"
4855 showHint={from !== null && from !== "settled"}4857 showHint={
4858 from !== null &&
4859 (renderedSettledThreads.length === 0 ||
4860 (from === "settled" &&
4861 renderedSettledThreads.length === 1 &&
4862 dragTargetSection !== null &&
4863 dragTargetSection !== "settled"))
4864 }
4856 isDropTarget={dragTargetSection === "settled"}4865 isDropTarget={dragTargetSection === "settled"}
4857 />,4866 />,
4858 );4867 );