Skip to content

feat(web): support cross-zone week event dnd#1868

Draft
tyler-dane wants to merge 6 commits into
mainfrom
feat/dnd-between-zones
Draft

feat(web): support cross-zone week event dnd#1868
tyler-dane wants to merge 6 commits into
mainfrom
feat/dnd-between-zones

Conversation

@tyler-dane

Copy link
Copy Markdown
Contributor

Summary

  • Adds cross-surface Week event dragging between timed grid and all-day row.
  • Adds calendar-event drops into Someday Week/Month sidebar zones using the existing Someday drop target registry.
  • Adds keyboard parity for active Week drafts: convert timed/all-day and move to Someday Week/Month through existing shortcuts.

Validation

  • bun test:web
  • bun lint
  • bun type-check
  • bun test:e2e (50 passed)

tyler-dane and others added 5 commits June 12, 2026 16:20
- 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.
*/

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
}: {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extending like this is a smell that indicates bad design. Instead, user a new object/schema

}),
}
: event.recurrence;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
) => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(() => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 tyler-dane left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant