calendar: move zoom slider to top-right horizontal control

This commit is contained in:
Ruslan Bakiev
2026-02-22 15:27:26 +07:00
parent fedc76c6f5
commit c4ef4d4297

View File

@@ -2133,20 +2133,12 @@ const calendarZoomOverlay = ref<{ active: boolean } & CalendarRect>({
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 normalizedCalendarView = computed<CalendarHierarchyView>(() =>
calendarView.value === "agenda" ? "month" : calendarView.value,
);
const calendarZoomLevelIndex = computed(() =>
Math.max(0, calendarZoomStops.findIndex((stop) => stop.view === normalizedCalendarView.value)),
);
const calendarZoomLevelIndex = computed(() => Math.max(0, calendarZoomOrder.indexOf(normalizedCalendarView.value)));
const calendarZoomOverlayStyle = computed(() => ({
left: `${calendarZoomOverlay.value.left}px`,
top: `${calendarZoomOverlay.value.top}px`,
@@ -2401,8 +2393,7 @@ async function setCalendarZoomLevel(targetView: CalendarHierarchyView) {
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 targetIndex = Math.max(0, Math.min(3, Math.round(value)));
const targetView = calendarZoomOrder[targetIndex];
if (!targetView) return;
void setCalendarZoomLevel(targetView);
@@ -4252,7 +4243,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] items-center gap-2">
<div class="grid grid-cols-[auto_1fr_auto] items-center gap-2">
<div class="flex items-center gap-1">
<button class="btn btn-xs" @click="setToday">Today</button>
</div>
@@ -4261,6 +4252,26 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
{{ calendarPeriodLabel }}
</div>
<div class="justify-self-end calendar-zoom-inline" @click.stop>
<input
class="calendar-zoom-slider"
type="range"
min="0"
max="3"
step="1"
:value="calendarZoomLevelIndex"
aria-label="Calendar zoom level"
@input="onCalendarZoomSliderInput"
>
<div class="calendar-zoom-marks" aria-hidden="true">
<span
v-for="index in 4"
:key="`calendar-zoom-mark-${index}`"
class="calendar-zoom-mark"
:class="calendarZoomLevelIndex === index - 1 ? 'calendar-zoom-mark-active' : ''"
/>
</div>
</div>
</div>
<article
@@ -4292,27 +4303,6 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
>
<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"
@@ -5525,7 +5515,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
.calendar-content-wrap {
position: relative;
padding-left: 40px;
padding-right: 128px;
padding-right: 40px;
}
.calendar-content-scroll {
@@ -5584,82 +5574,87 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
}
.calendar-side-nav-right {
right: 56px;
right: 4px;
}
.calendar-zoom-slider-shell {
position: absolute;
top: 50%;
right: 8px;
z-index: 5;
transform: translateY(-50%);
.calendar-zoom-inline {
position: relative;
display: flex;
align-items: center;
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;
width: 128px;
height: 22px;
padding: 0 10px;
}
.calendar-zoom-slider {
width: 124px;
height: 16px;
width: 100%;
height: 18px;
margin: 0;
transform: rotate(-90deg);
transform-origin: center;
accent-color: color-mix(in oklab, var(--color-primary) 82%, transparent);
background: transparent;
-webkit-appearance: none;
appearance: none;
cursor: pointer;
}
.calendar-zoom-slider:focus-visible {
outline: 2px solid color-mix(in oklab, var(--color-primary) 52%, transparent);
outline-offset: 2px;
outline: none;
}
.calendar-zoom-slider::-webkit-slider-runnable-track {
height: 4px;
height: 2px;
border-radius: 999px;
background: color-mix(in oklab, var(--color-base-content) 26%, transparent);
background: color-mix(in oklab, var(--color-base-content) 22%, transparent);
}
.calendar-zoom-slider::-webkit-slider-thumb {
-webkit-appearance: none;
margin-top: -5px;
width: 14px;
height: 14px;
margin-top: -4px;
width: 10px;
height: 10px;
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));
border: 1px solid color-mix(in oklab, var(--color-base-content) 35%, transparent);
background: color-mix(in oklab, var(--color-base-100) 98%, var(--color-base-content));
}
.calendar-zoom-slider::-moz-range-track {
height: 4px;
height: 2px;
border-radius: 999px;
background: color-mix(in oklab, var(--color-base-content) 26%, transparent);
background: color-mix(in oklab, var(--color-base-content) 22%, transparent);
}
.calendar-zoom-slider::-moz-range-progress {
background: transparent;
}
.calendar-zoom-slider::-moz-range-thumb {
width: 14px;
height: 14px;
width: 10px;
height: 10px;
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));
border: 1px solid color-mix(in oklab, var(--color-base-content) 35%, transparent);
background: color-mix(in oklab, var(--color-base-100) 98%, var(--color-base-content));
}
.calendar-zoom-slider-labels {
.calendar-zoom-marks {
position: absolute;
inset-inline: 10px;
top: 50%;
transform: translateY(-50%);
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);
pointer-events: none;
}
.calendar-zoom-slider-label-active {
color: color-mix(in oklab, var(--color-primary) 88%, var(--color-base-content));
font-weight: 700;
.calendar-zoom-mark {
width: 4px;
height: 4px;
border-radius: 999px;
background: color-mix(in oklab, var(--color-base-content) 30%, transparent);
}
.calendar-zoom-mark-active {
width: 6px;
height: 6px;
background: color-mix(in oklab, var(--color-base-content) 78%, transparent);
}
.calendar-zoom-overlay {
@@ -5679,7 +5674,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
@media (max-width: 960px) {
.calendar-content-wrap {
padding-left: 32px;
padding-right: 104px;
padding-right: 32px;
}
.calendar-week-grid {
@@ -5692,23 +5687,8 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
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;
.calendar-zoom-inline {
width: 108px;
}
}