Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 42 additions & 53 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,6 @@ function SidebarSectionPlaceholder(props: {
function SidebarDragBoundary(props: {
marker: "pinned-header" | "pinned-divider";
label: string;
hint: string | null;
visible: boolean;
isDropTarget: boolean;
}) {
Expand All @@ -587,7 +586,6 @@ function SidebarDragBoundary(props: {
)}
>
{props.label}
{props.hint ? <span className="font-normal">{props.hint}</span> : null}
</span>
<span
aria-hidden
Expand All @@ -603,7 +601,6 @@ function SidebarDragBoundary(props: {
function SidebarSectionHeader(props: {
marker: "snoozed-header" | "settled-header";
label: string;
hint?: string | null;
isDropTarget?: boolean;
toggle: { expanded: boolean; onToggle: () => void };
}) {
Expand All @@ -624,7 +621,6 @@ function SidebarSectionHeader(props: {
props.isDropTarget && "bg-primary/30",
)}
/>
{props.hint ? <span className="truncate text-[11px] font-normal">{props.hint}</span> : null}
<ChevronDownIcon
aria-hidden
className={cn(
Expand Down Expand Up @@ -910,7 +906,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
// sortable bag applied to the row root so the whole row drags (the
// pointer sensor's distance constraint keeps plain clicks working).
sortable?: SortableThreadRowBag | undefined;
dropSection: SidebarSection | null;
dropSection: Exclude<SidebarSection, "snoozed"> | null;
// Compact wake countdown ("2h") for rows in the snoozed shelf.
snoozeWakeLabelText: string | null;
// When a snooze ended (timer or early wake); drives the Woke pill until
Expand Down Expand Up @@ -1374,14 +1370,13 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
role="status"
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"
>
<span className="sr-only">Move to </span>
<span aria-hidden>→</span>
{props.dropSection === "pinned"
? "Pinned"
: props.dropSection === "active"
? "Active"
: props.dropSection === "settled"
? "Settled"
: "Snoozed"}
: "Settled"}
</span>
) : null;

Expand Down Expand Up @@ -1485,31 +1480,32 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
<TooltipPopup side="top">Unsent draft</TooltipPopup>
</Tooltip>
) : null;
const pinIndicator = props.isPinned ? (
props.pinningSupported ? (
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
aria-label="Unpin thread"
onClick={handleUnpinClick}
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"
/>
}
>
<PinIcon aria-hidden className="size-3 shrink-0" />
</TooltipTrigger>
<TooltipPopup>Unpin thread</TooltipPopup>
</Tooltip>
) : (
<PinIcon
aria-label="Pinned"
role="img"
className="size-3 shrink-0 text-muted-foreground/65"
/>
)
) : null;
const pinIndicator =
props.isPinned && !sortable?.isDragging ? (
props.pinningSupported ? (
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
aria-label="Unpin thread"
onClick={handleUnpinClick}
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"
/>
}
>
<PinIcon aria-hidden className="size-3 shrink-0" />
</TooltipTrigger>
<TooltipPopup>Unpin thread</TooltipPopup>
</Tooltip>
) : (
<PinIcon
aria-label="Pinned"
role="img"
className="size-3 shrink-0 text-muted-foreground/65"
/>
)
) : null;

if (variant === "slim") {
return (
Expand Down Expand Up @@ -1569,7 +1565,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
remain visible AND clickable while the row is hovered. Only
the time/jump label yields to the settle affordance. */}
{prBadge}
{dragDestination ?? (
{sortable?.isDragging ? (
dragDestination
) : (
<span className="relative ml-auto flex h-6 min-w-8 shrink-0 items-center justify-end">
<span
className={cn(
Expand Down Expand Up @@ -1722,7 +1720,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
actions on hover/keyboard focus or while the popover is open. Keeping
the hidden state out of flow lets the project label reclaim
space without either state overlapping it. */}
{dragDestination ?? (
{sortable?.isDragging ? (
dragDestination
) : (
<span className="group/sidebar-status-slot relative ml-auto flex h-5 min-w-8 shrink-0 items-stretch justify-end text-xs">
{/* Read-only status labels yield to the hover actions. Woke is
itself an action, so it stays pointer-enabled and visible
Expand Down Expand Up @@ -4569,7 +4569,11 @@ export default function Sidebar() {
isPinned={thread.pinnedAt != null}
sortable={sortable}
dropSection={
dragState?.activeKey === threadKey ? dragTargetSection : null
dragState?.activeKey === threadKey &&
dragTargetSection !== dragState.activeSection &&
dragTargetSection !== "snoozed"
? dragTargetSection
: null
}
snoozeWakeLabelText={
section === "snoozed" && thread.snoozedUntil != null
Expand Down Expand Up @@ -4673,14 +4677,6 @@ export default function Sidebar() {
dragTargetSection !== "pinned"
? 1
: 0);
const activeHint =
from === "pinned"
? "Drop to unpin"
: from === "settled"
? "Drop to un-settle"
: from === "snoozed"
? "Drop to wake"
: null;
const items: ReactNode[] = [
<SidebarDraftBlock
key="draft-sessions"
Expand All @@ -4706,7 +4702,6 @@ export default function Sidebar() {
key="pinned-header"
marker="pinned-header"
label="Pinned"
hint={from !== null && from !== "pinned" ? "Drop to pin" : null}
isDropTarget={dragTargetSection === "pinned"}
visible={from !== null}
/>,
Expand All @@ -4718,7 +4713,6 @@ export default function Sidebar() {
key="pinned-divider"
marker="pinned-divider"
label="Active"
hint={dragTargetSection === "active" ? activeHint : null}
isDropTarget={dragTargetSection === "active"}
visible={from !== null && previewPinnedCount > 0}
/>,
Expand All @@ -4729,7 +4723,7 @@ export default function Sidebar() {
<SidebarSectionPlaceholder
key="active-placeholder"
marker="active-placeholder"
label={activeHint ?? "Drop here"}
label="Active"
showHint={from !== null}
isDropTarget={dragTargetSection === "active"}
/>,
Expand Down Expand Up @@ -4762,11 +4756,6 @@ export default function Sidebar() {
? "Settled"
: `Settled (${settledThreads.length})`
}
hint={
dragTargetSection === "settled" && from !== "settled"
? "Drop to settle"
: null
}
isDropTarget={dragTargetSection === "settled"}
toggle={{
expanded: settledShelfExpanded,
Expand All @@ -4780,7 +4769,7 @@ export default function Sidebar() {
<SidebarSectionPlaceholder
key="settled-placeholder"
marker="settled-placeholder"
label="Drop to settle"
label="Settled"
showHint={from !== null && from !== "settled"}
isDropTarget={dragTargetSection === "settled"}
/>,
Expand Down
8 changes: 5 additions & 3 deletions docs/user/thread-sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ thread into the active list un-settles it. A snoozed thread can be dragged out o
shelf, which wakes it, but threads cannot be dragged into the shelf because snoozing needs a wake
time. Dragging a pinned thread out of the pinned section does not ask for unpin confirmation.
Pinned and active boundary labels appear only while dragging, without moving the rows. The
destination boundary highlights and the thread shows which section it will land in. When there
are no pins, drag to the top edge to pin a thread. Drop instructions also appear for empty sections
and a collapsed settled shelf.
destination boundary highlights. When you cross into another section, the dragged thread shows
its destination, such as **→ Active**. Its usual pin, status, and hover actions hide during the
drag. Reordering within the same section does not show a destination badge. When there are no
pins, drag to the top edge to pin a thread. Section labels also identify empty sections and a
collapsed settled shelf.

Drag within the pinned or active section to change its order. Other rows slide aside to show the
spot where the thread will land. Drops into either section keep the position you choose. On
Expand Down
Loading