From 894210cd42d0663e2b5817e21b18c54ac47e03b3 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:50:11 +0700 Subject: [PATCH] fix(calendar): remove overlay swap and keep in-place zoom flow --- .../components/workspace/CrmWorkspaceApp.vue | 219 +-------------- .../workspace/calendar/CrmCalendarPanel.vue | 250 +++++++----------- 2 files changed, 109 insertions(+), 360 deletions(-) diff --git a/frontend/app/components/workspace/CrmWorkspaceApp.vue b/frontend/app/components/workspace/CrmWorkspaceApp.vue index 8842f2f..7176999 100644 --- a/frontend/app/components/workspace/CrmWorkspaceApp.vue +++ b/frontend/app/components/workspace/CrmWorkspaceApp.vue @@ -2309,7 +2309,6 @@ onBeforeUnmount(() => { clearInterval(lifecycleClock); lifecycleClock = null; } - clearCalendarZoomOverlay(); clearCalendarZoomPrime(); }); @@ -2319,7 +2318,7 @@ const selectedDateKey = ref(dayKey(new Date())); const sortedEvents = computed(() => [...calendarEvents.value].sort((a, b) => a.start.localeCompare(b.start))); const focusedCalendarEvent = computed(() => { - const id = focusedCalendarEventId.value.trim(); + const id = (focusedCalendarEventId.value ?? "").trim(); if (!id) return null; return sortedEvents.value.find((event) => event.id === id) ?? null; }); @@ -2356,15 +2355,10 @@ const calendarViewOptions: { value: CalendarView; label: string }[] = [ type CalendarHierarchyView = "year" | "month" | "week" | "day"; type CalendarRect = { left: number; top: number; width: number; height: number }; -type CalendarZoomGhost = { - title: string; - subtitle?: string; -}; const calendarContentWrapRef = ref(null); const calendarContentScrollRef = ref(null); const calendarSceneRef = ref(null); -const calendarZoomOverlayRef = ref(null); const calendarHoveredMonthIndex = ref(null); const calendarHoveredWeekStartKey = ref(""); const calendarHoveredDayKey = ref(""); @@ -2381,10 +2375,6 @@ function setCalendarSceneRef(element: HTMLElement | null) { calendarSceneRef.value = element; } -function setCalendarZoomOverlayRef(element: HTMLElement | null) { - calendarZoomOverlayRef.value = element; -} - function setCalendarHoveredMonthIndex(value: number | null) { calendarHoveredMonthIndex.value = value; } @@ -2403,14 +2393,6 @@ function onCalendarSceneMouseLeave() { calendarHoveredDayKey.value = ""; clearCalendarZoomPrime(); } -const calendarZoomOverlay = ref<{ active: boolean } & CalendarRect>({ - active: false, - left: 0, - top: 0, - width: 0, - height: 0, -}); -const calendarZoomGhost = ref(null); const calendarZoomBusy = ref(false); const calendarCameraState = ref({ active: false, @@ -2426,8 +2408,6 @@ let calendarWheelLockUntil = 0; let calendarZoomPrimeTimer: ReturnType | null = null; let calendarZoomPrimeLastAt = 0; const CALENDAR_ZOOM_DURATION_MS = 2400; -const CALENDAR_ZOOM_FOCUS_MS = 1400; -const CALENDAR_ZOOM_REVEAL_MS = Math.max(500, CALENDAR_ZOOM_DURATION_MS - CALENDAR_ZOOM_FOCUS_MS); const CALENDAR_ZOOM_PRIME_STEPS = 2; const CALENDAR_ZOOM_PRIME_MAX_SCALE = 1.05; const CALENDAR_ZOOM_PRIME_RESET_MS = 900; @@ -2437,12 +2417,6 @@ const normalizedCalendarView = computed(() => calendarView.value === "agenda" ? "month" : calendarView.value, ); const calendarZoomLevelIndex = computed(() => Math.max(0, calendarZoomOrder.indexOf(normalizedCalendarView.value))); -const calendarZoomOverlayStyle = computed(() => ({ - left: `${calendarZoomOverlay.value.left}px`, - top: `${calendarZoomOverlay.value.top}px`, - width: `${calendarZoomOverlay.value.width}px`, - height: `${calendarZoomOverlay.value.height}px`, -})); const calendarSceneTransformStyle = computed(() => { if (!calendarCameraState.value.active) return undefined; return { @@ -2456,14 +2430,6 @@ const calendarSceneTransformStyle = computed(() => { }; }); -function clearCalendarZoomOverlay() { - calendarZoomOverlay.value = { - ...calendarZoomOverlay.value, - active: false, - }; - calendarZoomGhost.value = null; -} - function clearCalendarZoomPrime() { if (calendarZoomPrimeTimer) { clearTimeout(calendarZoomPrimeTimer); @@ -2592,92 +2558,6 @@ function weekRowStartForDate(key: string) { return dayKey(date); } -function zoomGhostForMonth(monthIndex: number): CalendarZoomGhost { - const item = yearMonths.value.find((entry) => entry.monthIndex === monthIndex); - if (!item) { - return { - title: new Intl.DateTimeFormat("en-US", { month: "long" }).format(new Date(calendarCursor.value.getFullYear(), monthIndex, 1)), - subtitle: "", - }; - } - return { - title: item.label, - subtitle: `${item.count} events`, - }; -} - -function zoomGhostForWeek(startKey: string): CalendarZoomGhost { - const start = new Date(`${startKey}T00:00:00`); - const end = new Date(start); - end.setDate(start.getDate() + 6); - const row = monthRows.value.find((item) => item.startKey === startKey); - const count = row ? row.cells.reduce((sum, cell) => sum + cell.events.length, 0) : 0; - return { - title: `${formatDay(`${dayKey(start)}T00:00:00`)} - ${formatDay(`${dayKey(end)}T00:00:00`)}`, - subtitle: `${count} events`, - }; -} - -function zoomGhostForDay(dayKeyValue: string): CalendarZoomGhost { - const day = weekDays.value.find((entry) => entry.key === dayKeyValue); - if (!day) { - return { - title: formatDay(`${dayKeyValue}T00:00:00`), - subtitle: `${getEventsByDate(dayKeyValue).length} events`, - }; - } - return { - title: `${day.label} ${day.day}`, - subtitle: `${day.events.length} events`, - }; -} - -function zoomGhostForCurrentView(): CalendarZoomGhost { - if (calendarView.value === "day") { - return { - title: formatDay(`${selectedDateKey.value}T00:00:00`), - subtitle: `${selectedDayEvents.value.length} events`, - }; - } - if (calendarView.value === "week") { - return { - title: calendarPeriodLabel.value, - subtitle: `${weekDays.value.reduce((sum, day) => sum + day.events.length, 0)} events`, - }; - } - if (calendarView.value === "month" || calendarView.value === "agenda") { - return { - title: monthLabel.value, - subtitle: `${monthCells.value.reduce((sum, cell) => sum + cell.events.length, 0)} events`, - }; - } - return { - title: String(calendarCursor.value.getFullYear()), - subtitle: `${sortedEvents.value.filter((event) => new Date(event.start).getFullYear() === calendarCursor.value.getFullYear()).length} events`, - }; -} - -function primeCalendarRect(rect: CalendarRect) { - clearCalendarZoomOverlay(); - calendarZoomOverlay.value = { - active: true, - left: rect.left, - top: rect.top, - width: rect.width, - height: rect.height, - }; -} - -function morphCalendarRect(toRect: CalendarRect) { - calendarZoomOverlay.value = { - active: true, - left: toRect.left, - top: toRect.top, - width: toRect.width, - height: toRect.height, - }; -} - function nextAnimationFrame() { return new Promise((resolve) => { requestAnimationFrame(() => resolve()); @@ -2745,50 +2625,13 @@ async function resetCalendarCamera() { }; } -async function flushCalendarZoomStartFrame() { - await nextTick(); - await nextAnimationFrame(); - calendarZoomOverlayRef.value?.getBoundingClientRect(); - await nextAnimationFrame(); -} - -function waitCalendarZoomTransition() { - const overlay = calendarZoomOverlayRef.value; - if (!overlay) { - return new Promise((resolve) => { - setTimeout(() => resolve(), CALENDAR_ZOOM_DURATION_MS); - }); - } - return new Promise((resolve) => { - let settled = false; - const finish = () => { - if (settled) return; - settled = true; - overlay.removeEventListener("transitionend", onTransitionEnd); - clearTimeout(fallbackTimer); - resolve(); - }; - const onTransitionEnd = (event: TransitionEvent) => { - if (event.target !== overlay) return; - if (!["left", "top", "width", "height"].includes(event.propertyName)) return; - finish(); - }; - const fallbackTimer = setTimeout(() => finish(), CALENDAR_ZOOM_DURATION_MS + 140); - overlay.addEventListener("transitionend", onTransitionEnd); - }); -} - async function animateCalendarZoomIn( sourceElement: HTMLElement | null, - ghost: CalendarZoomGhost, apply: () => void, - resolveRevealTarget?: () => HTMLElement | null, ) { clearCalendarZoomPrime(); calendarZoomBusy.value = true; - clearCalendarZoomOverlay(); try { - calendarZoomGhost.value = ghost; const fromRect = getElementRectInScene(sourceElement) ?? fallbackZoomOriginRectInScene(); const cameraTarget = fromRect ? cameraTransformForRect(fromRect) : null; if (!cameraTarget) { @@ -2810,36 +2653,21 @@ async function animateCalendarZoomIn( left: cameraTarget.left, top: cameraTarget.top, scale: cameraTarget.scale, - durationMs: CALENDAR_ZOOM_FOCUS_MS, + durationMs: CALENDAR_ZOOM_DURATION_MS, }; await waitCalendarCameraTransition(); apply(); await nextTick(); - const revealTargetElement = resolveRevealTarget ? resolveRevealTarget() : sourceElement; - const revealTargetRect = getElementRectInScene(revealTargetElement) ?? fallbackZoomOriginRectInScene(); - const revealTarget = revealTargetRect ? cameraTransformForRect(revealTargetRect) : null; - if (revealTarget) { - calendarCameraState.value = { - active: true, - left: revealTarget.left, - top: revealTarget.top, - scale: revealTarget.scale, - durationMs: 0, - }; - await nextTick(); - await nextAnimationFrame(); - } calendarCameraState.value = { active: true, left: 0, top: 0, scale: 1, - durationMs: CALENDAR_ZOOM_REVEAL_MS, + durationMs: 0, }; - await waitCalendarCameraTransition(); + await nextAnimationFrame(); } finally { await resetCalendarCamera(); - calendarZoomGhost.value = null; calendarZoomBusy.value = false; } } @@ -2847,9 +2675,7 @@ async function animateCalendarZoomIn( async function animateCalendarZoomOut(apply: () => void, resolveTarget: () => HTMLElement | null) { clearCalendarZoomPrime(); calendarZoomBusy.value = true; - clearCalendarZoomOverlay(); try { - calendarZoomGhost.value = zoomGhostForCurrentView(); apply(); await nextTick(); const targetRect = getElementRectInScene(resolveTarget()) ?? fallbackZoomOriginRectInScene(); @@ -2875,7 +2701,6 @@ async function animateCalendarZoomOut(apply: () => void, resolveTarget: () => HT await waitCalendarCameraTransition(); } finally { await resetCalendarCamera(); - calendarZoomGhost.value = null; calendarZoomBusy.value = false; } } @@ -2923,9 +2748,9 @@ async function zoomInCalendar(event?: Event) { queryCalendarElement(`[data-calendar-month-index="${monthIndex}"]`) ?? queryCalendarElement("[data-calendar-month-index]"); if (maybePrimeWheelZoom(wheelEvent, calendarPrimeMonthToken(monthIndex))) return; - await animateCalendarZoomIn(sourceElement, zoomGhostForMonth(monthIndex), () => { + await animateCalendarZoomIn(sourceElement, () => { openYearMonth(monthIndex); - }, () => queryCalendarElement(`[data-calendar-month-index="${monthIndex}"]`)); + }); return; } @@ -2938,16 +2763,9 @@ async function zoomInCalendar(event?: Event) { queryCalendarElement("[data-calendar-week-start-key]") ?? queryCalendarElement("[data-calendar-day-key]"); if (maybePrimeWheelZoom(wheelEvent, calendarPrimeWeekToken(rowStartKey))) return; - await animateCalendarZoomIn( - sourceElement, - zoomGhostForWeek(rowStartKey), - () => { - openWeekView(anchorDayKey); - }, - () => - queryCalendarElement(`[data-calendar-week-start-key="${rowStartKey}"]`) ?? - queryCalendarElement(`[data-calendar-day-key="${anchorDayKey}"]`), - ); + await animateCalendarZoomIn(sourceElement, () => { + openWeekView(anchorDayKey); + }); return; } @@ -2955,21 +2773,16 @@ async function zoomInCalendar(event?: Event) { const dayAnchor = resolveDayAnchor(wheelEvent); const sourceElement = queryCalendarElement(`[data-calendar-day-key="${dayAnchor}"]`) ?? queryCalendarElement("[data-calendar-day-key]"); if (maybePrimeWheelZoom(wheelEvent, calendarPrimeDayToken(dayAnchor))) return; - await animateCalendarZoomIn(sourceElement, zoomGhostForDay(dayAnchor), () => { + await animateCalendarZoomIn(sourceElement, () => { openDayView(dayAnchor); - }, () => queryCalendarElement(`[data-calendar-day-key="${dayAnchor}"]`)); + }); } } async function zoomToMonth(monthIndex: number) { - await animateCalendarZoomIn( - queryCalendarElement(`[data-calendar-month-index="${monthIndex}"]`), - zoomGhostForMonth(monthIndex), - () => { - openYearMonth(monthIndex); - }, - () => queryCalendarElement(`[data-calendar-month-index="${monthIndex}"]`), - ); + await animateCalendarZoomIn(queryCalendarElement(`[data-calendar-month-index="${monthIndex}"]`), () => { + openYearMonth(monthIndex); + }); } async function zoomOutCalendar() { @@ -4950,10 +4763,6 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected") :week-days="weekDays" :calendar-prime-day-token="calendarPrimeDayToken" :selected-day-events="selectedDayEvents" - :calendar-zoom-overlay="calendarZoomOverlay" - :set-calendar-zoom-overlay-ref="setCalendarZoomOverlayRef" - :calendar-zoom-overlay-style="calendarZoomOverlayStyle" - :calendar-zoom-ghost="calendarZoomGhost" /> diff --git a/frontend/app/components/workspace/calendar/CrmCalendarPanel.vue b/frontend/app/components/workspace/calendar/CrmCalendarPanel.vue index d4f7b4c..0746084 100644 --- a/frontend/app/components/workspace/calendar/CrmCalendarPanel.vue +++ b/frontend/app/components/workspace/calendar/CrmCalendarPanel.vue @@ -79,10 +79,6 @@ defineProps<{ weekDays: WeekDay[]; calendarPrimeDayToken: (dayKey: string) => string; selectedDayEvents: CalendarEvent[]; - calendarZoomOverlay: { active: boolean }; - setCalendarZoomOverlayRef: (element: HTMLDivElement | null) => void; - calendarZoomOverlayStyle: Record; - calendarZoomGhost: { title: string; subtitle?: string } | null; }>(); @@ -199,134 +195,116 @@ defineProps<{ {{ formatDay(item.first.start) }} ยท {{ item.first.title }} -
-
- Sun - Mon - Tue - Wed - Thu - Fri - Sat -
+
+
+
+ Sun + Mon + Tue + Wed + Thu + Fri + Sat +
-
-
-
- - +
-
-
-
-
-
-

{{ day.label }} {{ day.day }}

-
-
- -

No events

-
-
+
+
+
+
+

{{ day.label }} {{ day.day }}

+
+
+ +

No events

+
+
+
-
-
- -

No events on this day.

+
+ +

No events on this day.

+
-
-
-

{{ calendarZoomGhost.title }}

-

{{ calendarZoomGhost.subtitle }}

-
-
@@ -492,44 +470,6 @@ defineProps<{ background: color-mix(in oklab, var(--color-base-content) 85%, transparent); } -.calendar-zoom-overlay { - position: absolute; - z-index: 18; - border: 1px solid color-mix(in oklab, var(--color-primary) 62%, transparent); - border-radius: 14px; - background: - radial-gradient(circle at 20% 20%, color-mix(in oklab, var(--color-primary) 30%, transparent), transparent 55%), - color-mix(in oklab, var(--color-base-100) 84%, transparent); - box-shadow: - 0 0 0 1px color-mix(in oklab, var(--color-primary) 42%, transparent) inset, - 0 18px 38px rgba(18, 30, 58, 0.28); - pointer-events: none; - overflow: hidden; -} - -.calendar-zoom-overlay-content { - position: absolute; - inset: 0; - display: flex; - flex-direction: column; - justify-content: flex-start; - gap: 4px; - padding: 14px; - color: color-mix(in oklab, var(--color-base-content) 90%, transparent); -} - -.calendar-zoom-overlay-title { - margin: 0; - font-size: 13px; - font-weight: 600; -} - -.calendar-zoom-overlay-subtitle { - margin: 0; - font-size: 11px; - opacity: 0.72; -} - @media (max-width: 960px) { .calendar-content-wrap { padding-left: 32px;