Add swipe-to-open sidebar on all pages + fix bottom bar styling
- Create SwipeableContent component for sidebar swipe on non-tab pages - Add swipe-to-close sidebar from overlay - Make swipe work from entire page (ignoring interactive elements) - Show title and divider on desktop when tab bar is at bottom - Hide title/divider only on mobile for bottom position 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,11 @@ type SwipeTabsProps<T extends string | number> = {
|
||||
renderWindow?: number;
|
||||
scrollToTopOnChange?: boolean;
|
||||
onSwipePastStart?: () => void;
|
||||
// For 1:1 sidebar gesture tracking
|
||||
onSwipePastStartProgress?: (progress: number) => void;
|
||||
onSwipePastStartDragStart?: () => void;
|
||||
onSwipePastStartDragEnd?: (shouldOpen: boolean) => void;
|
||||
sidebarWidth?: number; // Width of sidebar for progress calculation
|
||||
};
|
||||
|
||||
const DRAG_START_DISTANCE = 3;
|
||||
@@ -59,7 +64,11 @@ export function SwipeTabs<T extends string | number>({
|
||||
threshold = DEFAULT_THRESHOLD,
|
||||
renderWindow = DEFAULT_RENDER_WINDOW,
|
||||
scrollToTopOnChange = true,
|
||||
onSwipePastStart
|
||||
onSwipePastStart,
|
||||
onSwipePastStartProgress,
|
||||
onSwipePastStartDragStart,
|
||||
onSwipePastStartDragEnd,
|
||||
sidebarWidth = 280
|
||||
}: SwipeTabsProps<T>) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const trackRef = useRef<HTMLDivElement>(null);
|
||||
@@ -271,6 +280,9 @@ export function SwipeTabs<T extends string | number>({
|
||||
[cancelAnimationForDrag, measureWidth, swipeDisabled, tabs.length]
|
||||
);
|
||||
|
||||
const sidebarDragStartedRef = useRef(false);
|
||||
const sidebarDragProgressRef = useRef(0);
|
||||
|
||||
const updateDrag = useCallback(
|
||||
(x: number, y: number, preventDefault: () => void, pointerType?: string) => {
|
||||
const state = dragRef.current;
|
||||
@@ -305,10 +317,27 @@ export function SwipeTabs<T extends string | number>({
|
||||
const atStart = state.startIndex <= 0;
|
||||
const atEnd = state.startIndex >= tabs.length - 1;
|
||||
// Don't apply resistance at start if onSwipePastStart is defined (sidebar can open)
|
||||
const hasVirtualTabAtStart = !!onSwipePastStart;
|
||||
if ((atStart && dx > 0 && !hasVirtualTabAtStart) || (atEnd && dx < 0)) {
|
||||
const hasVirtualTabAtStart = !!onSwipePastStart || !!onSwipePastStartProgress;
|
||||
|
||||
// Calculate sidebar drag progress when swiping right on first tab
|
||||
const isSidebarSwipe = atStart && dx > 0 && hasVirtualTabAtStart;
|
||||
if (isSidebarSwipe) {
|
||||
// Notify drag start
|
||||
if (!sidebarDragStartedRef.current) {
|
||||
sidebarDragStartedRef.current = true;
|
||||
onSwipePastStartDragStart?.();
|
||||
}
|
||||
// Calculate progress (0 to 1) based on sidebar width
|
||||
const progress = Math.min(1, dx / sidebarWidth);
|
||||
sidebarDragProgressRef.current = progress;
|
||||
onSwipePastStartProgress?.(progress);
|
||||
// Don't move the page - only the sidebar moves
|
||||
offset = 0;
|
||||
} else if ((atStart && dx > 0) || (atEnd && dx < 0)) {
|
||||
// Apply resistance at edges when not doing sidebar swipe
|
||||
offset = dx * EDGE_RESISTANCE;
|
||||
}
|
||||
|
||||
offset = Math.max(Math.min(offset, width), -width);
|
||||
dragOffsetRef.current = offset;
|
||||
scheduleDragUpdate();
|
||||
@@ -317,7 +346,7 @@ export function SwipeTabs<T extends string | number>({
|
||||
preventDefault();
|
||||
}
|
||||
},
|
||||
[onSwipePastStart, scheduleDragUpdate, tabs.length]
|
||||
[onSwipePastStart, onSwipePastStartProgress, onSwipePastStartDragStart, scheduleDragUpdate, sidebarWidth, tabs.length]
|
||||
);
|
||||
|
||||
const finishDrag = useCallback(
|
||||
@@ -334,6 +363,12 @@ export function SwipeTabs<T extends string | number>({
|
||||
isDraggingRef.current = false;
|
||||
setDraggingClass(false);
|
||||
|
||||
// Handle sidebar drag end
|
||||
const wasSidebarDragging = sidebarDragStartedRef.current;
|
||||
const sidebarProgress = sidebarDragProgressRef.current;
|
||||
sidebarDragStartedRef.current = false;
|
||||
sidebarDragProgressRef.current = 0;
|
||||
|
||||
if (!wasDragging) return;
|
||||
|
||||
if (rafRef.current) {
|
||||
@@ -346,12 +381,23 @@ export function SwipeTabs<T extends string | number>({
|
||||
const thresholdPx = width * threshold;
|
||||
let targetIndex = state.startIndex;
|
||||
|
||||
if (!cancelled && Math.abs(dx) > thresholdPx) {
|
||||
// Check if this was a sidebar gesture (using the flag, not dx which is 0 for sidebar swipes)
|
||||
const shouldOpenSidebar = !cancelled && sidebarProgress > 0.3; // 30% threshold
|
||||
|
||||
if (wasSidebarDragging) {
|
||||
// Call the new drag end callback with whether to open
|
||||
onSwipePastStartDragEnd?.(shouldOpenSidebar);
|
||||
|
||||
// Also call legacy callback if sidebar should open
|
||||
if (shouldOpenSidebar && onSwipePastStart) {
|
||||
onSwipePastStart();
|
||||
}
|
||||
} else if (!cancelled && Math.abs(dx) > thresholdPx) {
|
||||
const direction = dx < 0 ? 1 : -1;
|
||||
targetIndex = Math.min(Math.max(state.startIndex + direction, 0), tabs.length - 1);
|
||||
|
||||
// If swiping right on first tab, call onSwipePastStart
|
||||
if (state.startIndex === 0 && dx > 0 && onSwipePastStart) {
|
||||
// If swiping right on first tab without new callbacks, call onSwipePastStart
|
||||
if (state.startIndex === 0 && dx > 0 && onSwipePastStart && !onSwipePastStartDragEnd) {
|
||||
onSwipePastStart();
|
||||
}
|
||||
}
|
||||
@@ -372,7 +418,7 @@ export function SwipeTabs<T extends string | number>({
|
||||
}
|
||||
}
|
||||
},
|
||||
[activeTab, applyTransform, onTabChange, onSwipePastStart, setDraggingClass, tabs, threshold]
|
||||
[activeTab, applyTransform, onTabChange, onSwipePastStart, onSwipePastStartDragEnd, setDraggingClass, sidebarWidth, tabs, threshold]
|
||||
);
|
||||
|
||||
const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => {
|
||||
|
||||
Reference in New Issue
Block a user