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 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user