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 calendarWheelLockUntil = 0;
let calendarZoomOverlayTimer: ReturnType<typeof setTimeout> | null = null; let calendarZoomOverlayTimer: ReturnType<typeof setTimeout> | null = null;
const CALENDAR_ZOOM_DURATION_MS = 180; 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 calendarZoomOrder: CalendarHierarchyView[] = ["year", "month", "week", "day"];
const normalizedCalendarView = computed<CalendarHierarchyView>(() => const normalizedCalendarView = computed<CalendarHierarchyView>(() =>
calendarView.value === "agenda" ? "month" : calendarView.value, calendarView.value === "agenda" ? "month" : calendarView.value,
); );
const calendarZoomLevelIndex = computed(() => const calendarZoomLevelIndex = computed(() => Math.max(0, calendarZoomOrder.indexOf(normalizedCalendarView.value)));
Math.max(0, calendarZoomStops.findIndex((stop) => stop.view === normalizedCalendarView.value)),
);
const calendarZoomOverlayStyle = computed(() => ({ const calendarZoomOverlayStyle = computed(() => ({
left: `${calendarZoomOverlay.value.left}px`, left: `${calendarZoomOverlay.value.left}px`,
top: `${calendarZoomOverlay.value.top}px`, top: `${calendarZoomOverlay.value.top}px`,
@@ -2401,8 +2393,7 @@ async function setCalendarZoomLevel(targetView: CalendarHierarchyView) {
function onCalendarZoomSliderInput(event: Event) { function onCalendarZoomSliderInput(event: Event) {
const value = Number((event.target as HTMLInputElement | null)?.value ?? NaN); const value = Number((event.target as HTMLInputElement | null)?.value ?? NaN);
if (!Number.isFinite(value)) return; if (!Number.isFinite(value)) return;
const sliderStep = Math.max(0, Math.min(3, Math.round(value))); const targetIndex = Math.max(0, Math.min(3, Math.round(value)));
const targetIndex = 3 - sliderStep;
const targetView = calendarZoomOrder[targetIndex]; const targetView = calendarZoomOrder[targetIndex];
if (!targetView) return; if (!targetView) return;
void setCalendarZoomLevel(targetView); void setCalendarZoomLevel(targetView);
@@ -4252,7 +4243,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
v-if="contextPickerEnabled" v-if="contextPickerEnabled"
class="context-scope-label" class="context-scope-label"
>{{ contextScopeLabel('calendar') }}</span> >{{ 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"> <div class="flex items-center gap-1">
<button class="btn btn-xs" @click="setToday">Today</button> <button class="btn btn-xs" @click="setToday">Today</button>
</div> </div>
@@ -4261,6 +4252,26 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
{{ calendarPeriodLabel }} {{ calendarPeriodLabel }}
</div> </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> </div>
<article <article
@@ -4292,27 +4303,6 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
> >
<span></span> <span></span>
</button> </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 <div
class="calendar-content-scroll min-h-0 h-full overflow-y-auto pr-1" class="calendar-content-scroll min-h-0 h-full overflow-y-auto pr-1"
@wheel.prevent="onCalendarHierarchyWheel" @wheel.prevent="onCalendarHierarchyWheel"
@@ -5525,7 +5515,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
.calendar-content-wrap { .calendar-content-wrap {
position: relative; position: relative;
padding-left: 40px; padding-left: 40px;
padding-right: 128px; padding-right: 40px;
} }
.calendar-content-scroll { .calendar-content-scroll {
@@ -5584,82 +5574,87 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
} }
.calendar-side-nav-right { .calendar-side-nav-right {
right: 56px; right: 4px;
} }
.calendar-zoom-slider-shell { .calendar-zoom-inline {
position: absolute; position: relative;
top: 50%;
right: 8px;
z-index: 5;
transform: translateY(-50%);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; width: 128px;
border: 1px solid color-mix(in oklab, var(--color-base-content) 16%, transparent); height: 22px;
border-radius: 12px; padding: 0 10px;
background: color-mix(in oklab, var(--color-base-100) 90%, transparent);
padding: 8px 7px;
} }
.calendar-zoom-slider { .calendar-zoom-slider {
width: 124px; width: 100%;
height: 16px; height: 18px;
margin: 0; margin: 0;
transform: rotate(-90deg); background: transparent;
transform-origin: center; -webkit-appearance: none;
accent-color: color-mix(in oklab, var(--color-primary) 82%, transparent); appearance: none;
cursor: pointer; cursor: pointer;
} }
.calendar-zoom-slider:focus-visible { .calendar-zoom-slider:focus-visible {
outline: 2px solid color-mix(in oklab, var(--color-primary) 52%, transparent); outline: none;
outline-offset: 2px;
} }
.calendar-zoom-slider::-webkit-slider-runnable-track { .calendar-zoom-slider::-webkit-slider-runnable-track {
height: 4px; height: 2px;
border-radius: 999px; 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 { .calendar-zoom-slider::-webkit-slider-thumb {
-webkit-appearance: none; -webkit-appearance: none;
margin-top: -5px; margin-top: -4px;
width: 14px; width: 10px;
height: 14px; height: 10px;
border-radius: 999px; border-radius: 999px;
border: 1px solid color-mix(in oklab, var(--color-primary) 42%, transparent); border: 1px solid color-mix(in oklab, var(--color-base-content) 35%, transparent);
background: color-mix(in oklab, var(--color-primary) 86%, var(--color-base-100)); background: color-mix(in oklab, var(--color-base-100) 98%, var(--color-base-content));
} }
.calendar-zoom-slider::-moz-range-track { .calendar-zoom-slider::-moz-range-track {
height: 4px; height: 2px;
border-radius: 999px; 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 { .calendar-zoom-slider::-moz-range-thumb {
width: 14px; width: 10px;
height: 14px; height: 10px;
border-radius: 999px; border-radius: 999px;
border: 1px solid color-mix(in oklab, var(--color-primary) 42%, transparent); border: 1px solid color-mix(in oklab, var(--color-base-content) 35%, transparent);
background: color-mix(in oklab, var(--color-primary) 86%, var(--color-base-100)); 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; display: flex;
flex-direction: column;
justify-content: space-between; justify-content: space-between;
height: 124px; pointer-events: none;
font-size: 10px;
line-height: 1;
color: color-mix(in oklab, var(--color-base-content) 56%, transparent);
} }
.calendar-zoom-slider-label-active { .calendar-zoom-mark {
color: color-mix(in oklab, var(--color-primary) 88%, var(--color-base-content)); width: 4px;
font-weight: 700; 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 { .calendar-zoom-overlay {
@@ -5679,7 +5674,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
@media (max-width: 960px) { @media (max-width: 960px) {
.calendar-content-wrap { .calendar-content-wrap {
padding-left: 32px; padding-left: 32px;
padding-right: 104px; padding-right: 32px;
} }
.calendar-week-grid { .calendar-week-grid {
@@ -5692,23 +5687,8 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
height: 24px; height: 24px;
} }
.calendar-side-nav-right { .calendar-zoom-inline {
right: 44px; width: 108px;
}
.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;
} }
} }