1
0
Fork 0

Swipe Navigation

This commit is contained in:
Pabloader 2026-04-09 14:12:44 +00:00
parent 3d94a298b8
commit 3074d6dd8d
2 changed files with 82 additions and 1 deletions

View File

@ -0,0 +1,65 @@
import type { RefObject } from "preact";
import { useEffect, useRef } from "preact/hooks";
const EDGE_THRESHOLD = 30; // px from screen edge to start a swipe
const MIN_SWIPE = 50; // minimum horizontal distance to trigger
export function useSwipeNavigation(
elementRef: RefObject<HTMLElement>,
onSwipeLeft: () => void,
onSwipeRight: () => void,
) {
// Keep callbacks in a ref so the effect doesn't re-run when they change
const callbacksRef = useRef({ onSwipeLeft, onSwipeRight });
callbacksRef.current = { onSwipeLeft, onSwipeRight };
useEffect(() => {
const el = elementRef.current;
if (!el) return;
let start: { x: number; y: number } | null = null;
const onTouchStart = (e: TouchEvent) => {
const t = e.touches[0];
const fromEdge = t.clientX < EDGE_THRESHOLD
|| t.clientX > window.innerWidth - EDGE_THRESHOLD;
start = fromEdge ? { x: t.clientX, y: t.clientY } : null;
if (fromEdge) {
e.preventDefault(); // Prevent scrolling on swipe
}
};
const onTouchMove = (e: TouchEvent) => {
if (!start) return;
const t = e.touches[0];
const dx = Math.abs(t.clientX - start.x);
const dy = Math.abs(t.clientY - start.y);
// Prevent Chrome's back/forward navigation once we're sure it's horizontal
if (dx > dy && dx > 10) e.preventDefault();
};
const onTouchEnd = (e: TouchEvent) => {
if (!start) return;
const t = e.changedTouches[0];
const dx = t.clientX - start.x;
const dy = t.clientY - start.y;
start = null;
if (Math.abs(dx) < MIN_SWIPE || Math.abs(dx) < Math.abs(dy)) return;
e.preventDefault(); // prevent Chrome's back/forward navigation
if (dx > 0) callbacksRef.current.onSwipeRight();
else callbacksRef.current.onSwipeLeft();
};
el.addEventListener('touchstart', onTouchStart, { passive: false });
el.addEventListener('touchmove', onTouchMove, { passive: false });
el.addEventListener('touchend', onTouchEnd, { passive: false });
return () => {
el.removeEventListener('touchstart', onTouchStart);
el.removeEventListener('touchmove', onTouchMove);
el.removeEventListener('touchend', onTouchEnd);
};
}, [elementRef]);
}

View File

@ -1,5 +1,6 @@
import { highlight } from "@common/highlight"; import { highlight } from "@common/highlight";
import { useMediaQuery } from "@common/hooks/useMediaQuery"; import { useMediaQuery } from "@common/hooks/useMediaQuery";
import { useSwipeNavigation } from "@common/hooks/useSwipeNavigation";
import clsx from "clsx"; import clsx from "clsx";
import { BookMarked, BookOpen, BrainCircuit, Code, FileText, Globe, Layers, List, MapPin, MessageSquare, MessagesSquare, Users, type LucideIcon } from "lucide-preact"; import { BookMarked, BookOpen, BrainCircuit, Code, FileText, Globe, Layers, List, MapPin, MessageSquare, MessagesSquare, Users, type LucideIcon } from "lucide-preact";
import { useEffect, useMemo, useRef } from "preact/hooks"; import { useEffect, useMemo, useRef } from "preact/hooks";
@ -62,6 +63,7 @@ export const Editor = () => {
}; };
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<HTMLDivElement>(null);
const promptPreview = useMemo(() => { const promptPreview = useMemo(() => {
const text = Prompt.formatSystemPrompt(appState); const text = Prompt.formatSystemPrompt(appState);
@ -93,6 +95,20 @@ export const Editor = () => {
const isMobile = useMediaQuery(`(max-width: ${MOBILE_BREAKPOINT}px)`); const isMobile = useMediaQuery(`(max-width: ${MOBILE_BREAKPOINT}px)`);
useSwipeNavigation(
editorRef,
() => { // swipe left → next tab
const idx = tabs.findIndex(t => t.id === currentTab);
const next = tabs[idx + 1];
if (next) dispatch({ type: 'SET_CURRENT_TAB', tab: next.id });
},
() => { // swipe right → prev tab
const idx = tabs.findIndex(t => t.id === currentTab);
const prev = tabs[idx - 1];
if (prev) dispatch({ type: 'SET_CURRENT_TAB', tab: prev.id });
},
);
const hasSelection = currentWorld !== null; const hasSelection = currentWorld !== null;
const isChatOnly = currentWorld?.chatOnly ?? false; const isChatOnly = currentWorld?.chatOnly ?? false;
@ -117,7 +133,7 @@ export const Editor = () => {
: null; : null;
return ( return (
<div class={styles.editor}> <div class={styles.editor} ref={editorRef}>
{titleBar} {titleBar}
<div class={clsx( <div class={clsx(
styles.content, styles.content,