feat(calendar-lab): render content only after zoom settle
This commit is contained in:
@@ -3,6 +3,7 @@ defineProps<{
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isLoaded: boolean;
|
isLoaded: boolean;
|
||||||
|
showContent: boolean;
|
||||||
pulseScale: number;
|
pulseScale: number;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
@@ -18,22 +19,25 @@ defineProps<{
|
|||||||
<p class="calendar-lab-subtitle">Timeline events</p>
|
<p class="calendar-lab-subtitle">Timeline events</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<p v-if="isLoading" class="calendar-lab-loading">Loading GraphQL day payload…</p>
|
<template v-if="showContent">
|
||||||
<p v-else-if="isLoaded" class="calendar-lab-meta">Data ready</p>
|
<p v-if="isLoading" class="calendar-lab-loading">Loading GraphQL day payload…</p>
|
||||||
|
<p v-else-if="isLoaded" class="calendar-lab-meta">Data ready</p>
|
||||||
|
|
||||||
<div class="calendar-lab-timeline">
|
<div class="calendar-lab-timeline">
|
||||||
<article class="calendar-lab-event">
|
<article class="calendar-lab-event">
|
||||||
<span>09:30</span>
|
<span>09:30</span>
|
||||||
<p>Call with client</p>
|
<p>Call with client</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="calendar-lab-event">
|
<article class="calendar-lab-event">
|
||||||
<span>13:00</span>
|
<span>13:00</span>
|
||||||
<p>Prepare follow-up summary</p>
|
<p>Prepare follow-up summary</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="calendar-lab-event">
|
<article class="calendar-lab-event">
|
||||||
<span>16:45</span>
|
<span>16:45</span>
|
||||||
<p>Send proposal update</p>
|
<p>Send proposal update</p>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
<p v-else class="calendar-lab-hint">Zoom stopped. Day content will render here.</p>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ defineProps<{
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isLoaded: boolean;
|
isLoaded: boolean;
|
||||||
|
showContent: boolean;
|
||||||
|
nextLabel?: string;
|
||||||
pulseScale: number;
|
pulseScale: number;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
@@ -18,17 +20,22 @@ defineProps<{
|
|||||||
<p class="calendar-lab-subtitle">Weeks inside one month</p>
|
<p class="calendar-lab-subtitle">Weeks inside one month</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<p v-if="isLoading" class="calendar-lab-loading">Loading GraphQL month payload…</p>
|
<template v-if="showContent">
|
||||||
<p v-else-if="isLoaded" class="calendar-lab-meta">Data ready</p>
|
<p v-if="isLoading" class="calendar-lab-loading">Loading GraphQL month payload…</p>
|
||||||
|
<p v-else-if="isLoaded" class="calendar-lab-meta">Data ready</p>
|
||||||
|
|
||||||
<div class="calendar-lab-grid-month">
|
<div class="calendar-lab-grid-month">
|
||||||
<div
|
<div
|
||||||
v-for="week in 4"
|
v-for="week in 4"
|
||||||
:key="`lab-month-week-${week}`"
|
:key="`lab-month-week-${week}`"
|
||||||
class="calendar-lab-row"
|
class="calendar-lab-row"
|
||||||
>
|
>
|
||||||
Week {{ week }}
|
Week {{ week }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
|
<p v-else class="calendar-lab-hint">
|
||||||
|
Zoom into {{ nextLabel ?? "Week" }}
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ defineProps<{
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isLoaded: boolean;
|
isLoaded: boolean;
|
||||||
|
showContent: boolean;
|
||||||
|
nextLabel?: string;
|
||||||
pulseScale: number;
|
pulseScale: number;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
@@ -18,17 +20,22 @@ defineProps<{
|
|||||||
<p class="calendar-lab-subtitle">7 day columns</p>
|
<p class="calendar-lab-subtitle">7 day columns</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<p v-if="isLoading" class="calendar-lab-loading">Loading GraphQL week payload…</p>
|
<template v-if="showContent">
|
||||||
<p v-else-if="isLoaded" class="calendar-lab-meta">Data ready</p>
|
<p v-if="isLoading" class="calendar-lab-loading">Loading GraphQL week payload…</p>
|
||||||
|
<p v-else-if="isLoaded" class="calendar-lab-meta">Data ready</p>
|
||||||
|
|
||||||
<div class="calendar-lab-grid-week">
|
<div class="calendar-lab-grid-week">
|
||||||
<span
|
<span
|
||||||
v-for="day in 7"
|
v-for="day in 7"
|
||||||
:key="`lab-week-day-${day}`"
|
:key="`lab-week-day-${day}`"
|
||||||
class="calendar-lab-day"
|
class="calendar-lab-day"
|
||||||
>
|
>
|
||||||
D{{ day }}
|
D{{ day }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
<p v-else class="calendar-lab-hint">
|
||||||
|
Zoom into {{ nextLabel ?? "Day" }}
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ defineProps<{
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isLoaded: boolean;
|
isLoaded: boolean;
|
||||||
|
showContent: boolean;
|
||||||
|
nextLabel?: string;
|
||||||
pulseScale: number;
|
pulseScale: number;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
@@ -18,17 +20,22 @@ defineProps<{
|
|||||||
<p class="calendar-lab-subtitle">12 months overview</p>
|
<p class="calendar-lab-subtitle">12 months overview</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<p v-if="isLoading" class="calendar-lab-loading">Loading GraphQL year payload…</p>
|
<template v-if="showContent">
|
||||||
<p v-else-if="isLoaded" class="calendar-lab-meta">Data ready</p>
|
<p v-if="isLoading" class="calendar-lab-loading">Loading GraphQL year payload…</p>
|
||||||
|
<p v-else-if="isLoaded" class="calendar-lab-meta">Data ready</p>
|
||||||
|
|
||||||
<div class="calendar-lab-grid-year">
|
<div class="calendar-lab-grid-year">
|
||||||
<span
|
<span
|
||||||
v-for="month in 12"
|
v-for="month in 12"
|
||||||
:key="`lab-year-month-${month}`"
|
:key="`lab-year-month-${month}`"
|
||||||
class="calendar-lab-chip"
|
class="calendar-lab-chip"
|
||||||
>
|
>
|
||||||
{{ month }}
|
{{ month }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
<p v-else class="calendar-lab-hint">
|
||||||
|
Zoom into {{ nextLabel ?? "Month" }}
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ type LevelState = {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CALENDAR_LEVELS: Level[] = ["year", "month", "week", "day"];
|
const LEVELS: Level[] = ["year", "month", "week", "day"];
|
||||||
const LEVEL_LABELS: Record<Level, string> = {
|
const LEVEL_LABELS: Record<Level, string> = {
|
||||||
year: "Year",
|
year: "Year",
|
||||||
month: "Month",
|
month: "Month",
|
||||||
@@ -19,6 +19,20 @@ const LEVEL_LABELS: Record<Level, string> = {
|
|||||||
day: "Day",
|
day: "Day",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const NEXT_LEVEL: Record<Level, Level | null> = {
|
||||||
|
year: "month",
|
||||||
|
month: "week",
|
||||||
|
week: "day",
|
||||||
|
day: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const PREV_LEVEL: Record<Level, Level | null> = {
|
||||||
|
year: null,
|
||||||
|
month: "year",
|
||||||
|
week: "month",
|
||||||
|
day: "week",
|
||||||
|
};
|
||||||
|
|
||||||
const ZOOM_PRIME_STEPS = 2;
|
const ZOOM_PRIME_STEPS = 2;
|
||||||
const ZOOM_PRIME_SCALE = 1.05;
|
const ZOOM_PRIME_SCALE = 1.05;
|
||||||
const ZOOM_ANIMATION_MS = 2200;
|
const ZOOM_ANIMATION_MS = 2200;
|
||||||
@@ -31,17 +45,19 @@ const WORLD_RECT_BY_LEVEL: Record<Level, { x: number; y: number; width: number;
|
|||||||
day: { x: 560, y: 330, width: 280, height: 165 },
|
day: { x: 560, y: 330, width: 280, height: 165 },
|
||||||
};
|
};
|
||||||
|
|
||||||
const zoomLevelIndex = ref(0);
|
|
||||||
const hoveredLevel = ref<Level>("year");
|
|
||||||
const isAnimating = ref(false);
|
|
||||||
const useTransition = ref(false);
|
|
||||||
const viewportRef = ref<HTMLDivElement | null>(null);
|
const viewportRef = ref<HTMLDivElement | null>(null);
|
||||||
const resizeObserver = ref<ResizeObserver | null>(null);
|
const resizeObserver = ref<ResizeObserver | null>(null);
|
||||||
|
const currentLevel = ref<Level>("year");
|
||||||
|
const transitionTarget = ref<Level | null>(null);
|
||||||
|
const hoveredLevel = ref<Level>("month");
|
||||||
|
const isAnimating = ref(false);
|
||||||
|
const useTransition = ref(false);
|
||||||
|
|
||||||
const camera = reactive({
|
const camera = reactive({
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
scale: 1,
|
scaleX: 1,
|
||||||
|
scaleY: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const levelState = reactive<Record<Level, LevelState>>({
|
const levelState = reactive<Record<Level, LevelState>>({
|
||||||
@@ -51,10 +67,6 @@ const levelState = reactive<Record<Level, LevelState>>({
|
|||||||
day: { loaded: false, loading: false },
|
day: { loaded: false, loading: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
let animationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
let primeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
let loadTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
|
|
||||||
const wheelPrime = reactive({
|
const wheelPrime = reactive({
|
||||||
direction: "" as "" | "in" | "out",
|
direction: "" as "" | "in" | "out",
|
||||||
ticks: 0,
|
ticks: 0,
|
||||||
@@ -65,12 +77,17 @@ const pulseState = reactive({
|
|||||||
scale: 1,
|
scale: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const activeLevel = computed(() => CALENDAR_LEVELS[zoomLevelIndex.value] ?? "year");
|
let animationTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
const canZoomIn = computed(() => zoomLevelIndex.value < CALENDAR_LEVELS.length - 1);
|
let primeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
const canZoomOut = computed(() => zoomLevelIndex.value > 0);
|
let loadTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
const displayLevel = computed(() => transitionTarget.value ?? currentLevel.value);
|
||||||
|
const displayIndex = computed(() => LEVELS.indexOf(displayLevel.value));
|
||||||
|
const canZoomIn = computed(() => NEXT_LEVEL[currentLevel.value] !== null);
|
||||||
|
const canZoomOut = computed(() => PREV_LEVEL[currentLevel.value] !== null);
|
||||||
|
|
||||||
const sceneStyle = computed(() => ({
|
const sceneStyle = computed(() => ({
|
||||||
transform: `translate3d(${camera.x}px, ${camera.y}px, 0) scale(${camera.scale})`,
|
transform: `translate3d(${camera.x}px, ${camera.y}px, 0) scale(${camera.scaleX}, ${camera.scaleY})`,
|
||||||
transformOrigin: "0 0",
|
transformOrigin: "0 0",
|
||||||
transition: useTransition.value ? `transform ${ZOOM_ANIMATION_MS}ms cubic-bezier(0.2, 0.8, 0.2, 1)` : "none",
|
transition: useTransition.value ? `transform ${ZOOM_ANIMATION_MS}ms cubic-bezier(0.2, 0.8, 0.2, 1)` : "none",
|
||||||
}));
|
}));
|
||||||
@@ -79,6 +96,30 @@ function pulseScaleFor(level: Level) {
|
|||||||
return pulseState.level === level ? pulseState.scale : 1;
|
return pulseState.level === level ? pulseState.scale : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function nextLevelOf(level: Level) {
|
||||||
|
return NEXT_LEVEL[level];
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevLevelOf(level: Level) {
|
||||||
|
return PREV_LEVEL[level];
|
||||||
|
}
|
||||||
|
|
||||||
|
function levelByIndex(index: number): Level {
|
||||||
|
return LEVELS[Math.max(0, Math.min(LEVELS.length - 1, index))] ?? "year";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNodeVisible(level: Level) {
|
||||||
|
const currentIndex = LEVELS.indexOf(currentLevel.value);
|
||||||
|
const targetIndex = transitionTarget.value ? LEVELS.indexOf(transitionTarget.value) : currentIndex;
|
||||||
|
const maxIndex = Math.max(currentIndex, targetIndex);
|
||||||
|
return LEVELS.indexOf(level) <= Math.min(maxIndex + 1, LEVELS.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLevelContent(level: Level) {
|
||||||
|
if (isAnimating.value) return false;
|
||||||
|
return currentLevel.value === level;
|
||||||
|
}
|
||||||
|
|
||||||
function updateCameraForLevel(level: Level, animate: boolean) {
|
function updateCameraForLevel(level: Level, animate: boolean) {
|
||||||
const viewport = viewportRef.value;
|
const viewport = viewportRef.value;
|
||||||
if (!viewport) return;
|
if (!viewport) return;
|
||||||
@@ -88,20 +129,23 @@ function updateCameraForLevel(level: Level, animate: boolean) {
|
|||||||
const viewportHeight = Math.max(1, viewport.clientHeight);
|
const viewportHeight = Math.max(1, viewport.clientHeight);
|
||||||
const safeWidth = Math.max(1, viewportWidth - WORLD_PADDING * 2);
|
const safeWidth = Math.max(1, viewportWidth - WORLD_PADDING * 2);
|
||||||
const safeHeight = Math.max(1, viewportHeight - WORLD_PADDING * 2);
|
const safeHeight = Math.max(1, viewportHeight - WORLD_PADDING * 2);
|
||||||
const scale = Math.min(safeWidth / rect.width, safeHeight / rect.height);
|
|
||||||
|
|
||||||
const x = WORLD_PADDING + (safeWidth - rect.width * scale) / 2 - rect.x * scale;
|
const scaleX = safeWidth / rect.width;
|
||||||
const y = WORLD_PADDING + (safeHeight - rect.height * scale) / 2 - rect.y * scale;
|
const scaleY = safeHeight / rect.height;
|
||||||
|
|
||||||
|
const x = WORLD_PADDING - rect.x * scaleX;
|
||||||
|
const y = WORLD_PADDING - rect.y * scaleY;
|
||||||
|
|
||||||
useTransition.value = animate;
|
useTransition.value = animate;
|
||||||
camera.x = x;
|
camera.x = x;
|
||||||
camera.y = y;
|
camera.y = y;
|
||||||
camera.scale = scale;
|
camera.scaleX = scaleX;
|
||||||
|
camera.scaleY = scaleY;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureGraphqlPayload(level: Level) {
|
async function ensureGraphqlPayload(level: Level) {
|
||||||
const state = levelState[level];
|
const state = levelState[level];
|
||||||
if (state.loading || state.loaded) return;
|
if (state.loaded || state.loading) return;
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
|
|
||||||
if (loadTimer) {
|
if (loadTimer) {
|
||||||
@@ -114,7 +158,7 @@ async function ensureGraphqlPayload(level: Level) {
|
|||||||
state.loading = false;
|
state.loading = false;
|
||||||
state.loaded = true;
|
state.loaded = true;
|
||||||
resolve();
|
resolve();
|
||||||
}, 520);
|
}, 540);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,12 +167,14 @@ function startPulse(level: Level) {
|
|||||||
clearTimeout(primeTimer);
|
clearTimeout(primeTimer);
|
||||||
primeTimer = null;
|
primeTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pulseState.level = level;
|
pulseState.level = level;
|
||||||
pulseState.scale = ZOOM_PRIME_SCALE;
|
pulseState.scale = ZOOM_PRIME_SCALE;
|
||||||
|
|
||||||
primeTimer = setTimeout(() => {
|
primeTimer = setTimeout(() => {
|
||||||
pulseState.scale = 1;
|
pulseState.scale = 1;
|
||||||
pulseState.level = "";
|
pulseState.level = "";
|
||||||
}, 160);
|
}, 170);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetWheelPrime() {
|
function resetWheelPrime() {
|
||||||
@@ -136,15 +182,15 @@ function resetWheelPrime() {
|
|||||||
wheelPrime.ticks = 0;
|
wheelPrime.ticks = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runZoomTo(targetIndex: number) {
|
async function runZoomTo(target: Level) {
|
||||||
if (isAnimating.value) return;
|
if (isAnimating.value) return;
|
||||||
if (targetIndex < 0 || targetIndex >= CALENDAR_LEVELS.length) return;
|
if (target === currentLevel.value) return;
|
||||||
if (targetIndex === zoomLevelIndex.value) return;
|
|
||||||
|
|
||||||
isAnimating.value = true;
|
isAnimating.value = true;
|
||||||
zoomLevelIndex.value = targetIndex;
|
transitionTarget.value = target;
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
updateCameraForLevel(activeLevel.value, true);
|
updateCameraForLevel(target, true);
|
||||||
|
|
||||||
if (animationTimer) {
|
if (animationTimer) {
|
||||||
clearTimeout(animationTimer);
|
clearTimeout(animationTimer);
|
||||||
@@ -153,63 +199,69 @@ async function runZoomTo(targetIndex: number) {
|
|||||||
|
|
||||||
animationTimer = setTimeout(async () => {
|
animationTimer = setTimeout(async () => {
|
||||||
useTransition.value = false;
|
useTransition.value = false;
|
||||||
|
currentLevel.value = target;
|
||||||
|
transitionTarget.value = null;
|
||||||
isAnimating.value = false;
|
isAnimating.value = false;
|
||||||
await ensureGraphqlPayload(activeLevel.value);
|
hoveredLevel.value = nextLevelOf(target) ?? target;
|
||||||
|
await ensureGraphqlPayload(target);
|
||||||
}, ZOOM_ANIMATION_MS + 30);
|
}, ZOOM_ANIMATION_MS + 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWheel(event: WheelEvent) {
|
function onWheel(event: WheelEvent) {
|
||||||
if (isAnimating.value) return;
|
if (isAnimating.value) return;
|
||||||
const direction: "in" | "out" = event.deltaY > 0 ? "in" : "out";
|
|
||||||
if (direction === "in" && !canZoomIn.value) return;
|
|
||||||
if (direction === "out" && !canZoomOut.value) return;
|
|
||||||
|
|
||||||
const pulseLevel = activeLevel.value;
|
const direction: "in" | "out" = event.deltaY > 0 ? "in" : "out";
|
||||||
|
const nextLevel = direction === "in" ? nextLevelOf(currentLevel.value) : prevLevelOf(currentLevel.value);
|
||||||
|
if (!nextLevel) return;
|
||||||
|
|
||||||
if (wheelPrime.direction !== direction) {
|
if (wheelPrime.direction !== direction) {
|
||||||
wheelPrime.direction = direction;
|
wheelPrime.direction = direction;
|
||||||
wheelPrime.ticks = 0;
|
wheelPrime.ticks = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pulseTarget = direction === "in" ? (hoveredLevel.value === nextLevel ? hoveredLevel.value : nextLevel) : currentLevel.value;
|
||||||
if (wheelPrime.ticks < ZOOM_PRIME_STEPS) {
|
if (wheelPrime.ticks < ZOOM_PRIME_STEPS) {
|
||||||
wheelPrime.ticks += 1;
|
wheelPrime.ticks += 1;
|
||||||
startPulse(pulseLevel);
|
startPulse(pulseTarget);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
resetWheelPrime();
|
resetWheelPrime();
|
||||||
if (direction === "in") {
|
void runZoomTo(nextLevel);
|
||||||
void runZoomTo(zoomLevelIndex.value + 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
void runZoomTo(zoomLevelIndex.value - 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSliderInput(event: Event) {
|
function onSliderInput(event: Event) {
|
||||||
const target = event.target as HTMLInputElement | null;
|
const target = event.target as HTMLInputElement | null;
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
const index = Number(target.value);
|
|
||||||
|
const next = levelByIndex(Number(target.value));
|
||||||
resetWheelPrime();
|
resetWheelPrime();
|
||||||
void runZoomTo(index);
|
void runZoomTo(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onZoomInClick() {
|
function onZoomInClick() {
|
||||||
if (!canZoomIn.value) return;
|
const next = nextLevelOf(currentLevel.value);
|
||||||
|
if (!next) return;
|
||||||
resetWheelPrime();
|
resetWheelPrime();
|
||||||
void runZoomTo(zoomLevelIndex.value + 1);
|
void runZoomTo(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onZoomOutClick() {
|
function onZoomOutClick() {
|
||||||
if (!canZoomOut.value) return;
|
const prev = prevLevelOf(currentLevel.value);
|
||||||
|
if (!prev) return;
|
||||||
resetWheelPrime();
|
resetWheelPrime();
|
||||||
void runZoomTo(zoomLevelIndex.value - 1);
|
void runZoomTo(prev);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
updateCameraForLevel(activeLevel.value, false);
|
updateCameraForLevel(currentLevel.value, false);
|
||||||
|
hoveredLevel.value = nextLevelOf(currentLevel.value) ?? currentLevel.value;
|
||||||
|
|
||||||
resizeObserver.value = new ResizeObserver(() => {
|
resizeObserver.value = new ResizeObserver(() => {
|
||||||
updateCameraForLevel(activeLevel.value, false);
|
updateCameraForLevel(displayLevel.value, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (viewportRef.value) {
|
if (viewportRef.value) {
|
||||||
resizeObserver.value.observe(viewportRef.value);
|
resizeObserver.value.observe(viewportRef.value);
|
||||||
}
|
}
|
||||||
@@ -229,7 +281,7 @@ onBeforeUnmount(() => {
|
|||||||
<div class="calendar-lab-toolbar-left">
|
<div class="calendar-lab-toolbar-left">
|
||||||
<button class="btn btn-xs" :disabled="!canZoomOut" @click="onZoomOutClick">-</button>
|
<button class="btn btn-xs" :disabled="!canZoomOut" @click="onZoomOutClick">-</button>
|
||||||
<button class="btn btn-xs" :disabled="!canZoomIn" @click="onZoomInClick">+</button>
|
<button class="btn btn-xs" :disabled="!canZoomIn" @click="onZoomInClick">+</button>
|
||||||
<span class="calendar-lab-level-text">Current: {{ LEVEL_LABELS[activeLevel] }}</span>
|
<span class="calendar-lab-level-text">Current: {{ LEVEL_LABELS[currentLevel] }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="calendar-zoom-inline" @click.stop>
|
<div class="calendar-zoom-inline" @click.stop>
|
||||||
@@ -239,7 +291,7 @@ onBeforeUnmount(() => {
|
|||||||
min="0"
|
min="0"
|
||||||
max="3"
|
max="3"
|
||||||
step="1"
|
step="1"
|
||||||
:value="zoomLevelIndex"
|
:value="displayIndex"
|
||||||
aria-label="Calendar zoom level"
|
aria-label="Calendar zoom level"
|
||||||
@input="onSliderInput"
|
@input="onSliderInput"
|
||||||
>
|
>
|
||||||
@@ -248,7 +300,7 @@ onBeforeUnmount(() => {
|
|||||||
v-for="index in 4"
|
v-for="index in 4"
|
||||||
:key="`calendar-lab-zoom-mark-${index}`"
|
:key="`calendar-lab-zoom-mark-${index}`"
|
||||||
class="calendar-zoom-mark"
|
class="calendar-zoom-mark"
|
||||||
:class="zoomLevelIndex === index - 1 ? 'calendar-zoom-mark-active' : ''"
|
:class="displayIndex === index - 1 ? 'calendar-zoom-mark-active' : ''"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -263,62 +315,77 @@ onBeforeUnmount(() => {
|
|||||||
<div class="calendar-lab-scene" :style="sceneStyle">
|
<div class="calendar-lab-scene" :style="sceneStyle">
|
||||||
<div class="calendar-lab-world">
|
<div class="calendar-lab-world">
|
||||||
<div
|
<div
|
||||||
|
v-show="isNodeVisible('year')"
|
||||||
class="calendar-lab-node calendar-lab-node-year"
|
class="calendar-lab-node calendar-lab-node-year"
|
||||||
:class="[
|
:class="[
|
||||||
activeLevel === 'year' ? 'calendar-lab-node-active' : '',
|
currentLevel === 'year' ? 'calendar-lab-node-active' : '',
|
||||||
hoveredLevel === 'year' ? 'calendar-lab-node-hover' : '',
|
hoveredLevel === 'year' ? 'calendar-lab-node-hover' : '',
|
||||||
|
transitionTarget === 'year' ? 'calendar-lab-node-target' : '',
|
||||||
]"
|
]"
|
||||||
@mouseenter="hoveredLevel = 'year'"
|
@mouseenter="hoveredLevel = 'year'"
|
||||||
>
|
>
|
||||||
<CrmCalendarLabYearRect
|
<CrmCalendarLabYearRect
|
||||||
:is-active="activeLevel === 'year'"
|
:is-active="currentLevel === 'year'"
|
||||||
:is-loading="levelState.year.loading"
|
:is-loading="levelState.year.loading"
|
||||||
:is-loaded="levelState.year.loaded"
|
:is-loaded="levelState.year.loaded"
|
||||||
|
:show-content="showLevelContent('year')"
|
||||||
|
:next-label="LEVEL_LABELS.month"
|
||||||
:pulse-scale="pulseScaleFor('year')"
|
:pulse-scale="pulseScaleFor('year')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
v-show="isNodeVisible('month')"
|
||||||
class="calendar-lab-node calendar-lab-node-month"
|
class="calendar-lab-node calendar-lab-node-month"
|
||||||
:class="[
|
:class="[
|
||||||
activeLevel === 'month' ? 'calendar-lab-node-active' : '',
|
currentLevel === 'month' ? 'calendar-lab-node-active' : '',
|
||||||
hoveredLevel === 'month' ? 'calendar-lab-node-hover' : '',
|
hoveredLevel === 'month' ? 'calendar-lab-node-hover' : '',
|
||||||
|
transitionTarget === 'month' ? 'calendar-lab-node-target' : '',
|
||||||
]"
|
]"
|
||||||
@mouseenter="hoveredLevel = 'month'"
|
@mouseenter="hoveredLevel = 'month'"
|
||||||
>
|
>
|
||||||
<CrmCalendarLabMonthRect
|
<CrmCalendarLabMonthRect
|
||||||
:is-active="activeLevel === 'month'"
|
:is-active="currentLevel === 'month'"
|
||||||
:is-loading="levelState.month.loading"
|
:is-loading="levelState.month.loading"
|
||||||
:is-loaded="levelState.month.loaded"
|
:is-loaded="levelState.month.loaded"
|
||||||
|
:show-content="showLevelContent('month')"
|
||||||
|
:next-label="LEVEL_LABELS.week"
|
||||||
:pulse-scale="pulseScaleFor('month')"
|
:pulse-scale="pulseScaleFor('month')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
v-show="isNodeVisible('week')"
|
||||||
class="calendar-lab-node calendar-lab-node-week"
|
class="calendar-lab-node calendar-lab-node-week"
|
||||||
:class="[
|
:class="[
|
||||||
activeLevel === 'week' ? 'calendar-lab-node-active' : '',
|
currentLevel === 'week' ? 'calendar-lab-node-active' : '',
|
||||||
hoveredLevel === 'week' ? 'calendar-lab-node-hover' : '',
|
hoveredLevel === 'week' ? 'calendar-lab-node-hover' : '',
|
||||||
|
transitionTarget === 'week' ? 'calendar-lab-node-target' : '',
|
||||||
]"
|
]"
|
||||||
@mouseenter="hoveredLevel = 'week'"
|
@mouseenter="hoveredLevel = 'week'"
|
||||||
>
|
>
|
||||||
<CrmCalendarLabWeekRect
|
<CrmCalendarLabWeekRect
|
||||||
:is-active="activeLevel === 'week'"
|
:is-active="currentLevel === 'week'"
|
||||||
:is-loading="levelState.week.loading"
|
:is-loading="levelState.week.loading"
|
||||||
:is-loaded="levelState.week.loaded"
|
:is-loaded="levelState.week.loaded"
|
||||||
|
:show-content="showLevelContent('week')"
|
||||||
|
:next-label="LEVEL_LABELS.day"
|
||||||
:pulse-scale="pulseScaleFor('week')"
|
:pulse-scale="pulseScaleFor('week')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
v-show="isNodeVisible('day')"
|
||||||
class="calendar-lab-node calendar-lab-node-day"
|
class="calendar-lab-node calendar-lab-node-day"
|
||||||
:class="[
|
:class="[
|
||||||
activeLevel === 'day' ? 'calendar-lab-node-active' : '',
|
currentLevel === 'day' ? 'calendar-lab-node-active' : '',
|
||||||
hoveredLevel === 'day' ? 'calendar-lab-node-hover' : '',
|
hoveredLevel === 'day' ? 'calendar-lab-node-hover' : '',
|
||||||
|
transitionTarget === 'day' ? 'calendar-lab-node-target' : '',
|
||||||
]"
|
]"
|
||||||
@mouseenter="hoveredLevel = 'day'"
|
@mouseenter="hoveredLevel = 'day'"
|
||||||
>
|
>
|
||||||
<CrmCalendarLabDayRect
|
<CrmCalendarLabDayRect
|
||||||
:is-active="activeLevel === 'day'"
|
:is-active="currentLevel === 'day'"
|
||||||
:is-loading="levelState.day.loading"
|
:is-loading="levelState.day.loading"
|
||||||
:is-loaded="levelState.day.loaded"
|
:is-loaded="levelState.day.loaded"
|
||||||
|
:show-content="showLevelContent('day')"
|
||||||
:pulse-scale="pulseScaleFor('day')"
|
:pulse-scale="pulseScaleFor('day')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -415,7 +482,8 @@ onBeforeUnmount(() => {
|
|||||||
height: 165px;
|
height: 165px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-lab-node-active :deep(.calendar-lab-rect) {
|
.calendar-lab-node-active :deep(.calendar-lab-rect),
|
||||||
|
.calendar-lab-node-target :deep(.calendar-lab-rect) {
|
||||||
border-color: color-mix(in oklab, var(--color-primary) 85%, transparent);
|
border-color: color-mix(in oklab, var(--color-primary) 85%, transparent);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 0 2px color-mix(in oklab, var(--color-primary) 45%, transparent),
|
0 0 0 2px color-mix(in oklab, var(--color-primary) 45%, transparent),
|
||||||
@@ -460,7 +528,8 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:deep(.calendar-lab-loading),
|
:deep(.calendar-lab-loading),
|
||||||
:deep(.calendar-lab-meta) {
|
:deep(.calendar-lab-meta),
|
||||||
|
:deep(.calendar-lab-hint) {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
}
|
}
|
||||||
@@ -473,6 +542,10 @@ onBeforeUnmount(() => {
|
|||||||
color: color-mix(in oklab, var(--color-success) 86%, var(--color-base-content));
|
color: color-mix(in oklab, var(--color-success) 86%, var(--color-base-content));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.calendar-lab-hint) {
|
||||||
|
color: color-mix(in oklab, var(--color-base-content) 68%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.calendar-lab-grid-year) {
|
:deep(.calendar-lab-grid-year) {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||||
|
|||||||
Reference in New Issue
Block a user