calendar: replace fade transitions with zoom slider flow

This commit is contained in:
Ruslan Bakiev
2026-02-22 15:18:54 +07:00
parent 25a5e83f95
commit fedc76c6f5

View File

@@ -2072,6 +2072,7 @@ onBeforeUnmount(() => {
clearInterval(lifecycleClock);
lifecycleClock = null;
}
clearCalendarZoomOverlay();
});
const calendarView = ref<CalendarView>("year");
@@ -2115,32 +2116,154 @@ const calendarViewOptions: { value: CalendarView; label: string }[] = [
{ value: "agenda", label: "Agenda" },
];
type CalendarTransitionDirection = "in" | "out" | "side";
type CalendarHierarchyView = "year" | "month" | "week" | "day";
type CalendarRect = { left: number; top: number; width: number; height: number };
const calendarTransitionDirection = ref<CalendarTransitionDirection>("side");
const calendarContentWrapRef = ref<HTMLElement | null>(null);
const calendarHoveredMonthIndex = ref<number | null>(null);
const calendarHoveredWeekStartKey = ref("");
const calendarHoveredDayKey = ref("");
const calendarZoomOverlay = ref<{ active: boolean } & CalendarRect>({
active: false,
left: 0,
top: 0,
width: 0,
height: 0,
});
let calendarWheelLockUntil = 0;
let calendarZoomOverlayTimer: ReturnType<typeof setTimeout> | null = null;
const CALENDAR_ZOOM_DURATION_MS = 180;
const calendarZoomStops: Array<{ view: CalendarHierarchyView; label: string }> = [
{ view: "year", label: "Year" },
{ view: "month", label: "Month" },
{ view: "week", label: "Week" },
{ view: "day", label: "Day" },
];
const calendarZoomOrder: CalendarHierarchyView[] = ["year", "month", "week", "day"];
const calendarSceneKey = computed(() => `${calendarView.value}-${calendarRouteToken(calendarView.value)}`);
const calendarTransitionName = computed(() => {
if (calendarTransitionDirection.value === "in") return "calendar-zoom-in";
if (calendarTransitionDirection.value === "out") return "calendar-zoom-out";
return "calendar-zoom-side";
});
const normalizedCalendarView = computed<CalendarHierarchyView>(() =>
calendarView.value === "agenda" ? "month" : calendarView.value,
);
const calendarZoomLevelIndex = computed(() =>
Math.max(0, calendarZoomStops.findIndex((stop) => stop.view === 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 canCalendarZoomIn = computed(() => calendarView.value !== "day");
const canCalendarZoomOut = computed(() => calendarView.value !== "year");
const calendarZoomDepthLabel = computed(() => {
if (calendarView.value === "day") return "Day";
if (calendarView.value === "week") return "Week";
if (calendarView.value === "month" || calendarView.value === "agenda") return "Month";
return "Months";
});
function clearCalendarZoomOverlay() {
if (calendarZoomOverlayTimer) {
clearTimeout(calendarZoomOverlayTimer);
calendarZoomOverlayTimer = null;
}
}
function setCalendarTransition(direction: CalendarTransitionDirection) {
calendarTransitionDirection.value = direction;
function queryCalendarElement(selector: string) {
return calendarContentWrapRef.value?.querySelector<HTMLElement>(selector) ?? null;
}
function getCalendarViewportRect(): CalendarRect | null {
const wrapRect = calendarContentWrapRef.value?.getBoundingClientRect();
if (!wrapRect) return null;
return {
left: 0,
top: 0,
width: Math.max(24, wrapRect.width),
height: Math.max(24, wrapRect.height),
};
}
function getElementRectInCalendar(element: HTMLElement | null): CalendarRect | null {
if (!element) return null;
const wrapRect = calendarContentWrapRef.value?.getBoundingClientRect();
if (!wrapRect) return null;
const rect = element.getBoundingClientRect();
const left = Math.max(0, rect.left - wrapRect.left);
const top = Math.max(0, rect.top - wrapRect.top);
const width = Math.max(24, Math.min(rect.width, wrapRect.width - left));
const height = Math.max(24, Math.min(rect.height, wrapRect.height - top));
return { left, top, width, height };
}
function weekRowStartForDate(key: string) {
const date = new Date(`${key}T00:00:00`);
date.setDate(date.getDate() - date.getDay());
return dayKey(date);
}
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) {
requestAnimationFrame(() => {
calendarZoomOverlay.value = {
active: true,
left: toRect.left,
top: toRect.top,
width: toRect.width,
height: toRect.height,
};
});
calendarZoomOverlayTimer = setTimeout(() => {
calendarZoomOverlay.value = {
...calendarZoomOverlay.value,
active: false,
};
}, CALENDAR_ZOOM_DURATION_MS + 40);
}
function waitCalendarZoom() {
return new Promise<void>((resolve) => {
setTimeout(() => resolve(), CALENDAR_ZOOM_DURATION_MS);
});
}
async function animateCalendarZoomIn(sourceElement: HTMLElement | null, apply: () => void) {
const fromRect = getElementRectInCalendar(sourceElement);
const viewportRect = getCalendarViewportRect();
if (!fromRect || !viewportRect) {
apply();
return;
}
primeCalendarRect(fromRect);
apply();
await nextTick();
morphCalendarRect(viewportRect);
await waitCalendarZoom();
}
async function animateCalendarZoomOut(apply: () => void, resolveTarget: () => HTMLElement | null) {
const viewportRect = getCalendarViewportRect();
if (!viewportRect) {
apply();
return;
}
primeCalendarRect(viewportRect);
apply();
await nextTick();
const targetRect = getElementRectInCalendar(resolveTarget());
if (!targetRect) {
calendarZoomOverlay.value = {
...calendarZoomOverlay.value,
active: false,
};
return;
}
morphCalendarRect(targetRect);
await waitCalendarZoom();
}
function resolveMonthAnchor(event?: WheelEvent) {
@@ -2171,41 +2294,78 @@ function resolveDayAnchor(event?: WheelEvent) {
return selectedDateKey.value;
}
function zoomInCalendar(event?: Event) {
async function zoomInCalendar(event?: Event) {
const wheelEvent = event instanceof WheelEvent ? event : undefined;
if (calendarView.value === "year") {
openYearMonth(resolveMonthAnchor(wheelEvent), "in");
const monthIndex = resolveMonthAnchor(wheelEvent);
await animateCalendarZoomIn(queryCalendarElement(`[data-calendar-month-index="${monthIndex}"]`), () => {
openYearMonth(monthIndex);
});
return;
}
if (calendarView.value === "month" || calendarView.value === "agenda") {
openWeekView(resolveWeekAnchor(wheelEvent), "in");
const anchorDayKey = resolveWeekAnchor(wheelEvent);
const rowStartKey = weekRowStartForDate(anchorDayKey);
await animateCalendarZoomIn(
queryCalendarElement(`[data-calendar-week-start-key="${rowStartKey}"]`) ??
queryCalendarElement(`[data-calendar-day-key="${anchorDayKey}"]`),
() => {
openWeekView(anchorDayKey);
},
);
return;
}
if (calendarView.value === "week") {
openDayView(resolveDayAnchor(wheelEvent), "in");
const dayAnchor = resolveDayAnchor(wheelEvent);
await animateCalendarZoomIn(queryCalendarElement(`[data-calendar-day-key="${dayAnchor}"]`), () => {
openDayView(dayAnchor);
});
}
}
function zoomOutCalendar() {
async function zoomToMonth(monthIndex: number) {
await animateCalendarZoomIn(queryCalendarElement(`[data-calendar-month-index="${monthIndex}"]`), () => {
openYearMonth(monthIndex);
});
}
async function zoomOutCalendar() {
focusedCalendarEventId.value = "";
if (calendarView.value === "day") {
setCalendarTransition("out");
calendarView.value = "week";
const targetDayKey = selectedDateKey.value;
await animateCalendarZoomOut(
() => {
calendarView.value = "week";
},
() => queryCalendarElement(`[data-calendar-day-key="${targetDayKey}"]`),
);
return;
}
if (calendarView.value === "week") {
setCalendarTransition("out");
calendarView.value = "month";
const targetRowKey = weekRowStartForDate(selectedDateKey.value);
await animateCalendarZoomOut(
() => {
calendarView.value = "month";
},
() =>
queryCalendarElement(`[data-calendar-week-start-key="${targetRowKey}"]`) ??
queryCalendarElement(`[data-calendar-day-key="${selectedDateKey.value}"]`),
);
return;
}
if (calendarView.value === "month" || calendarView.value === "agenda") {
setCalendarTransition("out");
calendarView.value = "year";
const targetMonthIndex = calendarCursor.value.getMonth();
await animateCalendarZoomOut(
() => {
calendarView.value = "year";
},
() => queryCalendarElement(`[data-calendar-month-index="${targetMonthIndex}"]`),
);
}
}
@@ -2216,11 +2376,36 @@ function onCalendarHierarchyWheel(event: WheelEvent) {
calendarWheelLockUntil = now + 240;
if (event.deltaY < 0) {
zoomInCalendar(event);
void zoomInCalendar(event);
return;
}
zoomOutCalendar();
void zoomOutCalendar();
}
async function setCalendarZoomLevel(targetView: CalendarHierarchyView) {
let currentIndex = calendarZoomOrder.indexOf(normalizedCalendarView.value);
const targetIndex = calendarZoomOrder.indexOf(targetView);
if (currentIndex < 0 || targetIndex < 0 || currentIndex === targetIndex) return;
while (currentIndex !== targetIndex) {
if (targetIndex > currentIndex) {
await zoomInCalendar();
} else {
await zoomOutCalendar();
}
currentIndex = calendarZoomOrder.indexOf(normalizedCalendarView.value);
}
}
function onCalendarZoomSliderInput(event: Event) {
const value = Number((event.target as HTMLInputElement | null)?.value ?? NaN);
if (!Number.isFinite(value)) return;
const sliderStep = Math.max(0, Math.min(3, Math.round(value)));
const targetIndex = 3 - sliderStep;
const targetView = calendarZoomOrder[targetIndex];
if (!targetView) return;
void setCalendarZoomLevel(targetView);
}
const monthCells = computed(() => {
@@ -2338,7 +2523,6 @@ const selectedDayEvents = computed(() => getEventsByDate(selectedDateKey.value))
function shiftCalendar(step: number) {
focusedCalendarEventId.value = "";
setCalendarTransition("side");
if (calendarView.value === "year") {
const next = new Date(calendarCursor.value);
next.setFullYear(next.getFullYear() + step);
@@ -2366,7 +2550,6 @@ function shiftCalendar(step: number) {
function setToday() {
focusedCalendarEventId.value = "";
setCalendarTransition("side");
const now = new Date();
selectedDateKey.value = dayKey(now);
calendarCursor.value = new Date(now.getFullYear(), now.getMonth(), 1);
@@ -2379,24 +2562,21 @@ function pickDate(key: string) {
calendarCursor.value = new Date(d.getFullYear(), d.getMonth(), 1);
}
function openDayView(key: string, direction: CalendarTransitionDirection = "in") {
function openDayView(key: string) {
pickDate(key);
setCalendarTransition(direction);
calendarView.value = "day";
}
function openWeekView(key: string, direction: CalendarTransitionDirection = "in") {
function openWeekView(key: string) {
pickDate(key);
setCalendarTransition(direction);
calendarView.value = "week";
}
function openYearMonth(monthIndex: number, direction: CalendarTransitionDirection = "in") {
function openYearMonth(monthIndex: number) {
focusedCalendarEventId.value = "";
const year = calendarCursor.value.getFullYear();
calendarCursor.value = new Date(year, monthIndex, 1);
selectedDateKey.value = dayKey(new Date(year, monthIndex, 1));
setCalendarTransition(direction);
calendarView.value = "month";
}
@@ -4072,7 +4252,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
v-if="contextPickerEnabled"
class="context-scope-label"
>{{ contextScopeLabel('calendar') }}</span>
<div class="grid grid-cols-[auto_1fr_auto] items-center gap-2">
<div class="grid grid-cols-[auto_1fr] items-center gap-2">
<div class="flex items-center gap-1">
<button class="btn btn-xs" @click="setToday">Today</button>
</div>
@@ -4081,14 +4261,6 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
{{ calendarPeriodLabel }}
</div>
<div class="justify-self-end flex items-center gap-2">
<div class="hidden items-center gap-2 rounded-lg border border-base-300 bg-base-100 px-2 py-1 sm:flex">
<span class="text-[10px] font-semibold uppercase tracking-wide text-base-content/60">Wheel to zoom</span>
<span class="badge badge-ghost badge-xs min-w-[3.2rem] justify-center">{{ calendarZoomDepthLabel }}</span>
</div>
<button class="btn btn-xs btn-ghost" :disabled="!canCalendarZoomOut" title="Zoom out" @click.stop="zoomOutCalendar"></button>
<button class="btn btn-xs btn-ghost" :disabled="!canCalendarZoomIn" title="Zoom in" @click.stop="zoomInCalendar">+</button>
</div>
</div>
<article
@@ -4103,7 +4275,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
<p class="mt-1 text-xs text-base-content/80">{{ focusedCalendarEvent.note || "No note" }}</p>
</article>
<div class="calendar-content-wrap min-h-0 flex-1">
<div ref="calendarContentWrapRef" class="calendar-content-wrap min-h-0 flex-1">
<button
class="calendar-side-nav calendar-side-nav-left"
type="button"
@@ -4112,24 +4284,43 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
>
<span></span>
</button>
<button
class="calendar-side-nav calendar-side-nav-right"
<button
class="calendar-side-nav calendar-side-nav-right"
type="button"
title="Next period"
@click="shiftCalendar(1)"
>
<span></span>
</button>
<div
class="calendar-content-scroll min-h-0 h-full overflow-y-auto pr-1"
@wheel.prevent="onCalendarHierarchyWheel"
>
<Transition :name="calendarTransitionName" mode="out-in">
<div
:key="calendarSceneKey"
class="calendar-scene"
@mouseleave="calendarHoveredMonthIndex = null; calendarHoveredWeekStartKey = ''; calendarHoveredDayKey = ''"
>
<span></span>
</button>
<div class="calendar-zoom-slider-shell" @click.stop>
<input
class="calendar-zoom-slider"
type="range"
min="0"
max="3"
step="1"
:value="3 - calendarZoomLevelIndex"
aria-label="Calendar zoom level"
@input="onCalendarZoomSliderInput"
>
<div class="calendar-zoom-slider-labels" aria-hidden="true">
<span
v-for="item in calendarZoomStops"
:key="`calendar-zoom-label-${item.view}`"
:class="item.view === normalizedCalendarView ? 'calendar-zoom-slider-label-active' : ''"
>
{{ item.label }}
</span>
</div>
</div>
<div
class="calendar-content-scroll min-h-0 h-full overflow-y-auto pr-1"
@wheel.prevent="onCalendarHierarchyWheel"
>
<div
:class="normalizedCalendarView === 'day' ? 'calendar-scene cursor-zoom-out' : 'calendar-scene cursor-zoom-in'"
@mouseleave="calendarHoveredMonthIndex = null; calendarHoveredWeekStartKey = ''; calendarHoveredDayKey = ''"
>
<div v-if="calendarView === 'month'" class="space-y-1">
<div class="grid grid-cols-7 gap-1 text-center text-xs font-semibold text-base-content/60">
<span>Sun</span>
@@ -4149,17 +4340,6 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
:data-calendar-week-start-key="row.startKey"
@mouseenter="calendarHoveredWeekStartKey = row.startKey"
>
<button
type="button"
class="calendar-hover-jump calendar-hover-jump-row"
title="Expand week vertically"
aria-label="Expand week vertically"
@click.stop="openWeekView(row.startKey)"
>
<svg viewBox="0 0 20 20" class="h-3.5 w-3.5 fill-none stroke-current" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M10 2.5v15M6.2 6.4 10 2.5l3.8 3.9M6.2 13.6 10 17.5l3.8-3.9" />
</svg>
</button>
<div class="grid grid-cols-7 gap-1">
<button
v-for="cell in row.cells"
@@ -4175,17 +4355,6 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
@click="pickDate(cell.key)"
>
<p class="mb-1 text-xs font-semibold">{{ cell.day }}</p>
<button
type="button"
class="calendar-hover-jump"
title="Expand to day"
aria-label="Expand to day"
@click.stop="openDayView(cell.key)"
>
<svg viewBox="0 0 20 20" class="h-3.5 w-3.5 fill-none stroke-current" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M7 13L3 17M13 7l4-4M3 13V17h4M17 7V3h-4" />
</svg>
</button>
<button
v-for="event in monthCellEvents(cell.events)"
:key="event.id"
@@ -4214,17 +4383,6 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
>
<div class="mb-2 flex items-start justify-between gap-2">
<p class="text-sm font-semibold leading-tight">{{ day.label }} {{ day.day }}</p>
<button
type="button"
class="calendar-hover-jump calendar-hover-jump-week"
title="Expand day line"
aria-label="Expand day line"
@click.stop="openDayView(day.key)"
>
<svg viewBox="0 0 20 20" class="h-3.5 w-3.5 fill-none stroke-current" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M10 3v14M6 7l4-4 4 4M6 13l4 4 4-4" />
</svg>
</button>
</div>
<div class="space-y-1.5">
<button
@@ -4265,19 +4423,8 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
class="group relative rounded-xl border border-base-300 p-3 text-left transition hover:border-primary/50 hover:bg-primary/5 cursor-zoom-in"
:data-calendar-month-index="item.monthIndex"
@mouseenter="calendarHoveredMonthIndex = item.monthIndex"
@click="openYearMonth(item.monthIndex)"
@click="zoomToMonth(item.monthIndex)"
>
<button
type="button"
class="calendar-hover-jump calendar-hover-jump-month"
title="Expand month diagonally"
aria-label="Expand month diagonally"
@click.stop="openYearMonth(item.monthIndex)"
>
<svg viewBox="0 0 20 20" class="h-3.5 w-3.5 fill-none stroke-current" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M7 13L3 17M13 7l4-4M3 13V17h4M17 7V3h-4" />
</svg>
</button>
<p class="font-medium">{{ item.label }}</p>
<p class="text-xs text-base-content/60">{{ item.count }} events</p>
<button
@@ -4304,11 +4451,15 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
<p class="mt-1 text-sm text-base-content/80">{{ event.note }}</p>
</button>
</div>
</div>
</Transition>
</div>
</div>
</section>
</div>
</div>
<div
v-if="calendarZoomOverlay.active"
class="calendar-zoom-overlay"
:style="calendarZoomOverlayStyle"
/>
</div>
</section>
<section v-else-if="selectedTab === 'communications' && false" class="space-y-3">
<div class="mb-1 flex justify-end">
@@ -5374,7 +5525,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
.calendar-content-wrap {
position: relative;
padding-left: 40px;
padding-right: 40px;
padding-right: 128px;
}
.calendar-content-scroll {
@@ -5386,7 +5537,16 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
min-height: 100%;
min-width: 100%;
transform-origin: center center;
will-change: transform, opacity;
}
.calendar-scene.cursor-zoom-in,
.calendar-scene.cursor-zoom-in * {
cursor: zoom-in;
}
.calendar-scene.cursor-zoom-out,
.calendar-scene.cursor-zoom-out * {
cursor: zoom-out;
}
.calendar-week-grid {
@@ -5424,117 +5584,102 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
}
.calendar-side-nav-right {
right: 4px;
right: 56px;
}
.calendar-hover-jump {
.calendar-zoom-slider-shell {
position: absolute;
top: 4px;
right: 4px;
z-index: 3;
display: inline-flex;
top: 50%;
right: 8px;
z-index: 5;
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
min-width: 26px;
width: 26px;
height: 26px;
padding: 0;
border-radius: 8px;
border: 1px solid color-mix(in oklab, var(--color-primary) 35%, transparent);
background: color-mix(in oklab, var(--color-base-100) 86%, transparent);
color: color-mix(in oklab, var(--color-base-content) 78%, transparent);
opacity: 0;
pointer-events: none;
transition: opacity 120ms ease, transform 120ms ease, background-color 120ms ease;
transform: translateY(-2px);
gap: 8px;
border: 1px solid color-mix(in oklab, var(--color-base-content) 16%, transparent);
border-radius: 12px;
background: color-mix(in oklab, var(--color-base-100) 90%, transparent);
padding: 8px 7px;
}
.calendar-zoom-slider {
width: 124px;
height: 16px;
margin: 0;
transform: rotate(-90deg);
transform-origin: center;
accent-color: color-mix(in oklab, var(--color-primary) 82%, transparent);
cursor: pointer;
}
.group:hover > .calendar-hover-jump,
.group:focus-within > .calendar-hover-jump {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
.calendar-zoom-slider:focus-visible {
outline: 2px solid color-mix(in oklab, var(--color-primary) 52%, transparent);
outline-offset: 2px;
}
.calendar-hover-jump:focus-visible {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
outline: 2px solid color-mix(in oklab, var(--color-primary) 58%, transparent);
outline-offset: 1px;
.calendar-zoom-slider::-webkit-slider-runnable-track {
height: 4px;
border-radius: 999px;
background: color-mix(in oklab, var(--color-base-content) 26%, transparent);
}
.calendar-hover-jump:hover {
background: color-mix(in oklab, var(--color-primary) 12%, var(--color-base-100));
.calendar-zoom-slider::-webkit-slider-thumb {
-webkit-appearance: none;
margin-top: -5px;
width: 14px;
height: 14px;
border-radius: 999px;
border: 1px solid color-mix(in oklab, var(--color-primary) 42%, transparent);
background: color-mix(in oklab, var(--color-primary) 86%, var(--color-base-100));
}
.calendar-hover-jump-week,
.calendar-hover-jump-row {
top: 8px;
right: 10px;
.calendar-zoom-slider::-moz-range-track {
height: 4px;
border-radius: 999px;
background: color-mix(in oklab, var(--color-base-content) 26%, transparent);
}
.calendar-hover-jump-month {
top: 8px;
right: 8px;
.calendar-zoom-slider::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 999px;
border: 1px solid color-mix(in oklab, var(--color-primary) 42%, transparent);
background: color-mix(in oklab, var(--color-primary) 86%, var(--color-base-100));
}
.calendar-hover-jump-row {
top: 50%;
right: -14px;
transform: translate(6px, -50%);
.calendar-zoom-slider-labels {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 124px;
font-size: 10px;
line-height: 1;
color: color-mix(in oklab, var(--color-base-content) 56%, transparent);
}
.group:hover > .calendar-hover-jump-row,
.group:focus-within > .calendar-hover-jump-row,
.calendar-hover-jump-row:focus-visible {
transform: translate(0, -50%);
.calendar-zoom-slider-label-active {
color: color-mix(in oklab, var(--color-primary) 88%, var(--color-base-content));
font-weight: 700;
}
.calendar-zoom-in-enter-active,
.calendar-zoom-in-leave-active,
.calendar-zoom-out-enter-active,
.calendar-zoom-out-leave-active,
.calendar-zoom-side-enter-active,
.calendar-zoom-side-leave-active {
transition: transform 230ms cubic-bezier(0.2, 0.86, 0.2, 1), opacity 230ms ease;
}
.calendar-zoom-in-enter-from {
opacity: 0;
transform: scale(0.92) translateY(8px);
}
.calendar-zoom-in-leave-to {
opacity: 0;
transform: scale(1.07) translateY(-8px);
}
.calendar-zoom-out-enter-from {
opacity: 0;
transform: scale(1.07) translateY(-8px);
}
.calendar-zoom-out-leave-to {
opacity: 0;
transform: scale(0.92) translateY(8px);
}
.calendar-zoom-side-enter-from {
opacity: 0;
transform: translateY(8px);
}
.calendar-zoom-side-leave-to {
opacity: 0;
transform: translateY(-8px);
.calendar-zoom-overlay {
position: absolute;
z-index: 6;
border: 2px solid color-mix(in oklab, var(--color-primary) 60%, transparent);
border-radius: 12px;
background: color-mix(in oklab, var(--color-primary) 12%, transparent);
pointer-events: none;
transition:
left 180ms cubic-bezier(0.2, 0.85, 0.25, 1),
top 180ms cubic-bezier(0.2, 0.85, 0.25, 1),
width 180ms cubic-bezier(0.2, 0.85, 0.25, 1),
height 180ms cubic-bezier(0.2, 0.85, 0.25, 1);
}
@media (max-width: 960px) {
.calendar-content-wrap {
padding-left: 32px;
padding-right: 32px;
padding-right: 104px;
}
.calendar-week-grid {
@@ -5546,6 +5691,25 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
width: 24px;
height: 24px;
}
.calendar-side-nav-right {
right: 44px;
}
.calendar-zoom-slider-shell {
right: 4px;
padding: 7px 6px;
gap: 6px;
}
.calendar-zoom-slider {
width: 102px;
}
.calendar-zoom-slider-labels {
height: 102px;
font-size: 9px;
}
}
.pilot-shell {