From 0df4ec3f04682fc469fe7757fc10cc575ff3a56c Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 12 Nov 2025 00:09:46 -0800 Subject: [PATCH] modes --- .../main/components/Sidebar/Sidebar.tsx | 89 +++++---- .../components/ModeCarousel/ModeCarousel.tsx | 186 ++++++++++++++++++ .../Sidebar/components/ModeCarousel/index.ts | 2 + .../components/ModeSwitcher/ModeSwitcher.tsx | 61 ++++++ .../Sidebar/components/ModeSwitcher/index.ts | 2 + 5 files changed, 301 insertions(+), 39 deletions(-) create mode 100644 apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/ModeCarousel.tsx create mode 100644 apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/index.ts create mode 100644 apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeSwitcher/ModeSwitcher.tsx create mode 100644 apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeSwitcher/index.ts diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx index 000bf34dfd9..fd9738b7357 100644 --- a/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx @@ -4,12 +4,10 @@ import type { Tab, Workspace, Worktree } from "shared/types"; import { CreateWorktreeButton, CreateWorktreeModal, - SidebarHeader, - WorkspaceCarousel, - WorkspacePortIndicator, - WorkspaceSwitcher, WorktreeList, } from "./components"; +import { ModeCarousel, type SidebarMode } from "./components/ModeCarousel"; +import { ModeSwitcher } from "./components/ModeSwitcher"; interface SidebarProps { workspaces: Workspace[]; @@ -52,17 +50,16 @@ export function Sidebar({ const [description, setDescription] = useState(""); const [setupStatus, setSetupStatus] = useState(undefined); const [setupOutput, setSetupOutput] = useState(undefined); + const [currentMode, setCurrentMode] = useState("tabs"); - // Initialize with current workspace index - const currentIndex = workspaces.findIndex( - (w) => w.id === currentWorkspace?.id, - ); - const initialIndex = currentIndex >= 0 ? currentIndex : 0; - const defaultScrollProgress = useMotionValue(initialIndex); + // Initialize scroll progress + const defaultScrollProgress = useMotionValue(0); const [scrollProgress, setScrollProgress] = useState>( defaultScrollProgress, ); + const modes: SidebarMode[] = ["tabs", "diff"]; + // Auto-expand worktree if it contains the selected tab useEffect(() => { if (currentWorkspace && selectedTabId) { @@ -291,40 +288,54 @@ export function Sidebar({ return (
- + - {(workspace, isActive) => ( - <> - + {(mode, isActive) => { + if (mode === "diff") { + // Diff mode - empty for now + return
; + } - {workspace && ( - + - )} - - )} - + + {currentWorkspace && ( + + )} + + ); + }} + void; + children: (mode: SidebarMode, isActive: boolean) => ReactNode; + onScrollProgress: (progress: MotionValue) => void; + isDragging?: boolean; +} + +export function ModeCarousel({ + modes, + currentMode, + onModeSelect, + children, + onScrollProgress, + isDragging = false, +}: ModeCarouselProps) { + const scrollTimeoutRef = useRef(undefined); + const isInitialMount = useRef(true); + + const currentIndex = modes.findIndex((m) => m === currentMode); + const initialProgress = currentIndex >= 0 ? currentIndex : 0; + const modeProgress = useMotionValue(initialProgress); + + const [scrollContainer, setScrollContainer] = useState( + null, + ); + + // Use callback ref to get notified when the ref is attached + const scrollContainerRef = useCallback((node: HTMLDivElement | null) => { + if (node) { + setScrollContainer(node); + } + }, []); + + // 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]); + + // 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) > 10) { + scrollContainer.scrollTo({ + left: targetScrollX, + behavior: isInitialMount.current ? "auto" : "smooth", + }); + } + + // Mark that initial mount is complete + isInitialMount.current = false; + }, [currentIndex, scrollContainer]); + + // Detect when user finishes scrolling and update current mode + useEffect(() => { + if (!scrollContainer || isDragging) return; + + const handleScroll = () => { + // Clear existing timeout + if (scrollTimeoutRef.current) { + clearTimeout(scrollTimeoutRef.current); + } + + // Wait for scroll to settle (150ms after last scroll event) + scrollTimeoutRef.current = setTimeout(() => { + const scrollLeft = scrollContainer.scrollLeft; + const containerWidth = scrollContainer.offsetWidth; + + // Calculate which mode we're closest to + const newIndex = Math.round(scrollLeft / containerWidth); + + // Update mode if it changed + if ( + newIndex >= 0 && + newIndex < modes.length && + modes[newIndex] && + modes[newIndex] !== currentMode + ) { + onModeSelect(modes[newIndex]); + } + }, 150); + }; + + scrollContainer.addEventListener("scroll", handleScroll); + + return () => { + scrollContainer.removeEventListener("scroll", handleScroll); + if (scrollTimeoutRef.current) { + clearTimeout(scrollTimeoutRef.current); + } + }; + }, [modes, currentMode, onModeSelect, scrollContainer, isDragging]); + + // If only one mode or no modes, disable carousel + if (modes.length <= 1) { + return ( +
+ {children(currentMode, true)} +
+ ); + } + + return ( +
+
+ {modes.map((mode) => ( +
+ {children(mode, mode === currentMode)} +
+ ))} +
+
+ ); +} + 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 new file mode 100644 index 00000000000..8af51e0302b --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeCarousel/index.ts @@ -0,0 +1,2 @@ +export { ModeCarousel, type SidebarMode } from "./ModeCarousel"; + diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeSwitcher/ModeSwitcher.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeSwitcher/ModeSwitcher.tsx new file mode 100644 index 00000000000..070901c0aa1 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeSwitcher/ModeSwitcher.tsx @@ -0,0 +1,61 @@ +import { Button } from "@superset/ui/button"; +import { type MotionValue, motion, useTransform } from "framer-motion"; +import type { SidebarMode } from "../ModeCarousel"; + +interface ModeSwitcherProps { + modes: SidebarMode[]; + currentMode: SidebarMode; + onModeSelect: (mode: SidebarMode) => void; + scrollProgress: MotionValue; +} + +const modeLabels: Record = { + tabs: "Tabs", + diff: "Diffs", +}; + +export function ModeSwitcher({ + modes, + currentMode, + onModeSelect, + scrollProgress, +}: ModeSwitcherProps) { + // Calculate sliding background position from scroll progress + // scrollProgress is 0-1 (0 = tabs, 1 = diff), and we have 2 modes, so each mode is 50% width + // Transform to percentage: 0 -> 0%, 1 -> 50% + const backgroundX = useTransform(scrollProgress, (value) => `${value * 50}%`); + + return ( +
+
+ {/* Sliding background indicator */} + + + {modes.map((mode) => ( + + ))} +
+
+ ); +} + diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeSwitcher/index.ts b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeSwitcher/index.ts new file mode 100644 index 00000000000..78e1204bc2f --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/ModeSwitcher/index.ts @@ -0,0 +1,2 @@ +export { ModeSwitcher } from "./ModeSwitcher"; +