authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-06 11:10:14-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-06 11:10:14-07:00
log9a47c7bd40522977c7df022a5b64759aae7b58e0
tree4cf0187d05c2eff497f102cf330d82f11a4b5ec5
parent4023d93bce3f610349fbf38e991f737c1c5e0c98
signature Signed by PGP key B5690EEEBB952194

feat(web): simplify sidebar drag destination cues (#9750)


2 files changed, 47 insertions(+), 56 deletions(-)

apps/web/src/components/Sidebar.tsx+42-53
...@@ -568,7 +568,6 @@ function SidebarSectionPlaceholder(props: {...@@ -568,7 +568,6 @@ function SidebarSectionPlaceholder(props: {
568function SidebarDragBoundary(props: {568function SidebarDragBoundary(props: {
569 marker: "pinned-header" | "pinned-divider";569 marker: "pinned-header" | "pinned-divider";
570 label: string;570 label: string;
571 hint: string | null;
572 visible: boolean;571 visible: boolean;
573 isDropTarget: boolean;572 isDropTarget: boolean;
574}) {573}) {
...@@ -587,7 +586,6 @@ function SidebarDragBoundary(props: {...@@ -587,7 +586,6 @@ function SidebarDragBoundary(props: {
587 )}586 )}
588 >587 >
589 {props.label}588 {props.label}
590 {props.hint ? <span className="font-normal">{props.hint}</span> : null}
591 </span>589 </span>
592 <span590 <span
593 aria-hidden591 aria-hidden
...@@ -603,7 +601,6 @@ function SidebarDragBoundary(props: {...@@ -603,7 +601,6 @@ function SidebarDragBoundary(props: {
603function SidebarSectionHeader(props: {601function SidebarSectionHeader(props: {
604 marker: "snoozed-header" | "settled-header";602 marker: "snoozed-header" | "settled-header";
605 label: string;603 label: string;
606 hint?: string | null;
607 isDropTarget?: boolean;604 isDropTarget?: boolean;
608 toggle: { expanded: boolean; onToggle: () => void };605 toggle: { expanded: boolean; onToggle: () => void };
609}) {606}) {
...@@ -624,7 +621,6 @@ function SidebarSectionHeader(props: {...@@ -624,7 +621,6 @@ function SidebarSectionHeader(props: {
624 props.isDropTarget && "bg-primary/30",621 props.isDropTarget && "bg-primary/30",
625 )}622 )}
626 />623 />
627 {props.hint ? <span className="truncate text-[11px] font-normal">{props.hint}</span> : null}
628 <ChevronDownIcon624 <ChevronDownIcon
629 aria-hidden625 aria-hidden
630 className={cn(626 className={cn(
...@@ -910,7 +906,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -910,7 +906,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
910 // sortable bag applied to the row root so the whole row drags (the906 // sortable bag applied to the row root so the whole row drags (the
911 // pointer sensor's distance constraint keeps plain clicks working).907 // pointer sensor's distance constraint keeps plain clicks working).
912 sortable?: SortableThreadRowBag | undefined;908 sortable?: SortableThreadRowBag | undefined;
913 dropSection: SidebarSection | null;909 dropSection: Exclude<SidebarSection, "snoozed"> | null;
914 // Compact wake countdown ("2h") for rows in the snoozed shelf.910 // Compact wake countdown ("2h") for rows in the snoozed shelf.
915 snoozeWakeLabelText: string | null;911 snoozeWakeLabelText: string | null;
916 // When a snooze ended (timer or early wake); drives the Woke pill until912 // When a snooze ended (timer or early wake); drives the Woke pill until
...@@ -1331,14 +1327,13 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1331,14 +1327,13 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1331 role="status"1327 role="status"
1332 className="pointer-events-none ml-auto inline-flex h-5 shrink-0 items-center gap-1 rounded-sm border border-primary/30 bg-sidebar px-1.5 text-[11px] font-medium text-primary"1328 className="pointer-events-none ml-auto inline-flex h-5 shrink-0 items-center gap-1 rounded-sm border border-primary/30 bg-sidebar px-1.5 text-[11px] font-medium text-primary"
1333 >1329 >
1330 <span className="sr-only">Move to </span>
1334 <span aria-hidden>→</span>1331 <span aria-hidden>→</span>
1335 {props.dropSection === "pinned"1332 {props.dropSection === "pinned"
1336 ? "Pinned"1333 ? "Pinned"
1337 : props.dropSection === "active"1334 : props.dropSection === "active"
1338 ? "Active"1335 ? "Active"
1339 : props.dropSection === "settled"1336 : "Settled"}
1340 ? "Settled"
1341 : "Snoozed"}
1342 </span>1337 </span>
1343 ) : null;1338 ) : null;
13441339
...@@ -1442,31 +1437,32 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1442,31 +1437,32 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1442 <TooltipPopup side="top">Unsent draft</TooltipPopup>1437 <TooltipPopup side="top">Unsent draft</TooltipPopup>
1443 </Tooltip>1438 </Tooltip>
1444 ) : null;1439 ) : null;
1445 const pinIndicator = props.isPinned ? (1440 const pinIndicator =
1446 props.pinningSupported ? (1441 props.isPinned && !sortable?.isDragging ? (
1447 <Tooltip>1442 props.pinningSupported ? (
1448 <TooltipTrigger1443 <Tooltip>
1449 render={1444 <TooltipTrigger
1450 <button1445 render={
1451 type="button"1446 <button
1452 aria-label="Unpin thread"1447 type="button"
1453 onClick={handleUnpinClick}1448 aria-label="Unpin thread"
1454 className="inline-flex cursor-pointer items-center rounded-sm text-muted-foreground/65 outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"1449 onClick={handleUnpinClick}
1455 />1450 className="inline-flex cursor-pointer items-center rounded-sm text-muted-foreground/65 outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
1456 }1451 />
1457 >1452 }
1458 <PinIcon aria-hidden className="size-3 shrink-0" />1453 >
1459 </TooltipTrigger>1454 <PinIcon aria-hidden className="size-3 shrink-0" />
1460 <TooltipPopup>Unpin thread</TooltipPopup>1455 </TooltipTrigger>
1461 </Tooltip>1456 <TooltipPopup>Unpin thread</TooltipPopup>
1462 ) : (1457 </Tooltip>
1463 <PinIcon1458 ) : (
1464 aria-label="Pinned"1459 <PinIcon
1465 role="img"1460 aria-label="Pinned"
1466 className="size-3 shrink-0 text-muted-foreground/65"1461 role="img"
1467 />1462 className="size-3 shrink-0 text-muted-foreground/65"
1468 )1463 />
1469 ) : null;1464 )
1465 ) : null;
14701466
1471 if (variant === "slim") {1467 if (variant === "slim") {
1472 return (1468 return (
...@@ -1526,7 +1522,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1526,7 +1522,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1526 remain visible AND clickable while the row is hovered. Only1522 remain visible AND clickable while the row is hovered. Only
1527 the time/jump label yields to the settle affordance. */}1523 the time/jump label yields to the settle affordance. */}
1528 {prBadge}1524 {prBadge}
1529 {dragDestination ?? (1525 {sortable?.isDragging ? (
1526 dragDestination
1527 ) : (
1530 <span className="relative ml-auto flex h-6 min-w-8 shrink-0 items-center justify-end">1528 <span className="relative ml-auto flex h-6 min-w-8 shrink-0 items-center justify-end">
1531 <span1529 <span
1532 className={cn(1530 className={cn(
...@@ -1679,7 +1677,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1679,7 +1677,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1679 actions on hover/keyboard focus or while the popover is open. Keeping1677 actions on hover/keyboard focus or while the popover is open. Keeping
1680 the hidden state out of flow lets the project label reclaim1678 the hidden state out of flow lets the project label reclaim
1681 space without either state overlapping it. */}1679 space without either state overlapping it. */}
1682 {dragDestination ?? (1680 {sortable?.isDragging ? (
1681 dragDestination
1682 ) : (
1683 <span className="group/sidebar-status-slot relative ml-auto flex h-5 min-w-8 shrink-0 items-stretch justify-end text-xs">1683 <span className="group/sidebar-status-slot relative ml-auto flex h-5 min-w-8 shrink-0 items-stretch justify-end text-xs">
1684 {/* Read-only status labels yield to the hover actions. Woke is1684 {/* Read-only status labels yield to the hover actions. Woke is
1685 itself an action, so it stays pointer-enabled and visible1685 itself an action, so it stays pointer-enabled and visible
...@@ -4534,7 +4534,11 @@ export default function Sidebar() {...@@ -4534,7 +4534,11 @@ export default function Sidebar() {
4534 isPinned={thread.pinnedAt != null}4534 isPinned={thread.pinnedAt != null}
4535 sortable={sortable}4535 sortable={sortable}
4536 dropSection={4536 dropSection={
4537 dragState?.activeKey === threadKey ? dragTargetSection : null4537 dragState?.activeKey === threadKey &&
4538 dragTargetSection !== dragState.activeSection &&
4539 dragTargetSection !== "snoozed"
4540 ? dragTargetSection
4541 : null
4538 }4542 }
4539 snoozeWakeLabelText={4543 snoozeWakeLabelText={
4540 section === "snoozed" && thread.snoozedUntil != null4544 section === "snoozed" && thread.snoozedUntil != null
...@@ -4634,14 +4638,6 @@ export default function Sidebar() {...@@ -4634,14 +4638,6 @@ export default function Sidebar() {
4634 dragTargetSection !== "pinned"4638 dragTargetSection !== "pinned"
4635 ? 14639 ? 1
4636 : 0);4640 : 0);
4637 const activeHint =
4638 from === "pinned"
4639 ? "Drop to unpin"
4640 : from === "settled"
4641 ? "Drop to un-settle"
4642 : from === "snoozed"
4643 ? "Drop to wake"
4644 : null;
4645 const items: ReactNode[] = [4641 const items: ReactNode[] = [
4646 <SidebarDraftBlock4642 <SidebarDraftBlock
4647 key="draft-sessions"4643 key="draft-sessions"
...@@ -4667,7 +4663,6 @@ export default function Sidebar() {...@@ -4667,7 +4663,6 @@ export default function Sidebar() {
4667 key="pinned-header"4663 key="pinned-header"
4668 marker="pinned-header"4664 marker="pinned-header"
4669 label="Pinned"4665 label="Pinned"
4670 hint={from !== null && from !== "pinned" ? "Drop to pin" : null}
4671 isDropTarget={dragTargetSection === "pinned"}4666 isDropTarget={dragTargetSection === "pinned"}
4672 visible={from !== null}4667 visible={from !== null}
4673 />,4668 />,
...@@ -4679,7 +4674,6 @@ export default function Sidebar() {...@@ -4679,7 +4674,6 @@ export default function Sidebar() {
4679 key="pinned-divider"4674 key="pinned-divider"
4680 marker="pinned-divider"4675 marker="pinned-divider"
4681 label="Active"4676 label="Active"
4682 hint={dragTargetSection === "active" ? activeHint : null}
4683 isDropTarget={dragTargetSection === "active"}4677 isDropTarget={dragTargetSection === "active"}
4684 visible={from !== null && previewPinnedCount > 0}4678 visible={from !== null && previewPinnedCount > 0}
4685 />,4679 />,
...@@ -4690,7 +4684,7 @@ export default function Sidebar() {...@@ -4690,7 +4684,7 @@ export default function Sidebar() {
4690 <SidebarSectionPlaceholder4684 <SidebarSectionPlaceholder
4691 key="active-placeholder"4685 key="active-placeholder"
4692 marker="active-placeholder"4686 marker="active-placeholder"
4693 label={activeHint ?? "Drop here"}4687 label="Active"
4694 showHint={from !== null}4688 showHint={from !== null}
4695 isDropTarget={dragTargetSection === "active"}4689 isDropTarget={dragTargetSection === "active"}
4696 />,4690 />,
...@@ -4723,11 +4717,6 @@ export default function Sidebar() {...@@ -4723,11 +4717,6 @@ export default function Sidebar() {
4723 ? "Settled"4717 ? "Settled"
4724 : `Settled (${settledThreads.length})`4718 : `Settled (${settledThreads.length})`
4725 }4719 }
4726 hint={
4727 dragTargetSection === "settled" && from !== "settled"
4728 ? "Drop to settle"
4729 : null
4730 }
4731 isDropTarget={dragTargetSection === "settled"}4720 isDropTarget={dragTargetSection === "settled"}
4732 toggle={{4721 toggle={{
4733 expanded: settledShelfExpanded,4722 expanded: settledShelfExpanded,
...@@ -4741,7 +4730,7 @@ export default function Sidebar() {...@@ -4741,7 +4730,7 @@ export default function Sidebar() {
4741 <SidebarSectionPlaceholder4730 <SidebarSectionPlaceholder
4742 key="settled-placeholder"4731 key="settled-placeholder"
4743 marker="settled-placeholder"4732 marker="settled-placeholder"
4744 label="Drop to settle"4733 label="Settled"
4745 showHint={from !== null && from !== "settled"}4734 showHint={from !== null && from !== "settled"}
4746 isDropTarget={dragTargetSection === "settled"}4735 isDropTarget={dragTargetSection === "settled"}
4747 />,4736 />,
docs/user/thread-sidebar.md+5-3
...@@ -33,9 +33,11 @@ thread into the active list un-settles it. A snoozed thread can be dragged out o...@@ -33,9 +33,11 @@ thread into the active list un-settles it. A snoozed thread can be dragged out o
33shelf, which wakes it, but threads cannot be dragged into the shelf because snoozing needs a wake33shelf, which wakes it, but threads cannot be dragged into the shelf because snoozing needs a wake
34time. Dragging a pinned thread out of the pinned section does not ask for unpin confirmation.34time. Dragging a pinned thread out of the pinned section does not ask for unpin confirmation.
35Pinned and active boundary labels appear only while dragging, without moving the rows. The35Pinned and active boundary labels appear only while dragging, without moving the rows. The
36destination boundary highlights and the thread shows which section it will land in. When there36destination boundary highlights. When you cross into another section, the dragged thread shows
37are no pins, drag to the top edge to pin a thread. Drop instructions also appear for empty sections37its destination, such as **→ Active**. Its usual pin, status, and hover actions hide during the
38and a collapsed settled shelf.38drag. Reordering within the same section does not show a destination badge. When there are no
39pins, drag to the top edge to pin a thread. Section labels also identify empty sections and a
40collapsed settled shelf.
3941
40Drag within the pinned or active section to change its order. Other rows slide aside to show the42Drag within the pinned or active section to change its order. Other rows slide aside to show the
41spot where the thread will land. Drops into either section keep the position you choose. On43spot where the thread will land. Drops into either section keep the position you choose. On