+
+ {/* Carousel content */}
- {modes.map((mode) => (
-
- {children(mode, mode === currentMode)}
-
- ))}
+
+ {modes.map((mode) => (
+
+
+ {children(mode, mode === currentMode)}
+
+
+ ))}
+
+
+ {/* Bottom navigation bar */}
+
);
}
-
diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/AnimatedBackground/AnimatedBackground.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/AnimatedBackground/AnimatedBackground.tsx
new file mode 100644
index 00000000000..91852fe33b3
--- /dev/null
+++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/AnimatedBackground/AnimatedBackground.tsx
@@ -0,0 +1,41 @@
+import { type MotionValue, motion, useTransform } from "framer-motion";
+
+interface AnimatedBackgroundProps {
+ progress: MotionValue
;
+ modeCount: number;
+}
+
+export function AnimatedBackground({
+ progress,
+ modeCount,
+}: AnimatedBackgroundProps) {
+ // Calculate the width of each button (36px = h-9 w-9) + gap (4px = gap-1)
+ const buttonWidth = 36;
+ const gap = 4;
+ const totalButtonWidth = buttonWidth + gap;
+
+ // Transform progress (0-1) to translateX position
+ // For 2 modes: 0 -> 0px, 1 -> 40px (buttonWidth + gap)
+ const translateX = useTransform(
+ progress,
+ [0, modeCount - 1],
+ [0, (modeCount - 1) * totalButtonWidth]
+ );
+
+ return (
+
+ );
+}
+
diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/AnimatedBackground/index.ts b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/AnimatedBackground/index.ts
new file mode 100644
index 00000000000..f57394e4fd0
--- /dev/null
+++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/AnimatedBackground/index.ts
@@ -0,0 +1,2 @@
+export { AnimatedBackground } from "./AnimatedBackground";
+
diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/ModeContent/ModeContent.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/ModeContent/ModeContent.tsx
new file mode 100644
index 00000000000..5a55d2d7573
--- /dev/null
+++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/ModeContent/ModeContent.tsx
@@ -0,0 +1,25 @@
+import type { ReactNode } from "react";
+import { ModeHeader } from "../ModeHeader";
+import type { SidebarMode } from "../../types";
+
+interface ModeContentProps {
+ mode: SidebarMode;
+ isActive: boolean;
+ children: ReactNode;
+}
+
+export function ModeContent({ mode, children }: ModeContentProps) {
+ return (
+
+ );
+}
+
diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/ModeContent/index.ts b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/ModeContent/index.ts
new file mode 100644
index 00000000000..d745b999a59
--- /dev/null
+++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/ModeContent/index.ts
@@ -0,0 +1,2 @@
+export { ModeContent } from "./ModeContent";
+
diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/ModeHeader/ModeHeader.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/ModeHeader/ModeHeader.tsx
new file mode 100644
index 00000000000..6abbe28631d
--- /dev/null
+++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/ModeHeader/ModeHeader.tsx
@@ -0,0 +1,17 @@
+import { modeLabels } from "../../constants";
+import type { SidebarMode } from "../../types";
+
+interface ModeHeaderProps {
+ mode: SidebarMode;
+}
+
+export function ModeHeader({ mode }: ModeHeaderProps) {
+ return (
+
+
+ {modeLabels[mode]}
+
+
+ );
+}
+
diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/ModeHeader/index.ts b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/ModeHeader/index.ts
new file mode 100644
index 00000000000..80cc09d0ea1
--- /dev/null
+++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/ModeHeader/index.ts
@@ -0,0 +1,2 @@
+export { ModeHeader } from "./ModeHeader";
+
diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/ModeNavigation/ModeNavigation.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/ModeNavigation/ModeNavigation.tsx
new file mode 100644
index 00000000000..ba4ed8709e8
--- /dev/null
+++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/ModeNavigation/ModeNavigation.tsx
@@ -0,0 +1,49 @@
+import { Button } from "@superset/ui/button";
+import type { MotionValue } from "framer-motion";
+import { AnimatedBackground } from "../AnimatedBackground";
+import { modeIcons } from "../../constants";
+import type { SidebarMode } from "../../types";
+
+interface ModeNavigationProps {
+ modes: SidebarMode[];
+ currentMode: SidebarMode;
+ onModeSelect: (mode: SidebarMode) => void;
+ scrollProgress: MotionValue;
+}
+
+export function ModeNavigation({
+ modes,
+ currentMode,
+ onModeSelect,
+ scrollProgress,
+}: ModeNavigationProps) {
+ return (
+
+
+
+
+ {modes.map((mode) => {
+ const Icon = modeIcons[mode];
+ const isActive = mode === currentMode;
+
+ return (
+
+ );
+ })}
+
+
+ );
+}
+
diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/ModeNavigation/index.ts b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/ModeNavigation/index.ts
new file mode 100644
index 00000000000..2d7c7a6c88e
--- /dev/null
+++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/components/ModeNavigation/index.ts
@@ -0,0 +1,2 @@
+export { ModeNavigation } from "./ModeNavigation";
+
diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/constants.ts b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/constants.ts
new file mode 100644
index 00000000000..17ea710233e
--- /dev/null
+++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/constants.ts
@@ -0,0 +1,13 @@
+import { GitBranch, LayoutList } from "lucide-react";
+import type { SidebarMode } from "./types";
+
+export const modeIcons: Record = {
+ tabs: LayoutList,
+ diff: GitBranch,
+};
+
+export const modeLabels: Record = {
+ tabs: "Tabs",
+ diff: "Diffs",
+};
+
diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/hooks/useModeDetection.ts b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/hooks/useModeDetection.ts
new file mode 100644
index 00000000000..4c65b268477
--- /dev/null
+++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/hooks/useModeDetection.ts
@@ -0,0 +1,71 @@
+import { useEffect } from "react";
+import type { SidebarMode } from "../types";
+
+interface UseModeDetectionOptions {
+ scrollContainer: HTMLDivElement | null;
+ modes: SidebarMode[];
+ currentMode: SidebarMode;
+ onModeSelect: (mode: SidebarMode) => void;
+ isDragging?: boolean;
+}
+
+export function useModeDetection({
+ scrollContainer,
+ modes,
+ currentMode,
+ onModeSelect,
+ isDragging = false,
+}: UseModeDetectionOptions) {
+ // Detect when user finishes scrolling and update current mode
+ useEffect(() => {
+ if (!scrollContainer || isDragging) return;
+
+ let scrollEndTimer: NodeJS.Timeout | undefined;
+
+ const handleScroll = () => {
+ // Clear existing timeout
+ if (scrollEndTimer) {
+ clearTimeout(scrollEndTimer);
+ }
+
+ // Wait for scroll to settle before updating mode (reduces jitter)
+ scrollEndTimer = setTimeout(() => {
+ const finalScrollLeft = scrollContainer.scrollLeft;
+ const finalContainerWidth = scrollContainer.offsetWidth;
+
+ // Calculate which mode we're closest to and snap to it
+ const finalIndex = Math.round(finalScrollLeft / finalContainerWidth);
+
+ if (
+ finalIndex >= 0 &&
+ finalIndex < modes.length &&
+ modes[finalIndex]
+ ) {
+ // Snap to the nearest mode
+ const targetScrollX = finalIndex * finalContainerWidth;
+ if (Math.abs(finalScrollLeft - targetScrollX) > 5) {
+ scrollContainer.scrollTo({
+ left: targetScrollX,
+ behavior: "smooth",
+ });
+ }
+
+ // Update mode if it changed
+ if (modes[finalIndex] !== currentMode) {
+ onModeSelect(modes[finalIndex]);
+ }
+ }
+ }, 150);
+ };
+
+ scrollContainer.addEventListener("scroll", handleScroll, { passive: true });
+
+ return () => {
+ scrollContainer.removeEventListener("scroll", handleScroll);
+ if (scrollEndTimer) {
+ clearTimeout(scrollEndTimer);
+ }
+ };
+ }, [modes, currentMode, onModeSelect, scrollContainer, isDragging]);
+}
+
diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/hooks/useScrollProgress.ts b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/hooks/useScrollProgress.ts
new file mode 100644
index 00000000000..42d211285c2
--- /dev/null
+++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/hooks/useScrollProgress.ts
@@ -0,0 +1,59 @@
+import { type MotionValue, useMotionValue } from "framer-motion";
+import { useEffect } from "react";
+
+interface UseScrollProgressOptions {
+ scrollContainer: HTMLDivElement | null;
+ currentIndex: number;
+ onScrollProgress: (progress: MotionValue) => void;
+}
+
+export function useScrollProgress({
+ scrollContainer,
+ currentIndex,
+ onScrollProgress,
+}: UseScrollProgressOptions) {
+ const initialProgress = currentIndex >= 0 ? currentIndex : 0;
+ const modeProgress = useMotionValue(initialProgress);
+
+ // Track scroll position and update motion value
+ useEffect(() => {
+ if (!scrollContainer) return;
+
+ let rafId: number | undefined;
+
+ const updateProgress = () => {
+ const scrollLeft = scrollContainer.scrollLeft;
+ const containerWidth = scrollContainer.offsetWidth;
+ const progress = scrollLeft / containerWidth;
+ modeProgress.set(progress);
+ };
+
+ const handleScroll = () => {
+ // Use requestAnimationFrame for smooth updates
+ if (rafId !== undefined) {
+ cancelAnimationFrame(rafId);
+ }
+ rafId = requestAnimationFrame(updateProgress);
+ };
+
+ scrollContainer.addEventListener("scroll", handleScroll, { passive: true });
+
+ // Initial value
+ updateProgress();
+
+ return () => {
+ scrollContainer.removeEventListener("scroll", handleScroll);
+ if (rafId !== undefined) {
+ cancelAnimationFrame(rafId);
+ }
+ };
+ }, [scrollContainer, modeProgress]);
+
+ // Expose scroll progress to parent
+ useEffect(() => {
+ onScrollProgress(modeProgress);
+ }, [onScrollProgress, modeProgress]);
+
+ return modeProgress;
+}
+
diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/hooks/useScrollSnap.ts b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/hooks/useScrollSnap.ts
new file mode 100644
index 00000000000..9439dc24856
--- /dev/null
+++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/hooks/useScrollSnap.ts
@@ -0,0 +1,33 @@
+import { useEffect, useRef } from "react";
+import type { SidebarMode } from "../types";
+
+interface UseScrollSnapOptions {
+ scrollContainer: HTMLDivElement | null;
+ currentIndex: number;
+ isInitialMount: React.MutableRefObject;
+}
+
+export function useScrollSnap({
+ scrollContainer,
+ currentIndex,
+ isInitialMount,
+}: UseScrollSnapOptions) {
+ // Scroll to current mode when it changes externally
+ useEffect(() => {
+ if (!scrollContainer || currentIndex < 0) return;
+
+ const targetScrollX = currentIndex * scrollContainer.offsetWidth;
+
+ // Only scroll if we're not already at the target position
+ if (Math.abs(scrollContainer.scrollLeft - targetScrollX) > 5) {
+ scrollContainer.scrollTo({
+ left: targetScrollX,
+ behavior: isInitialMount.current ? "auto" : "smooth",
+ });
+ }
+
+ // Mark that initial mount is complete
+ isInitialMount.current = false;
+ }, [currentIndex, scrollContainer, isInitialMount]);
+}
+
diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/index.ts b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/index.ts
index 8af51e0302b..de0e8d8c3dc 100644
--- a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/index.ts
+++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/index.ts
@@ -1,2 +1,2 @@
-export { ModeCarousel, type SidebarMode } from "./ModeCarousel";
-
+export { ModeCarousel } from "./ModeCarousel";
+export type { SidebarMode, ModeCarouselProps } from "./types";
diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/types.ts b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/types.ts
new file mode 100644
index 00000000000..136c1ce7bf8
--- /dev/null
+++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/types.ts
@@ -0,0 +1,13 @@
+import type { MotionValue, ReactNode } from "react";
+
+export type SidebarMode = "tabs" | "diff";
+
+export interface ModeCarouselProps {
+ modes: SidebarMode[];
+ currentMode: SidebarMode;
+ onModeSelect: (mode: SidebarMode) => void;
+ children: (mode: SidebarMode, isActive: boolean) => ReactNode;
+ onScrollProgress: (progress: MotionValue) => void;
+ isDragging?: boolean;
+}
+