diff --git a/apps/dashboard/src/components/layouts/dashboard-layout.tsx b/apps/dashboard/src/components/layouts/dashboard-layout.tsx
index d1acc52..dd0cacf 100644
--- a/apps/dashboard/src/components/layouts/dashboard-layout.tsx
+++ b/apps/dashboard/src/components/layouts/dashboard-layout.tsx
@@ -1,5 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { getRouteApi, Outlet } from "@tanstack/react-router";
+import { motion } from "motion/react";
import { lazy, Suspense } from "react";
import {
githubMyIssuesQueryOptions,
@@ -7,9 +8,11 @@ import {
} from "#/lib/github.query";
import { useGitHubRevalidation } from "#/lib/use-github-revalidation";
import { useHasMounted } from "#/lib/use-has-mounted";
+import { useMediaQuery } from "#/lib/use-media-query";
import { DashboardBottomBar } from "./dashboard-bottombar";
import { DashboardMobileNav } from "./dashboard-mobile-nav";
import {
+ SIDE_PANEL_WIDTH,
SidePanelProvider,
SidePanelSlot,
SidePanelToggle,
@@ -61,6 +64,8 @@ export function DashboardLayout() {
const tabsReady = hasMounted && Boolean(pullsQuery.data && issuesQuery.data);
const sidePanel = useSidePanelSlot();
+ const isXl = useMediaQuery("(min-width: 1280px)");
+ const showPanel = isXl && sidePanel.hasContent && !sidePanel.collapsed;
return (
@@ -83,19 +88,29 @@ export function DashboardLayout() {
toggle: sidePanel.toggle,
}}
>
-
-
+
(null);
+ const ghostRef = useRef(null);
+ const exitTimer = useRef | null>(null);
const refCallback = useCallback(
(el: HTMLDivElement | null) => {
@@ -86,31 +91,52 @@ export function SidePanelSlot({
};
check();
- const observer = new MutationObserver(check);
+ const observer = new MutationObserver((mutations) => {
+ const has = el.childNodes.length > 0;
+
+ if (!has && ghostRef.current) {
+ // Content removed — clone into ghost for exit animation
+ ghostRef.current.innerHTML = "";
+ for (const mutation of mutations) {
+ for (const node of mutation.removedNodes) {
+ ghostRef.current.appendChild(node.cloneNode(true));
+ }
+ }
+ if (exitTimer.current) clearTimeout(exitTimer.current);
+ exitTimer.current = setTimeout(() => {
+ if (ghostRef.current) ghostRef.current.innerHTML = "";
+ }, 500);
+ } else if (has && ghostRef.current) {
+ // Content added — clear ghost
+ ghostRef.current.innerHTML = "";
+ if (exitTimer.current) clearTimeout(exitTimer.current);
+ }
+
+ setHasChildren(has);
+ onHasContent(has);
+ });
observer.observe(el, { childList: true });
el.addEventListener("sidepanel-content", check);
return () => {
observer.disconnect();
el.removeEventListener("sidepanel-content", check);
+ if (exitTimer.current) clearTimeout(exitTimer.current);
};
}, [onHasContent]);
const show = hasChildren && !collapsed;
return (
-
+
);
}
diff --git a/apps/dashboard/src/lib/use-media-query.ts b/apps/dashboard/src/lib/use-media-query.ts
new file mode 100644
index 0000000..64d49e3
--- /dev/null
+++ b/apps/dashboard/src/lib/use-media-query.ts
@@ -0,0 +1,15 @@
+import { useEffect, useState } from "react";
+
+export function useMediaQuery(query: string) {
+ const [matches, setMatches] = useState(false);
+
+ useEffect(() => {
+ const mq = window.matchMedia(query);
+ setMatches(mq.matches);
+ const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
+ mq.addEventListener("change", handler);
+ return () => mq.removeEventListener("change", handler);
+ }, [query]);
+
+ return matches;
+}