calendar: move zoom slider to top-right horizontal control
This commit is contained in:
166
frontend/app.vue
166
frontend/app.vue
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user