feat(web): support cross-zone week event dnd#1868
Conversation
- Stop grid smart scroll once a timed drag preview snaps to the all-day row or a sidebar drop zone - Suppress edge week-navigation dwell while hovering sidebar drop zones so the week no longer changes under a sidebar drop - Restore the drag overlay's size when returning to the home surface and keep transform un-eased outside the snapped cross-surface mode - Smart scroll the grid while an all-day drag hovers the timed grid so off-screen times are reachable - Clamp all-day to timed conversions so the event ends within the dropped day - Fix crash on Someday Month drops (wrong column key) and route all calendar-to-someday conversions through a shared helper that sets category date ranges, rewrites recurrence frequency, and defaults priority/user; discard the draft after pointer conversions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dragging a timed or all-day grid event over the Someday sidebar now lights up the Week/Month drop zones (dashed active border, red when the column is full), matching the feedback shown when reordering someday items. Previously the zones stayed inert because their styling keys off sidebar `isDragging` (= isDNDing && sidebar draft), which the separate WeekInteractionAdapter drag path never sets. - Add a dedicated `isCalendarDragActive` sidebar state + a `setCalendarSidebarDropPreview` action, reusing `blockedSomedayDropColumn` for the full-column state. This avoids faking a sidebar draft (which would render a ghost someday list item via isDraftingNew). - SomedayEventsContainer honors the new flag alongside isDragging for the active style, expanded drop height, and hidden add button. - WeekInteractionAdapter emits a deduped onPreviewCalendarToSidebar on each move (the category under the pointer, or null), and clears it on commit and cancel via clearInteractionState. - WeekInteractionCoordinator maps the previewed category to its column and capacity-blocked flag (reusing the limit selectors it already reads) and drives the sidebar action through the shared SidebarDraftProvider context. Styling activates only while the dragged event is over the sidebar and clears on return to the grid, on a normal grid drop, and on Escape/cancel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two follow-up fixes for dragging a grid event into the Someday sidebar: 1. Live reordering. While hovering a grid event over a Someday list, the existing rows now shift with the standard FLIP animation as the pointer moves, instead of only rearranging after the drop. The calendar-drag preview now inserts a someday-shaped placeholder at the hovered index (via setSomedayEvents), mirroring the native sidebar reorder path; the placeholder is excluded from the index math to avoid jitter, and the snapshot is restored on leave/cancel. 2. Nearest-list snap. Dragging into the empty space between (or around) the two lists no longer jumps the event back to the grid. resolveSidebarDrop now treats the whole sidebar column as sidebar territory and snaps to the vertically nearest zone, so moving down from the Week list into the gap lands on the Month list. The preview channel (onPreviewCalendarToSidebar) now carries the hovered index and the dragged event, deduped on category+index so the placeholder follows the pointer without a setState every frame. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| * sidebar event. Someday events are categorized into the Week/Month columns | ||
| * by their dates, so the original grid dates must be replaced with the | ||
| * category's date range. | ||
| */ |
There was a problem hiding this comment.
Instead of doing this in someday.event.util, create a function whose responsibility it is to map payloads between event types. See map.event.ts for a reference.
| order, | ||
| viewEnd, | ||
| viewStart, | ||
| }: { |
There was a problem hiding this comment.
Extending like this is a smell that indicates bad design. Instead, user a new object/schema
| }), | ||
| } | ||
| : event.recurrence; | ||
|
|
There was a problem hiding this comment.
Sneaking in such heavy parsing is not good. Create a separate function for rrule parsing, which uses Zod for more clear parsing.
| baseEvents, | ||
| column, | ||
| event, | ||
| index, |
There was a problem hiding this comment.
We shouldn't have to manage the index and preview manually like this. Instead, just use the standard DND kit objects so we can delegate that responsibility to the library
| index: number; | ||
| isBlocked: boolean; | ||
| } | null, | ||
| ) => { |
There was a problem hiding this comment.
This is too hacky. Redesign to minimize the amount of manual logic we need. Use the dnd kit library primitives as much as possible to make it easier
| const isDraftingThisCategory = | ||
| state.isDraftingNew && category === draftCategory; | ||
| const isBlockedDropTarget = state.blockedSomedayDropColumn === colName; | ||
| // A drag is active for styling purposes whether it originates from the |
There was a problem hiding this comment.
Having multiple drag states is indicative of poor design. Redesign so we only need one piece of state to track whether a drag is active.
| [activity, draft, isInsideVisibleWeek, isTimedDraftInsideOneDay, setDraft], | ||
| ); | ||
|
|
||
| const convertDraftSurfaceByKeyboard = useCallback(() => { |
There was a problem hiding this comment.
remove this keyboard conversion feature. we can add that in a separate PR
| let lastReportedSidebarKey: string | null = null; | ||
| let layout: WeekLayoutCache | null = null; | ||
| let scrollTop: number | null = null; | ||
| let timedLayout: WeekLayoutCache | null = null; |
There was a problem hiding this comment.
we're adding too much state here, indicating a poor design. rethink how we can accomplish the intended behavior without needing so much
tyler-dane
left a comment
There was a problem hiding this comment.
Overall, this implementation is way too complicated to understand and maintain. It relies too much on introducing new state and functions. Instead, we should consolidate how things work and use simpler means
Addresses review feedback that the cross-zone drag was too complex and
introduced too much parallel state/logic. Consolidates onto existing
primitives instead of a second preview pipeline.
- Reuse the native someday reorder pipeline. A grid event dragged over the
sidebar is now injected into the someday snapshot (startCalendarSidebarDrag)
so the existing previewSomedaySidebarDrop / getSomedayEventsAfterSidebarDrop
/ isDragging machinery drives the live reorder, blocked state, and drop-zone
styling. Removes the parallel setCalendarSidebarDropPreview and
getSomedayEventsAfterCalendarDrop.
- One drag state. Deletes isCalendarDragActive; the single isDragging flag now
covers grid drags too. SomedayEventsContainer keys only on isDragging.
- Conversion via a mapper. Replaces the generic assembleSomedayConversionEvent
(with inline RRULE regex) with MapEvent.toSomeday (concrete signature) plus a
Zod-validated rewriteRecurrenceFreq helper in core. The existing "Move to
Sidebar" action shares the same mapper.
- Slims the week adapter: onPreviewCalendarToSidebar payload narrows to
{category,index}; adds an onCancelInteraction teardown hook.
- Removes the keyboard conversion feature (Shift+A/W/M draft conversion) to be
reintroduced in a separate PR.
The floating overlay is shrunk to a someday-row chip over the sidebar so it no
longer covers the list (the visual bug that made the reorder look broken).
Verified visually with a new Playwright repro
(e2e/someday/drag-grid-event-to-sidebar.spec.ts): rows open a live gap, the
overlay is a small chip, and a drop between lists converts to the nearest list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
Validation
bun test:webbun lintbun type-checkbun test:e2e(50 passed)