feat(calendar-lab): render content only after zoom settle

This commit is contained in:
Ruslan Bakiev
2026-02-23 17:47:58 +07:00
parent 6d5402dcc1
commit 67a186e916
5 changed files with 204 additions and 106 deletions

View File

@@ -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,6 +19,7 @@ defineProps<{
<p class="calendar-lab-subtitle">Timeline events</p> <p class="calendar-lab-subtitle">Timeline events</p>
</header> </header>
<template v-if="showContent">
<p v-if="isLoading" class="calendar-lab-loading">Loading GraphQL day payload</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> <p v-else-if="isLoaded" class="calendar-lab-meta">Data ready</p>
@@ -35,5 +37,7 @@ defineProps<{
<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>

View File

@@ -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,6 +20,7 @@ defineProps<{
<p class="calendar-lab-subtitle">Weeks inside one month</p> <p class="calendar-lab-subtitle">Weeks inside one month</p>
</header> </header>
<template v-if="showContent">
<p v-if="isLoading" class="calendar-lab-loading">Loading GraphQL month payload</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> <p v-else-if="isLoaded" class="calendar-lab-meta">Data ready</p>
@@ -30,5 +33,9 @@ defineProps<{
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>

View File

@@ -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,6 +20,7 @@ defineProps<{
<p class="calendar-lab-subtitle">7 day columns</p> <p class="calendar-lab-subtitle">7 day columns</p>
</header> </header>
<template v-if="showContent">
<p v-if="isLoading" class="calendar-lab-loading">Loading GraphQL week payload</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> <p v-else-if="isLoaded" class="calendar-lab-meta">Data ready</p>
@@ -30,5 +33,9 @@ defineProps<{
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>

View File

@@ -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,6 +20,7 @@ defineProps<{
<p class="calendar-lab-subtitle">12 months overview</p> <p class="calendar-lab-subtitle">12 months overview</p>
</header> </header>
<template v-if="showContent">
<p v-if="isLoading" class="calendar-lab-loading">Loading GraphQL year payload</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> <p v-else-if="isLoaded" class="calendar-lab-meta">Data ready</p>
@@ -30,5 +33,9 @@ defineProps<{
{{ month }} {{ month }}
</span> </span>
</div> </div>
</template>
<p v-else class="calendar-lab-hint">
Zoom into {{ nextLabel ?? "Month" }}
</p>
</section> </section>
</template> </template>

View File

@@ -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));