116 lines
3.8 KiB
Vue
116 lines
3.8 KiB
Vue
<script setup lang="ts">
|
|
import { useEventListener, usePreferredReducedMotion } from "@vueuse/core";
|
|
import { computed, ref } from "vue";
|
|
import { useRoute } from "vue-router";
|
|
import { useAppStore } from "@/stores/app";
|
|
import AppSidebar from "@/layouts/components/AppSidebar.vue";
|
|
import AppTabs from "@/layouts/components/AppTabs.vue";
|
|
import AppTopbar from "@/layouts/components/AppTopbar.vue";
|
|
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
|
|
|
const route = useRoute();
|
|
const appStore = useAppStore();
|
|
const preferredReducedMotion = usePreferredReducedMotion();
|
|
const routeInstanceKey = computed(
|
|
() => `${route.fullPath}:${appStore.getRouteRefreshKey(route.fullPath)}`,
|
|
);
|
|
|
|
// -- drag rail --
|
|
const MIN_WIDTH = 180;
|
|
const MAX_WIDTH = 400;
|
|
const dragging = ref(false);
|
|
let startX = 0,
|
|
startWidth = 0;
|
|
|
|
useEventListener("mousemove", (e: MouseEvent) => {
|
|
if (!dragging.value) return;
|
|
const newWidth = Math.max(
|
|
MIN_WIDTH,
|
|
Math.min(MAX_WIDTH, Math.round(startWidth + e.clientX - startX)),
|
|
);
|
|
const wrapper = document.querySelector("[data-slot='sidebar-wrapper']") as HTMLElement | null;
|
|
wrapper?.style.setProperty("--sidebar-width", `${newWidth / 16}rem`);
|
|
});
|
|
|
|
useEventListener("mouseup", () => {
|
|
if (!dragging.value) return;
|
|
dragging.value = false;
|
|
const wrapper = document.querySelector("[data-slot='sidebar-wrapper']") as HTMLElement | null;
|
|
if (wrapper) {
|
|
wrapper.style.userSelect = "";
|
|
const group = wrapper.querySelector(".group.peer") as HTMLElement | null;
|
|
group?.querySelectorAll("*").forEach((el) => {
|
|
(el as HTMLElement).style.removeProperty("transition");
|
|
});
|
|
}
|
|
});
|
|
|
|
function onRailMouseDown(e: MouseEvent) {
|
|
const sidebar = document.querySelector("[data-slot='sidebar']") as HTMLElement | null;
|
|
if (sidebar?.dataset.state !== "expanded") return;
|
|
e.preventDefault();
|
|
dragging.value = true;
|
|
startX = e.clientX;
|
|
const wrapper = document.querySelector("[data-slot='sidebar-wrapper']") as HTMLElement | null;
|
|
if (wrapper) {
|
|
const w = getComputedStyle(wrapper).getPropertyValue("--sidebar-width").trim();
|
|
startWidth = parseFloat(w || "14") * 16;
|
|
wrapper.style.userSelect = "none";
|
|
const group = wrapper.querySelector(".group.peer") as HTMLElement | null;
|
|
group?.querySelectorAll("*").forEach((el) => {
|
|
(el as HTMLElement).style.setProperty("transition", "none", "important");
|
|
});
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<SidebarProvider
|
|
class="group/sidebar h-svh min-w-0 overflow-hidden bg-sidebar"
|
|
:style="{ '--sidebar-width': '14rem' }"
|
|
>
|
|
<AppSidebar />
|
|
|
|
<!-- Drag rail: hidden when collapsed, follows sidebar width when expanded -->
|
|
<div
|
|
data-sidebar="rail"
|
|
class="fixed top-0 z-50 hidden w-4 h-screen cursor-col-resize sm:block group-has-[[data-state=collapsed]]/sidebar:hidden after:absolute after:left-1/2 after:-translate-x-px after:inset-y-0 after:w-0.5 hover:after:bg-sidebar-border"
|
|
:style="{ left: 'calc(var(--sidebar-width) - 0.5rem)' }"
|
|
@mousedown="onRailMouseDown"
|
|
/>
|
|
|
|
<SidebarInset class="m-2 ml-0 min-w-0 overflow-hidden rounded-xl shadow z-10">
|
|
<AppTopbar />
|
|
<AppTabs />
|
|
|
|
<div class="flex min-h-0 min-w-0 flex-1 flex-col gap-4 overflow-auto p-4 pt-2">
|
|
<router-view v-slot="{ Component }">
|
|
<transition name="page" mode="out-in" :css="preferredReducedMotion !== 'reduce'">
|
|
<keep-alive :max="8">
|
|
<component :is="Component" :key="routeInstanceKey" />
|
|
</keep-alive>
|
|
</transition>
|
|
</router-view>
|
|
</div>
|
|
</SidebarInset>
|
|
</SidebarProvider>
|
|
</template>
|
|
|
|
<style>
|
|
.page-enter-active {
|
|
transition:
|
|
opacity 160ms ease-out,
|
|
transform 160ms ease-out;
|
|
}
|
|
.page-leave-active {
|
|
transition: opacity 80ms ease-in;
|
|
}
|
|
.page-enter-from {
|
|
opacity: 0;
|
|
transform: translateY(4px);
|
|
}
|
|
.page-leave-to {
|
|
opacity: 0;
|
|
}
|
|
</style>
|