feat(desktop): drag sidebar rows by the title, not just the grabber
The grabber in the lead column was the only way to reorder a session or a project, and it only appears on hover — a 14px target for the whole gesture. The row's title is the obvious thing to grab, so put the sortable listeners on the row shell. For a session that means two drags share one press, since the title already starts the drag into the layout. They need no arbitration: each declines outside its own region. Over the sidebar only the reorder has a target (the session drop denies — side chrome hosts no main tile); over the tree only the session drop does (no sortable row there). Whichever the release lands on is the one that commits. Rows exclude their own controls through one data-row-actions selector, now owned by SidebarRowShell instead of restated per row.
This commit is contained in:
parent
e128f1c131
commit
35b82fdef3
|
|
@ -55,7 +55,9 @@ export function SidebarDateDivider({ className, label, ...props }: React.Compone
|
|||
)
|
||||
}
|
||||
|
||||
/** Outer grid — sole owner of row height. */
|
||||
/** Outer grid — sole owner of row height. The trailing `actions` slot is
|
||||
* marked `data-row-actions` so a row-wide drag gesture can exclude it with
|
||||
* one selector: it holds real controls, never grab surface. */
|
||||
export function SidebarRowShell({
|
||||
actions,
|
||||
children,
|
||||
|
|
@ -65,7 +67,11 @@ export function SidebarRowShell({
|
|||
return (
|
||||
<div className={cn(rowMinH, 'grid grid-cols-[minmax(0,1fr)_auto] items-stretch rounded-md', className)} {...props}>
|
||||
{children}
|
||||
{actions ? <div className="flex shrink-0 items-center self-center">{actions}</div> : null}
|
||||
{actions ? (
|
||||
<div className="flex shrink-0 items-center self-center" data-row-actions>
|
||||
{actions}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,6 +125,18 @@ export function ProjectOverviewRow({
|
|||
</>
|
||||
}
|
||||
className={cn('group/workspace', dragging && 'cursor-grabbing bg-(--ui-sidebar-surface-background)')}
|
||||
// The label is grab surface too, not just the lead's grabber — same
|
||||
// listeners, minus the controls that keep their own gestures. A project
|
||||
// row has no rival drag (its title navigates on CLICK), so the sortable
|
||||
// owns the press outright.
|
||||
{...dragHandleProps}
|
||||
onPointerDown={event => {
|
||||
if ((event.target as HTMLElement).closest('[data-reorder-handle], [data-row-actions]')) {
|
||||
return
|
||||
}
|
||||
|
||||
dragHandleProps?.onPointerDown?.(event)
|
||||
}}
|
||||
ref={rowRef}
|
||||
>
|
||||
<SidebarRowCluster className="min-w-0 flex-1">
|
||||
|
|
@ -141,6 +153,7 @@ export function ProjectOverviewRow({
|
|||
<button
|
||||
aria-label={s.projects.toggle(project.label, !open)}
|
||||
className="flex flex-1 items-center self-stretch bg-transparent p-0"
|
||||
data-row-actions
|
||||
onClick={toggleOpen}
|
||||
type="button"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -140,19 +140,26 @@ function SidebarSessionRowImpl({
|
|||
className
|
||||
)}
|
||||
data-working={liveTurn ? 'true' : undefined}
|
||||
// The row runs BOTH drags off one press, and each declines outside its
|
||||
// own region — so no timing/arbitration rule is needed and neither can
|
||||
// steal the other's gesture. Over the sidebar only the reorder has a
|
||||
// target (the session drop denies: side chrome hosts no main tile);
|
||||
// over the tree only the session drop does (no sortable row there).
|
||||
// Whichever one the release lands on is the one that commits.
|
||||
{...dragHandleProps}
|
||||
onPointerDown={event => {
|
||||
// Reorder drags belong to dnd-kit (the grab handle); the ⋯ actions
|
||||
// cluster keeps its own gestures. Everything else on the row —
|
||||
// including the row-body BUTTON, the natural grab surface — is a
|
||||
// session drag source: a POINTER drag on the shared drag session
|
||||
// (never native HTML5 DnD: no macOS snap-back, Esc aborts
|
||||
// instantly). Sub-threshold releases stay ordinary clicks, so
|
||||
// resume / pin / open-in-window are untouched.
|
||||
// The grabber already carries these same listeners, and the ⋯
|
||||
// cluster keeps its own gestures.
|
||||
if ((event.target as HTMLElement).closest('[data-reorder-handle], [data-row-actions]')) {
|
||||
return
|
||||
}
|
||||
|
||||
// A POINTER drag on the shared drag session (never native HTML5 DnD:
|
||||
// no macOS snap-back, Esc aborts instantly). Sub-threshold releases
|
||||
// stay ordinary clicks, so resume / pin / open-in-window are
|
||||
// untouched.
|
||||
startSessionDrag({ id: session.id, profile: session.profile || 'default', title }, event)
|
||||
dragHandleProps?.onPointerDown?.(event)
|
||||
}}
|
||||
// Hovering a row from another profile (the all-profiles view) telegraphs
|
||||
// a cross-profile resume — start that backend's spawn now so the click
|
||||
|
|
|
|||
Loading…
Reference in New Issue