feat: enhance media panel tab navigation with scroll functionality
This commit is contained in:
@ -2,12 +2,75 @@
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Tab, tabs, useMediaPanelStore } from "./store";
|
import { Tab, tabs, useMediaPanelStore } from "./store";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ChevronRight, ChevronLeft } from "lucide-react";
|
||||||
|
import { useRef, useState, useEffect } from "react";
|
||||||
|
|
||||||
export function TabBar() {
|
export function TabBar() {
|
||||||
const { activeTab, setActiveTab } = useMediaPanelStore();
|
const { activeTab, setActiveTab } = useMediaPanelStore();
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isAtEnd, setIsAtEnd] = useState(false);
|
||||||
|
const [isAtStart, setIsAtStart] = useState(true);
|
||||||
|
|
||||||
|
const scrollToEnd = () => {
|
||||||
|
if (scrollContainerRef.current) {
|
||||||
|
scrollContainerRef.current.scrollTo({
|
||||||
|
left: scrollContainerRef.current.scrollWidth,
|
||||||
|
});
|
||||||
|
setIsAtEnd(true);
|
||||||
|
setIsAtStart(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToStart = () => {
|
||||||
|
if (scrollContainerRef.current) {
|
||||||
|
scrollContainerRef.current.scrollTo({
|
||||||
|
left: 0,
|
||||||
|
});
|
||||||
|
setIsAtStart(true);
|
||||||
|
setIsAtEnd(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkScrollPosition = () => {
|
||||||
|
if (scrollContainerRef.current) {
|
||||||
|
const { scrollLeft, scrollWidth, clientWidth } =
|
||||||
|
scrollContainerRef.current;
|
||||||
|
const isAtEndNow = scrollLeft + clientWidth >= scrollWidth - 1;
|
||||||
|
const isAtStartNow = scrollLeft <= 1;
|
||||||
|
setIsAtEnd(isAtEndNow);
|
||||||
|
setIsAtStart(isAtStartNow);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// We're using useEffect because we need to sync with external DOM scroll events
|
||||||
|
useEffect(() => {
|
||||||
|
const container = scrollContainerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
checkScrollPosition();
|
||||||
|
container.addEventListener("scroll", checkScrollPosition);
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(checkScrollPosition);
|
||||||
|
resizeObserver.observe(container);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
container.removeEventListener("scroll", checkScrollPosition);
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-12 bg-accent/50 px-3 flex justify-start items-center gap-6">
|
<div className="flex">
|
||||||
|
<ScrollButton
|
||||||
|
direction="left"
|
||||||
|
onClick={scrollToStart}
|
||||||
|
isVisible={!isAtStart}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
ref={scrollContainerRef}
|
||||||
|
className="h-12 bg-accent/50 px-3 flex justify-start items-center gap-5 overflow-x-auto scrollbar-x-hidden relative"
|
||||||
|
>
|
||||||
{(Object.keys(tabs) as Tab[]).map((tabKey) => {
|
{(Object.keys(tabs) as Tab[]).map((tabKey) => {
|
||||||
const tab = tabs[tabKey];
|
const tab = tabs[tabKey];
|
||||||
return (
|
return (
|
||||||
@ -19,11 +82,43 @@ export function TabBar() {
|
|||||||
onClick={() => setActiveTab(tabKey)}
|
onClick={() => setActiveTab(tabKey)}
|
||||||
key={tabKey}
|
key={tabKey}
|
||||||
>
|
>
|
||||||
<tab.icon className="!size-5" />
|
<tab.icon className="!size-[1.1rem]" />
|
||||||
<span className="text-[0.65rem]">{tab.label}</span>
|
<span className="text-[0.65rem]">{tab.label}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
<ScrollButton
|
||||||
|
direction="right"
|
||||||
|
onClick={scrollToEnd}
|
||||||
|
isVisible={!isAtEnd}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScrollButton({
|
||||||
|
direction,
|
||||||
|
onClick,
|
||||||
|
isVisible,
|
||||||
|
}: {
|
||||||
|
direction: "left" | "right";
|
||||||
|
onClick: () => void;
|
||||||
|
isVisible: boolean;
|
||||||
|
}) {
|
||||||
|
if (!isVisible) return null;
|
||||||
|
|
||||||
|
const Icon = direction === "left" ? ChevronLeft : ChevronRight;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-accent/50 w-12 h-full flex items-center justify-center">
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
className="rounded-[0.4rem] w-4 h-7 !bg-foreground/10"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<Icon className="!size-4 text-foreground" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user