Skip to content

Add resizable event preview with top/bottom handles#1372

Merged
victor-enogwe merged 10 commits into
mainfrom
copilot/add-resizable-event-preview
Dec 23, 2025
Merged

Add resizable event preview with top/bottom handles#1372
victor-enogwe merged 10 commits into
mainfrom
copilot/add-resizable-event-preview

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Dec 18, 2025

Implements resizable event previews for timed agenda events, allowing users to adjust event duration by dragging top or bottom edges. Resize operations update draft state in real-time and snap to 15-minute intervals.

Use Case

closes #1364

Changes

  • Enabled resize handles: Set top: true and bottom: true in Resizable component config
  • Added resize handler: Calculates new start/end dates based on delta, handles bidirectional resize (positive/negative deltas)
  • Type compatibility: Normalizes Schema_GridEvent.recurrence (allows rule: null) to Schema_Event format (expects rule?: string[])
  • Performance: Helper function moved outside callback to avoid recreation on each resize event

Implementation

const normalizeRecurrence = () =>
  event.recurrence?.rule === null ? undefined : event.recurrence;

onResize={(_, direction, _ref, delta) => {
  const { height } = delta;
  const slot = Math.floor(Math.abs(height / SLOT_HEIGHT));
  const minutesToAdd = slot * MINUTES_PER_SLOT;

  if (direction === "top") {
    const oldStartDate = parseCompassEventDate(event.startDate);
    const startDate = height >= 0
      ? oldStartDate.subtract(minutesToAdd, "minute")
      : oldStartDate.add(minutesToAdd, "minute");

    setDraft({ ...event, startDate: startDate.toISOString(), recurrence: normalizeRecurrence() });
  }
  // Similar logic for bottom handle
}}

Floating form placement already uses right-start/left-start strategy via useFloatingAtCursor, ensuring preview remains visible during resize.

Original prompt

This section details on the original issue you should resolve

<issue_title>Resizable event preview for event forms</issue_title>
<issue_description>### Priority

None

Feature Description

Add the ability to resize the event preview for both existing events and new events. When the event form is open, the preview should not be blocked (except when limited by available floating space). The floating form should be aligned right-start or left-start, so the preview is clearly visible and can be resized.

Use Case

Users will be able to see and interact with event previews more flexibly during event creation and editing. This ensures the preview is clearly visible and adjustable, improving usability and user experience.

Additional Context

  • For floating forms, align them to right-start or left-start depending on available space.
  • Prevent the event preview from being blocked by the form wherever possible.
  • Applies to both existing and newly created events.
  • Consider technical constraints in UI placement and responsiveness.</issue_description>

<agent_instructions>a preliminary implementation starter code for packages/web/src/views/Day/components/Agenda/Events/TimedAgendaEvent/DraggableTimedAgendaEvent.tsx is given below.

import classNames from "classnames";
import fastDeepEqual from "fast-deep-equal/react";
import { memo } from "react";
import { UseInteractionsReturn } from "@floating-ui/react";
import { Categories_Event } from "@core/types/event.types";
import { CLASS_TIMED_CALENDAR_EVENT } from "@web/common/constants/web.constants";
import { useGridMaxZIndex } from "@web/common/hooks/useGridMaxZIndex";
import { Schema_GridEvent } from "@web/common/types/web.event.types";
import { Draggable } from "@web/components/DND/Draggable";
import { Resizable } from "@web/components/DND/Resizable";
import { TimedAgendaEvent } from "@web/views/Day/components/Agenda/Events/TimedAgendaEvent/TimedAgendaEvent";
import { SLOT_HEIGHT } from "@web/views/Day/constants/day.constants";
import { useOpenAgendaEventPreview } from "@web/views/Day/hooks/events/useOpenAgendaEventPreview";
import { useOpenEventContextMenu } from "@web/views/Day/hooks/events/useOpenEventContextMenu";
import { getAgendaEventPosition } from "@web/views/Day/util/agenda/agenda.util";
import { setDraft } from '../../../../../Calendar/components/Draft/context/useDraft';
import { parseCompassEventDate } from '../../../../../../../../core/src/util/event/event.util';

export const DraggableTimedAgendaEvent = memo(
  ({
    event,
    bounds,
    interactions,
    isDraftEvent,
    isNewDraftEvent,
  }: {
    event: Schema_GridEvent;
    bounds: HTMLElement;
    interactions: UseInteractionsReturn;
    isDraftEvent: boolean;
    isNewDraftEvent: boolean;
  }) => {
    const openAgendaEventPreview = useOpenAgendaEventPreview();
    const openEventContextMenu = useOpenEventContextMenu();
    const maxZIndex = useGridMaxZIndex();

    if (!event.startDate || !event.endDate || event.isAllDay) return null;

    const startDate = new Date(event.startDate);
    const startPosition = getAgendaEventPosition(startDate);

    return (
      <Draggable
        {...interactions?.getReferenceProps({
          onContextMenu: isNewDraftEvent ? undefined : openEventContextMenu,
          onFocus: isNewDraftEvent ? undefined : openAgendaEventPreview,
          onPointerEnter: isNewDraftEvent ? undefined : openAgendaEventPreview,
        })}
        dndProps={{
          id: event._id,
          data: {
            event,
            type: Categories_Event.TIMED,
            view: "day",
          },
        }}
        as="div"
        asChild
        className={classNames(
          CLASS_TIMED_CALENDAR_EVENT,
          "absolute cursor-move touch-none rounded focus:outline-none",
          "focus-visible:rounded focus-visible:ring-2",
          "focus:outline-none focus-visible:ring-yellow-200",
        )}
        style={{
          top: `${startPosition}px`,
          zIndex: isDraftEvent ? maxZIndex + 3 : undefined,
        }}
        tabIndex={0}
        role="button"
        data-draft-event={isDraftEvent}
        data-new-draft-event={isNewDraftEvent}
        data-event-id={event._id}
        aria-label={event.title || "Untitled event"}
      >
        <Resizable
          enable={{
            top: false, // all handles temporary disables until full implementation
            bottom: false,
            right: false,
            left: false,
            topRight: false,
            bottomRight: false,
            bottomLeft: false,
            topLeft: false,
          }}
          minHeight={SLOT_HEIGHT}
          minWidth="100%"
          maxWidth="100%"
          defaultSize={{ width: "100%" }}
          bounds={bounds}
          onResize={(_, direction, _ref, delta) => {
            const { height } = delta;
            const slot = Math.ceil(height / SLOT_HEIGHT);
            const minutesToAdd = slot * MINUTES_PER_SLOT;

       ...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes SwitchbackTech/compass#1364

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Copilot AI and others added 2 commits December 18, 2025 15:39
Co-authored-by: victor-enogwe <23452630+victor-enogwe@users.noreply.github.com>
…tion

Co-authored-by: victor-enogwe <23452630+victor-enogwe@users.noreply.github.com>
Copilot AI changed the title [WIP] Add resizable event preview for event forms Add resizable event preview with top/bottom handles Dec 18, 2025
Copilot AI requested a review from victor-enogwe December 18, 2025 15:49
…review

:package deps(rxjs-state-management): introduce rxjs state  @ngneat/elf library

- Updated EventContextMenu and related components to utilize activeEvent$ observable for managing active events.
- Replaced setDraft with resetDraft and resetActiveEvent in event form hooks to improve state management.
- Refactored tests to mock new event store structure and ensure proper event handling.
- Introduced utility functions for calculating event height and rounding minutes to nearest fifteen.
- Updated yarn.lock to include new dependencies for @ngneat/elf-entities and @ngneat/use-observable.
@victor-enogwe victor-enogwe force-pushed the copilot/add-resizable-event-preview branch from 17eb600 to ba0314d Compare December 22, 2025 18:49
@victor-enogwe victor-enogwe marked this pull request as ready for review December 23, 2025 09:47
Copilot AI review requested due to automatic review settings December 23, 2025 09:47
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements resizable event previews for timed agenda events with drag handles on top and bottom edges. Users can now adjust event duration by dragging, with changes snapping to 15-minute intervals and updating in real-time via draft state.

Key Changes:

  • Enabled top/bottom resize handles on timed events with snap-to-grid behavior
  • Introduced Elf state management library alongside Redux for draft/active event state
  • Enhanced floating UI positioning with full-width event detection and better overlap handling

Reviewed changes

Copilot reviewed 62 out of 67 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/web/src/common/hooks/useEventResizeActions.ts New hook handling resize lifecycle (start/resize/stop) with 15-min snapping
packages/web/src/views/Day/components/Agenda/Events/TimedAgendaEvent/DraggableTimedAgendaEvent.tsx Enabled resize handles and integrated resize actions
packages/web/src/store/events.ts New Elf store managing draft state, active events, and event collections
packages/web/src/common/hooks/useOpenAtCursor.ts Enhanced floating UI control with open state tracking
packages/web/src/common/hooks/useFloatingAtCursor.ts Improved positioning logic for overlapping/full-width events
packages/web/src/common/utils/dom/grid-organization.util.ts Enhanced grid layout with full-width detection for tall events
packages/web/src/views/Forms/hooks/useOpenEventForm.ts Migrated to Elf store for draft management
packages/web/src/views/Day/components/Agenda/Agenda.tsx Integrated Elf observables for event rendering
packages/web/src/common/hooks/useUpdateEvent.ts New hook abstracting event updates with save control
Test files Comprehensive test coverage for new hooks and components

Comment thread packages/web/src/common/hooks/useEventResizeActions.ts Outdated
Comment thread packages/web/src/common/hooks/useEventResizeActions.ts
Comment thread packages/web/src/store/events.ts Outdated
Comment thread packages/web/src/store/events.ts
Comment thread packages/web/src/common/hooks/useOpenAtCursor.ts Outdated
@victor-enogwe victor-enogwe merged commit ad64c52 into main Dec 23, 2025
5 checks passed
@victor-enogwe victor-enogwe deleted the copilot/add-resizable-event-preview branch December 23, 2025 10:59
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.

Resizable event preview for event forms

3 participants