From 9505cecab2bfce4c6e7a31497b56fa44a1fc99ec Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Tue, 24 Feb 2026 14:28:31 +0700 Subject: [PATCH] feat(calendar): header continuity with week numbers + skeleton content in fly-rect - Add ISO week numbers to the left of week rows in month view (8, 9, 10...) with spacer alignment on day-of-week headers - Inject label + skeleton placeholder lines into fly-rect during zoom animations: zoom-in shows source label (month name / "Week N" / day name) + pulsing bars zoom-out shows target context label + skeleton - Skeleton CSS uses pulse animation (0.8s alternate) for loading hint - Non-scoped style block for dynamically injected innerHTML elements - isoWeekNumber helper for ISO 8601 week calculation - Extended MonthRow type with weekNumber property Co-Authored-By: Claude Opus 4.6 --- .../components/workspace/CrmWorkspaceApp.vue | 68 ++++++++++++++-- .../workspace/calendar/CrmCalendarPanel.vue | 78 ++++++++++++++++--- 2 files changed, 131 insertions(+), 15 deletions(-) diff --git a/frontend/app/components/workspace/CrmWorkspaceApp.vue b/frontend/app/components/workspace/CrmWorkspaceApp.vue index f5fd92c..ff537d7 100644 --- a/frontend/app/components/workspace/CrmWorkspaceApp.vue +++ b/frontend/app/components/workspace/CrmWorkspaceApp.vue @@ -2672,6 +2672,14 @@ function weekRowStartForDate(key: string) { return dayKey(date); } +function isoWeekNumber(dateString: string): number { + const d = new Date(`${dateString}T00:00:00`); + const t = new Date(d.getTime()); + t.setDate(t.getDate() + 3 - ((t.getDay() + 6) % 7)); + const y = new Date(t.getFullYear(), 0, 4); + return 1 + Math.round(((t.getTime() - y.getTime()) / 86400000 - 3 + ((y.getDay() + 6) % 7)) / 7); +} + function nextAnimationFrame() { return new Promise((resolve) => { requestAnimationFrame(() => resolve()); @@ -2695,6 +2703,48 @@ function resetFlyRectStyle(flyEl: HTMLElement) { flyEl.style.backgroundColor = ""; flyEl.style.borderRadius = ""; flyEl.style.boxShadow = ""; + flyEl.innerHTML = ""; +} + +function extractSourceLabel(sourceElement: HTMLElement, viewBefore: string): string { + if (viewBefore === "year") { + const p = sourceElement.querySelector("p"); + return p?.textContent?.trim() ?? ""; + } + if (viewBefore === "month" || viewBefore === "agenda") { + const wn = sourceElement.querySelector(".calendar-week-number"); + return wn ? `Week ${wn.textContent?.trim()}` : ""; + } + if (viewBefore === "week") { + const p = sourceElement.querySelector("p"); + return p?.textContent?.trim() ?? ""; + } + return ""; +} + +function buildFlyRectContent(labelText: string): string { + const skeleton = ` +
+
+
+
+
+ `; + return `

${labelText}

${skeleton}
`; +} + +function buildFlyRectContentForZoomOut(currentView: string): string { + let labelText = ""; + if (currentView === "day") { + const d = new Date(`${selectedDateKey.value}T00:00:00`); + labelText = new Intl.DateTimeFormat("en-US", { weekday: "short" }).format(d) + " " + d.getDate(); + } else if (currentView === "week") { + const weekStart = weekRowStartForDate(selectedDateKey.value); + labelText = `Week ${isoWeekNumber(weekStart)}`; + } else if (currentView === "month" || currentView === "agenda") { + labelText = new Intl.DateTimeFormat("en-US", { month: "long" }).format(calendarCursor.value); + } + return buildFlyRectContent(labelText); } async function animateCalendarFlipTransition( @@ -2740,6 +2790,8 @@ async function animateCalendarFlipTransition( flyEl.style.borderColor = "color-mix(in oklab, var(--color-base-300) 100%, transparent)"; flyEl.style.backgroundColor = "color-mix(in oklab, var(--color-base-100) 100%, transparent)"; flyEl.style.boxShadow = ""; + // Inject label + skeleton for zoom-out + flyEl.innerHTML = buildFlyRectContentForZoomOut(calendarView.value); calendarFlyVisible.value = true; // 3. Switch to parent view @@ -2815,18 +2867,22 @@ async function animateCalendarZoomIntoSource( return; } - // 1. Fade out siblings + // 1. Extract label before fading children + const labelText = extractSourceLabel(sourceElement, calendarView.value); + + // 2. Fade out siblings const siblings = Array.from( sceneEl?.querySelectorAll(".calendar-hover-targetable") ?? [], ).filter((el) => el !== sourceElement && !sourceElement.contains(el) && !el.contains(sourceElement)); await calendarTweenTo(siblings, { opacity: 0, duration: CALENDAR_FADE_DURATION, ease: "power2.in" }); - // 2. Fade out source element's inner content (keep border/bg visible) + // 3. Fade out source element's inner content (keep border/bg visible) const sourceChildren = Array.from(sourceElement.children) as HTMLElement[]; await calendarTweenTo(sourceChildren, { opacity: 0, duration: 0.12, ease: "power2.in" }); - // 3. Clone source visual style to fly-rect, position at source bounds + // 4. Clone source visual style to fly-rect, inject label + skeleton cloneElementStyleToFlyRect(sourceElement, flyEl); + flyEl.innerHTML = buildFlyRectContent(labelText); const srcLeft = sourceRect.left - wrapRect.left; const srcTop = sourceRect.top - wrapRect.top; gsap.set(flyEl, { @@ -3067,13 +3123,15 @@ const monthCells = computed(() => { }); const monthRows = computed(() => { - const rows: Array<{ key: string; startKey: string; cells: typeof monthCells.value }> = []; + const rows: Array<{ key: string; startKey: string; weekNumber: number; cells: typeof monthCells.value }> = []; for (let index = 0; index < monthCells.value.length; index += 7) { const cells = monthCells.value.slice(index, index + 7); if (!cells.length) continue; + const startKey = cells[0]?.key ?? selectedDateKey.value; rows.push({ key: `${cells[0]?.key ?? index}-week-row`, - startKey: cells[0]?.key ?? selectedDateKey.value, + startKey, + weekNumber: isoWeekNumber(startKey), cells, }); } diff --git a/frontend/app/components/workspace/calendar/CrmCalendarPanel.vue b/frontend/app/components/workspace/calendar/CrmCalendarPanel.vue index e685706..450a258 100644 --- a/frontend/app/components/workspace/calendar/CrmCalendarPanel.vue +++ b/frontend/app/components/workspace/calendar/CrmCalendarPanel.vue @@ -25,6 +25,7 @@ type MonthCell = { type MonthRow = { key: string; startKey: string; + weekNumber: number; cells: MonthCell[]; }; @@ -220,21 +221,24 @@ defineProps<{ data-calendar-layer="month" :class="calendarView === 'month' || calendarView === 'agenda' ? 'calendar-depth-layer-active' : 'calendar-depth-layer-hidden'" > -
- Sun - Mon - Tue - Wed - Thu - Fri - Sat +
+ +
+ Sun + Mon + Tue + Wed + Thu + Fri + Sat +
-
+ {{ row.weekNumber }} +