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: {
568568function SidebarDragBoundary(props: {
569569 marker: "pinned-header" | "pinned-divider";
570570 label: string;
571 hint: string | null;
572571 visible: boolean;
573572 isDropTarget: boolean;
574573}) {
......@@ -587,7 +586,6 @@ function SidebarDragBoundary(props: {
587586 )}
588587 >
589588 {props.label}
590 {props.hint ? <span className="font-normal">{props.hint}</span> : null}
591589 </span>
592590 <span
593591 aria-hidden
......@@ -603,7 +601,6 @@ function SidebarDragBoundary(props: {
603601function SidebarSectionHeader(props: {
604602 marker: "snoozed-header" | "settled-header";
605603 label: string;
606 hint?: string | null;
607604 isDropTarget?: boolean;
608605 toggle: { expanded: boolean; onToggle: () => void };
609606}) {
......@@ -624,7 +621,6 @@ function SidebarSectionHeader(props: {
624621 props.isDropTarget && "bg-primary/30",
625622 )}
626623 />
627 {props.hint ? <span className="truncate text-[11px] font-normal">{props.hint}</span> : null}
628624 <ChevronDownIcon
629625 aria-hidden
630626 className={cn(
......@@ -910,7 +906,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
910906 // sortable bag applied to the row root so the whole row drags (the
911907 // pointer sensor's distance constraint keeps plain clicks working).
912908 sortable?: SortableThreadRowBag | undefined;
913 dropSection: SidebarSection | null;
909 dropSection: Exclude<SidebarSection, "snoozed"> | null;
914910 // Compact wake countdown ("2h") for rows in the snoozed shelf.
915911 snoozeWakeLabelText: string | null;
916912 // When a snooze ended (timer or early wake); drives the Woke pill until
......@@ -1331,14 +1327,13 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
13311327 role="status"
13321328 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"
13331329 >
1330 <span className="sr-only">Move to </span>
13341331 <span aria-hidden>→</span>
13351332 {props.dropSection === "pinned"
13361333 ? "Pinned"
13371334 : props.dropSection === "active"
13381335 ? "Active"
1339 : props.dropSection === "settled"
1340 ? "Settled"
1341 : "Snoozed"}
1336 : "Settled"}
13421337 </span>
13431338 ) : null;
13441339
......@@ -1442,31 +1437,32 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
14421437 <TooltipPopup side="top">Unsent draft</TooltipPopup>
14431438 </Tooltip>
14441439 ) : null;
1445 const pinIndicator = props.isPinned ? (
1446 props.pinningSupported ? (
1447 <Tooltip>
1448 <TooltipTrigger
1449 render={
1450 <button
1451 type="button"
1452 aria-label="Unpin thread"
1453 onClick={handleUnpinClick}
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"
1455 />
1456 }
1457 >
1458 <PinIcon aria-hidden className="size-3 shrink-0" />
1459 </TooltipTrigger>
1460 <TooltipPopup>Unpin thread</TooltipPopup>
1461 </Tooltip>
1462 ) : (
1463 <PinIcon
1464 aria-label="Pinned"
1465 role="img"
1466 className="size-3 shrink-0 text-muted-foreground/65"
1467 />
1468 )
1469 ) : null;
1440 const pinIndicator =
1441 props.isPinned && !sortable?.isDragging ? (
1442 props.pinningSupported ? (
1443 <Tooltip>
1444 <TooltipTrigger
1445 render={
1446 <button
1447 type="button"
1448 aria-label="Unpin thread"
1449 onClick={handleUnpinClick}
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"
1451 />
1452 }
1453 >
1454 <PinIcon aria-hidden className="size-3 shrink-0" />
1455 </TooltipTrigger>
1456 <TooltipPopup>Unpin thread</TooltipPopup>
1457 </Tooltip>
1458 ) : (
1459 <PinIcon
1460 aria-label="Pinned"
1461 role="img"
1462 className="size-3 shrink-0 text-muted-foreground/65"
1463 />
1464 )
1465 ) : null;
14701466
14711467 if (variant === "slim") {
14721468 return (
......@@ -1526,7 +1522,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
15261522 remain visible AND clickable while the row is hovered. Only
15271523 the time/jump label yields to the settle affordance. */}
15281524 {prBadge}
1529 {dragDestination ?? (
1525 {sortable?.isDragging ? (
1526 dragDestination
1527 ) : (
15301528 <span className="relative ml-auto flex h-6 min-w-8 shrink-0 items-center justify-end">
15311529 <span
15321530 className={cn(
......@@ -1679,7 +1677,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
16791677 actions on hover/keyboard focus or while the popover is open. Keeping
16801678 the hidden state out of flow lets the project label reclaim
16811679 space without either state overlapping it. */}
1682 {dragDestination ?? (
1680 {sortable?.isDragging ? (
1681 dragDestination
1682 ) : (
16831683 <span className="group/sidebar-status-slot relative ml-auto flex h-5 min-w-8 shrink-0 items-stretch justify-end text-xs">
16841684 {/* Read-only status labels yield to the hover actions. Woke is
16851685 itself an action, so it stays pointer-enabled and visible
......@@ -4534,7 +4534,11 @@ export default function Sidebar() {
45344534 isPinned={thread.pinnedAt != null}
45354535 sortable={sortable}
45364536 dropSection={
4537 dragState?.activeKey === threadKey ? dragTargetSection : null
4537 dragState?.activeKey === threadKey &&
4538 dragTargetSection !== dragState.activeSection &&
4539 dragTargetSection !== "snoozed"
4540 ? dragTargetSection
4541 : null
45384542 }
45394543 snoozeWakeLabelText={
45404544 section === "snoozed" && thread.snoozedUntil != null
......@@ -4634,14 +4638,6 @@ export default function Sidebar() {
46344638 dragTargetSection !== "pinned"
46354639 ? 1
46364640 : 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;
46454641 const items: ReactNode[] = [
46464642 <SidebarDraftBlock
46474643 key="draft-sessions"
......@@ -4667,7 +4663,6 @@ export default function Sidebar() {
46674663 key="pinned-header"
46684664 marker="pinned-header"
46694665 label="Pinned"
4670 hint={from !== null && from !== "pinned" ? "Drop to pin" : null}
46714666 isDropTarget={dragTargetSection === "pinned"}
46724667 visible={from !== null}
46734668 />,
......@@ -4679,7 +4674,6 @@ export default function Sidebar() {
46794674 key="pinned-divider"
46804675 marker="pinned-divider"
46814676 label="Active"
4682 hint={dragTargetSection === "active" ? activeHint : null}
46834677 isDropTarget={dragTargetSection === "active"}
46844678 visible={from !== null && previewPinnedCount > 0}
46854679 />,
......@@ -4690,7 +4684,7 @@ export default function Sidebar() {
46904684 <SidebarSectionPlaceholder
46914685 key="active-placeholder"
46924686 marker="active-placeholder"
4693 label={activeHint ?? "Drop here"}
4687 label="Active"
46944688 showHint={from !== null}
46954689 isDropTarget={dragTargetSection === "active"}
46964690 />,
......@@ -4723,11 +4717,6 @@ export default function Sidebar() {
47234717 ? "Settled"
47244718 : `Settled (${settledThreads.length})`
47254719 }
4726 hint={
4727 dragTargetSection === "settled" && from !== "settled"
4728 ? "Drop to settle"
4729 : null
4730 }
47314720 isDropTarget={dragTargetSection === "settled"}
47324721 toggle={{
47334722 expanded: settledShelfExpanded,
......@@ -4741,7 +4730,7 @@ export default function Sidebar() {
47414730 <SidebarSectionPlaceholder
47424731 key="settled-placeholder"
47434732 marker="settled-placeholder"
4744 label="Drop to settle"
4733 label="Settled"
47454734 showHint={from !== null && from !== "settled"}
47464735 isDropTarget={dragTargetSection === "settled"}
47474736 />,
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
3333shelf, which wakes it, but threads cannot be dragged into the shelf because snoozing needs a wake
3434time. Dragging a pinned thread out of the pinned section does not ask for unpin confirmation.
3535Pinned 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 there
37are no pins, drag to the top edge to pin a thread. Drop instructions also appear for empty sections
38and a collapsed settled shelf.
36destination boundary highlights. When you cross into another section, the dragged thread shows
37its destination, such as **→ Active**. Its usual pin, status, and hover actions hide during the
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
4042Drag within the pinned or active section to change its order. Other rows slide aside to show the
4143spot where the thread will land. Drops into either section keep the position you choose. On