|
|
|
|
@@ -0,0 +1,629 @@
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
|
|
|
|
import CrmCalendarLabDayRect from "~~/app/components/workspace/calendar/lab/CrmCalendarLabDayRect.vue";
|
|
|
|
|
import CrmCalendarLabMonthRect from "~~/app/components/workspace/calendar/lab/CrmCalendarLabMonthRect.vue";
|
|
|
|
|
import CrmCalendarLabWeekRect from "~~/app/components/workspace/calendar/lab/CrmCalendarLabWeekRect.vue";
|
|
|
|
|
import CrmCalendarLabYearRect from "~~/app/components/workspace/calendar/lab/CrmCalendarLabYearRect.vue";
|
|
|
|
|
|
|
|
|
|
type Level = "year" | "month" | "week" | "day";
|
|
|
|
|
type LevelState = {
|
|
|
|
|
loaded: boolean;
|
|
|
|
|
loading: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const CALENDAR_LEVELS: Level[] = ["year", "month", "week", "day"];
|
|
|
|
|
const LEVEL_LABELS: Record<Level, string> = {
|
|
|
|
|
year: "Year",
|
|
|
|
|
month: "Month",
|
|
|
|
|
week: "Week",
|
|
|
|
|
day: "Day",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const ZOOM_PRIME_STEPS = 2;
|
|
|
|
|
const ZOOM_PRIME_SCALE = 1.05;
|
|
|
|
|
const ZOOM_ANIMATION_MS = 2200;
|
|
|
|
|
const WORLD_PADDING = 20;
|
|
|
|
|
|
|
|
|
|
const WORLD_RECT_BY_LEVEL: Record<Level, { x: number; y: number; width: number; height: number }> = {
|
|
|
|
|
year: { x: 80, y: 50, width: 1240, height: 760 },
|
|
|
|
|
month: { x: 180, y: 130, width: 1040, height: 620 },
|
|
|
|
|
week: { x: 260, y: 220, width: 860, height: 460 },
|
|
|
|
|
day: { x: 320, y: 290, width: 730, height: 320 },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const zoomLevelIndex = ref(0);
|
|
|
|
|
const hoveredLevel = ref<Level>("year");
|
|
|
|
|
const isAnimating = ref(false);
|
|
|
|
|
const useTransition = ref(false);
|
|
|
|
|
const viewportRef = ref<HTMLDivElement | null>(null);
|
|
|
|
|
const resizeObserver = ref<ResizeObserver | null>(null);
|
|
|
|
|
|
|
|
|
|
const camera = reactive({
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
|
|
|
|
scale: 1,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const levelState = reactive<Record<Level, LevelState>>({
|
|
|
|
|
year: { loaded: true, loading: false },
|
|
|
|
|
month: { loaded: false, loading: false },
|
|
|
|
|
week: { 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({
|
|
|
|
|
direction: "" as "" | "in" | "out",
|
|
|
|
|
ticks: 0,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const pulseState = reactive({
|
|
|
|
|
level: "" as "" | Level,
|
|
|
|
|
scale: 1,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const activeLevel = computed(() => CALENDAR_LEVELS[zoomLevelIndex.value] ?? "year");
|
|
|
|
|
const canZoomIn = computed(() => zoomLevelIndex.value < CALENDAR_LEVELS.length - 1);
|
|
|
|
|
const canZoomOut = computed(() => zoomLevelIndex.value > 0);
|
|
|
|
|
|
|
|
|
|
const sceneStyle = computed(() => ({
|
|
|
|
|
transform: `translate3d(${camera.x}px, ${camera.y}px, 0) scale(${camera.scale})`,
|
|
|
|
|
transformOrigin: "0 0",
|
|
|
|
|
transition: useTransition.value ? `transform ${ZOOM_ANIMATION_MS}ms cubic-bezier(0.2, 0.8, 0.2, 1)` : "none",
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
function pulseScaleFor(level: Level) {
|
|
|
|
|
return pulseState.level === level ? pulseState.scale : 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateCameraForLevel(level: Level, animate: boolean) {
|
|
|
|
|
const viewport = viewportRef.value;
|
|
|
|
|
if (!viewport) return;
|
|
|
|
|
|
|
|
|
|
const rect = WORLD_RECT_BY_LEVEL[level];
|
|
|
|
|
const viewportWidth = Math.max(1, viewport.clientWidth);
|
|
|
|
|
const viewportHeight = Math.max(1, viewport.clientHeight);
|
|
|
|
|
const safeWidth = Math.max(1, viewportWidth - 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 y = WORLD_PADDING + (safeHeight - rect.height * scale) / 2 - rect.y * scale;
|
|
|
|
|
|
|
|
|
|
useTransition.value = animate;
|
|
|
|
|
camera.x = x;
|
|
|
|
|
camera.y = y;
|
|
|
|
|
camera.scale = scale;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function ensureGraphqlPayload(level: Level) {
|
|
|
|
|
const state = levelState[level];
|
|
|
|
|
if (state.loading || state.loaded) return;
|
|
|
|
|
state.loading = true;
|
|
|
|
|
|
|
|
|
|
if (loadTimer) {
|
|
|
|
|
clearTimeout(loadTimer);
|
|
|
|
|
loadTimer = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await new Promise<void>((resolve) => {
|
|
|
|
|
loadTimer = setTimeout(() => {
|
|
|
|
|
state.loading = false;
|
|
|
|
|
state.loaded = true;
|
|
|
|
|
resolve();
|
|
|
|
|
}, 520);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function startPulse(level: Level) {
|
|
|
|
|
if (primeTimer) {
|
|
|
|
|
clearTimeout(primeTimer);
|
|
|
|
|
primeTimer = null;
|
|
|
|
|
}
|
|
|
|
|
pulseState.level = level;
|
|
|
|
|
pulseState.scale = ZOOM_PRIME_SCALE;
|
|
|
|
|
primeTimer = setTimeout(() => {
|
|
|
|
|
pulseState.scale = 1;
|
|
|
|
|
pulseState.level = "";
|
|
|
|
|
}, 160);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetWheelPrime() {
|
|
|
|
|
wheelPrime.direction = "";
|
|
|
|
|
wheelPrime.ticks = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function runZoomTo(targetIndex: number) {
|
|
|
|
|
if (isAnimating.value) return;
|
|
|
|
|
if (targetIndex < 0 || targetIndex >= CALENDAR_LEVELS.length) return;
|
|
|
|
|
if (targetIndex === zoomLevelIndex.value) return;
|
|
|
|
|
|
|
|
|
|
isAnimating.value = true;
|
|
|
|
|
zoomLevelIndex.value = targetIndex;
|
|
|
|
|
await nextTick();
|
|
|
|
|
updateCameraForLevel(activeLevel.value, true);
|
|
|
|
|
|
|
|
|
|
if (animationTimer) {
|
|
|
|
|
clearTimeout(animationTimer);
|
|
|
|
|
animationTimer = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
animationTimer = setTimeout(async () => {
|
|
|
|
|
useTransition.value = false;
|
|
|
|
|
isAnimating.value = false;
|
|
|
|
|
await ensureGraphqlPayload(activeLevel.value);
|
|
|
|
|
}, ZOOM_ANIMATION_MS + 30);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onWheel(event: WheelEvent) {
|
|
|
|
|
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;
|
|
|
|
|
if (wheelPrime.direction !== direction) {
|
|
|
|
|
wheelPrime.direction = direction;
|
|
|
|
|
wheelPrime.ticks = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (wheelPrime.ticks < ZOOM_PRIME_STEPS) {
|
|
|
|
|
wheelPrime.ticks += 1;
|
|
|
|
|
startPulse(pulseLevel);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resetWheelPrime();
|
|
|
|
|
if (direction === "in") {
|
|
|
|
|
void runZoomTo(zoomLevelIndex.value + 1);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
void runZoomTo(zoomLevelIndex.value - 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onSliderInput(event: Event) {
|
|
|
|
|
const target = event.target as HTMLInputElement | null;
|
|
|
|
|
if (!target) return;
|
|
|
|
|
const index = Number(target.value);
|
|
|
|
|
resetWheelPrime();
|
|
|
|
|
void runZoomTo(index);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onZoomInClick() {
|
|
|
|
|
if (!canZoomIn.value) return;
|
|
|
|
|
resetWheelPrime();
|
|
|
|
|
void runZoomTo(zoomLevelIndex.value + 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onZoomOutClick() {
|
|
|
|
|
if (!canZoomOut.value) return;
|
|
|
|
|
resetWheelPrime();
|
|
|
|
|
void runZoomTo(zoomLevelIndex.value - 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
await nextTick();
|
|
|
|
|
updateCameraForLevel(activeLevel.value, false);
|
|
|
|
|
resizeObserver.value = new ResizeObserver(() => {
|
|
|
|
|
updateCameraForLevel(activeLevel.value, false);
|
|
|
|
|
});
|
|
|
|
|
if (viewportRef.value) {
|
|
|
|
|
resizeObserver.value.observe(viewportRef.value);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
if (animationTimer) clearTimeout(animationTimer);
|
|
|
|
|
if (primeTimer) clearTimeout(primeTimer);
|
|
|
|
|
if (loadTimer) clearTimeout(loadTimer);
|
|
|
|
|
resizeObserver.value?.disconnect();
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<section class="calendar-lab-root">
|
|
|
|
|
<header class="calendar-lab-toolbar">
|
|
|
|
|
<div class="calendar-lab-toolbar-left">
|
|
|
|
|
<button class="btn btn-xs" :disabled="!canZoomOut" @click="onZoomOutClick">-</button>
|
|
|
|
|
<button class="btn btn-xs" :disabled="!canZoomIn" @click="onZoomInClick">+</button>
|
|
|
|
|
<span class="calendar-lab-level-text">Current: {{ LEVEL_LABELS[activeLevel] }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="calendar-zoom-inline" @click.stop>
|
|
|
|
|
<input
|
|
|
|
|
class="calendar-zoom-slider"
|
|
|
|
|
type="range"
|
|
|
|
|
min="0"
|
|
|
|
|
max="3"
|
|
|
|
|
step="1"
|
|
|
|
|
:value="zoomLevelIndex"
|
|
|
|
|
aria-label="Calendar zoom level"
|
|
|
|
|
@input="onSliderInput"
|
|
|
|
|
>
|
|
|
|
|
<div class="calendar-zoom-marks" aria-hidden="true">
|
|
|
|
|
<span
|
|
|
|
|
v-for="index in 4"
|
|
|
|
|
:key="`calendar-lab-zoom-mark-${index}`"
|
|
|
|
|
class="calendar-zoom-mark"
|
|
|
|
|
:class="zoomLevelIndex === index - 1 ? 'calendar-zoom-mark-active' : ''"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
ref="viewportRef"
|
|
|
|
|
class="calendar-lab-viewport"
|
|
|
|
|
:class="canZoomIn ? 'cursor-zoom-in' : 'cursor-zoom-out'"
|
|
|
|
|
@wheel.prevent="onWheel"
|
|
|
|
|
>
|
|
|
|
|
<div class="calendar-lab-scene" :style="sceneStyle">
|
|
|
|
|
<div class="calendar-lab-world">
|
|
|
|
|
<div
|
|
|
|
|
class="calendar-lab-node calendar-lab-node-year"
|
|
|
|
|
:class="[
|
|
|
|
|
activeLevel === 'year' ? 'calendar-lab-node-active' : '',
|
|
|
|
|
hoveredLevel === 'year' ? 'calendar-lab-node-hover' : '',
|
|
|
|
|
]"
|
|
|
|
|
@mouseenter="hoveredLevel = 'year'"
|
|
|
|
|
>
|
|
|
|
|
<CrmCalendarLabYearRect
|
|
|
|
|
:is-active="activeLevel === 'year'"
|
|
|
|
|
:is-loading="levelState.year.loading"
|
|
|
|
|
:is-loaded="levelState.year.loaded"
|
|
|
|
|
:pulse-scale="pulseScaleFor('year')"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
class="calendar-lab-node calendar-lab-node-month"
|
|
|
|
|
:class="[
|
|
|
|
|
activeLevel === 'month' ? 'calendar-lab-node-active' : '',
|
|
|
|
|
hoveredLevel === 'month' ? 'calendar-lab-node-hover' : '',
|
|
|
|
|
]"
|
|
|
|
|
@mouseenter="hoveredLevel = 'month'"
|
|
|
|
|
>
|
|
|
|
|
<CrmCalendarLabMonthRect
|
|
|
|
|
:is-active="activeLevel === 'month'"
|
|
|
|
|
:is-loading="levelState.month.loading"
|
|
|
|
|
:is-loaded="levelState.month.loaded"
|
|
|
|
|
:pulse-scale="pulseScaleFor('month')"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
class="calendar-lab-node calendar-lab-node-week"
|
|
|
|
|
:class="[
|
|
|
|
|
activeLevel === 'week' ? 'calendar-lab-node-active' : '',
|
|
|
|
|
hoveredLevel === 'week' ? 'calendar-lab-node-hover' : '',
|
|
|
|
|
]"
|
|
|
|
|
@mouseenter="hoveredLevel = 'week'"
|
|
|
|
|
>
|
|
|
|
|
<CrmCalendarLabWeekRect
|
|
|
|
|
:is-active="activeLevel === 'week'"
|
|
|
|
|
:is-loading="levelState.week.loading"
|
|
|
|
|
:is-loaded="levelState.week.loaded"
|
|
|
|
|
:pulse-scale="pulseScaleFor('week')"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
class="calendar-lab-node calendar-lab-node-day"
|
|
|
|
|
:class="[
|
|
|
|
|
activeLevel === 'day' ? 'calendar-lab-node-active' : '',
|
|
|
|
|
hoveredLevel === 'day' ? 'calendar-lab-node-hover' : '',
|
|
|
|
|
]"
|
|
|
|
|
@mouseenter="hoveredLevel = 'day'"
|
|
|
|
|
>
|
|
|
|
|
<CrmCalendarLabDayRect
|
|
|
|
|
:is-active="activeLevel === 'day'"
|
|
|
|
|
:is-loading="levelState.day.loading"
|
|
|
|
|
:is-loaded="levelState.day.loaded"
|
|
|
|
|
:pulse-scale="pulseScaleFor('day')"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.calendar-lab-root {
|
|
|
|
|
height: calc(100dvh - 2.5rem);
|
|
|
|
|
min-height: 620px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
padding: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-lab-toolbar {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-lab-toolbar-left {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-lab-level-text {
|
|
|
|
|
font-size: 0.78rem;
|
|
|
|
|
color: color-mix(in oklab, var(--color-base-content) 72%, transparent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-lab-viewport {
|
|
|
|
|
position: relative;
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
border-radius: 14px;
|
|
|
|
|
border: 1px solid color-mix(in oklab, var(--color-base-content) 18%, transparent);
|
|
|
|
|
background:
|
|
|
|
|
radial-gradient(circle at 15% 15%, color-mix(in oklab, var(--color-base-content) 6%, transparent), transparent 40%),
|
|
|
|
|
color-mix(in oklab, var(--color-base-100) 94%, transparent);
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-lab-scene {
|
|
|
|
|
width: 1400px;
|
|
|
|
|
height: 900px;
|
|
|
|
|
will-change: transform;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-lab-world {
|
|
|
|
|
width: 1400px;
|
|
|
|
|
height: 900px;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-lab-node {
|
|
|
|
|
position: absolute;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-lab-node-year {
|
|
|
|
|
left: 80px;
|
|
|
|
|
top: 50px;
|
|
|
|
|
width: 1240px;
|
|
|
|
|
height: 760px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-lab-node-month {
|
|
|
|
|
left: 100px;
|
|
|
|
|
top: 80px;
|
|
|
|
|
width: 1040px;
|
|
|
|
|
height: 620px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-lab-node-week {
|
|
|
|
|
left: 80px;
|
|
|
|
|
top: 90px;
|
|
|
|
|
width: 860px;
|
|
|
|
|
height: 460px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-lab-node-day {
|
|
|
|
|
left: 60px;
|
|
|
|
|
top: 70px;
|
|
|
|
|
width: 730px;
|
|
|
|
|
height: 320px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-lab-node-active :deep(.calendar-lab-rect) {
|
|
|
|
|
border-color: color-mix(in oklab, var(--color-primary) 85%, transparent);
|
|
|
|
|
box-shadow:
|
|
|
|
|
0 0 0 2px color-mix(in oklab, var(--color-primary) 45%, transparent),
|
|
|
|
|
0 16px 36px color-mix(in oklab, var(--color-base-content) 10%, transparent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-lab-node-hover :deep(.calendar-lab-rect) {
|
|
|
|
|
border-color: color-mix(in oklab, var(--color-primary) 70%, transparent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.calendar-lab-rect) {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
border-radius: 14px;
|
|
|
|
|
border: 1px solid color-mix(in oklab, var(--color-base-content) 24%, transparent);
|
|
|
|
|
background: color-mix(in oklab, var(--color-base-100) 96%, transparent);
|
|
|
|
|
padding: 12px;
|
|
|
|
|
transform-origin: center center;
|
|
|
|
|
transition:
|
|
|
|
|
transform 200ms ease,
|
|
|
|
|
border-color 180ms ease,
|
|
|
|
|
box-shadow 180ms ease,
|
|
|
|
|
background-color 180ms ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.calendar-lab-header) {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.calendar-lab-title) {
|
|
|
|
|
font-size: 0.84rem;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.calendar-lab-subtitle) {
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
color: color-mix(in oklab, var(--color-base-content) 65%, transparent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.calendar-lab-loading),
|
|
|
|
|
:deep(.calendar-lab-meta) {
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
font-size: 0.72rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.calendar-lab-loading) {
|
|
|
|
|
color: color-mix(in oklab, var(--color-warning) 85%, var(--color-base-content));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.calendar-lab-meta) {
|
|
|
|
|
color: color-mix(in oklab, var(--color-success) 86%, var(--color-base-content));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.calendar-lab-grid-year) {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.calendar-lab-chip),
|
|
|
|
|
:deep(.calendar-lab-row),
|
|
|
|
|
:deep(.calendar-lab-day),
|
|
|
|
|
:deep(.calendar-lab-event) {
|
|
|
|
|
border-radius: 9px;
|
|
|
|
|
border: 1px solid color-mix(in oklab, var(--color-base-content) 16%, transparent);
|
|
|
|
|
background: color-mix(in oklab, var(--color-base-200) 78%, transparent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.calendar-lab-chip) {
|
|
|
|
|
min-height: 28px;
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-size: 0.74rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.calendar-lab-grid-month) {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.calendar-lab-row) {
|
|
|
|
|
min-height: 60px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 0 8px;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.calendar-lab-grid-week) {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.calendar-lab-day) {
|
|
|
|
|
min-height: 70px;
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-size: 0.72rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.calendar-lab-timeline) {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.calendar-lab-event) {
|
|
|
|
|
min-height: 44px;
|
|
|
|
|
padding: 6px 8px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
font-size: 0.72rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.calendar-lab-event span) {
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
min-width: 50px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-zoom-inline {
|
|
|
|
|
position: relative;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
width: 128px;
|
|
|
|
|
height: 22px;
|
|
|
|
|
padding: 0 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-zoom-slider {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 18px;
|
|
|
|
|
margin: 0;
|
|
|
|
|
background: transparent;
|
|
|
|
|
-webkit-appearance: none;
|
|
|
|
|
appearance: none;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-zoom-slider:focus-visible {
|
|
|
|
|
outline: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-zoom-slider::-webkit-slider-runnable-track {
|
|
|
|
|
height: 2px;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
background: color-mix(in oklab, var(--color-base-content) 22%, transparent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-zoom-slider::-webkit-slider-thumb {
|
|
|
|
|
-webkit-appearance: none;
|
|
|
|
|
appearance: none;
|
|
|
|
|
width: 10px;
|
|
|
|
|
height: 10px;
|
|
|
|
|
margin-top: -4px;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
background: color-mix(in oklab, var(--color-base-content) 88%, transparent);
|
|
|
|
|
border: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-zoom-slider::-moz-range-track {
|
|
|
|
|
height: 2px;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
background: color-mix(in oklab, var(--color-base-content) 22%, transparent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-zoom-slider::-moz-range-progress {
|
|
|
|
|
height: 2px;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
background: transparent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-zoom-slider::-moz-range-thumb {
|
|
|
|
|
width: 10px;
|
|
|
|
|
height: 10px;
|
|
|
|
|
border: 0;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
background: color-mix(in oklab, var(--color-base-content) 88%, transparent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-zoom-marks {
|
|
|
|
|
position: absolute;
|
|
|
|
|
inset: 0 10px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-zoom-mark {
|
|
|
|
|
width: 4px;
|
|
|
|
|
height: 4px;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
background: color-mix(in oklab, var(--color-base-content) 35%, transparent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-zoom-mark-active {
|
|
|
|
|
background: color-mix(in oklab, var(--color-base-content) 85%, transparent);
|
|
|
|
|
}
|
|
|
|
|
</style>
|